feat: add HTML visualization reports with Plotly.js charts

Add a `visualization` feature flag that generates self-contained HTML
reports with interactive Plotly.js charts for offline visualization of
optimization results. Charts include optimization history, slice plots,
parallel coordinates, parameter importance, trial timeline, and
intermediate values (learning curves).
This commit is contained in:
Manuel Raimann
2026-02-11 20:12:35 +01:00
parent 0e54356345
commit e359392a00
7 changed files with 716 additions and 0 deletions
+2
View File
@@ -80,6 +80,8 @@ jobs:
run: cargo run --example parameter_api --features derive
- name: Run async example (async_api_optimization)
run: cargo run --example async_api_optimization --features async
- name: Run visualization example (visualization_demo)
run: cargo run --example visualization_demo --features visualization
docs:
name: Docs
+6
View File
@@ -35,6 +35,7 @@ serde = ["dep:serde", "dep:serde_json"]
tracing = ["dep:tracing"]
sobol = ["dep:sobol_burley"]
cma-es = ["dep:nalgebra"]
visualization = []
[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
@@ -63,3 +64,8 @@ path = "examples/ml_hyperparameter_tuning.rs"
name = "parameter_api"
path = "examples/parameter_api.rs"
required-features = ["derive"]
[[example]]
name = "visualization_demo"
path = "examples/visualization_demo.rs"
required-features = ["visualization"]
+45
View File
@@ -0,0 +1,45 @@
use optimizer::prelude::*;
fn main() {
// Multi-parameter optimization with TPE sampler.
let sampler = TpeSampler::builder().seed(42).build().unwrap();
let mut study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
study.set_pruner(MedianPruner::new(Direction::Minimize));
let lr = FloatParam::new(1e-5, 1e-1)
.log_scale()
.name("learning_rate");
let n_layers = IntParam::new(1, 5).name("n_layers");
let dropout = FloatParam::new(0.0, 0.5).step(0.05).name("dropout");
let batch_size = CategoricalParam::new(vec![16, 32, 64, 128]).name("batch_size");
study
.optimize(80, |trial| {
let lr_val = lr.suggest(trial)?;
let layers = n_layers.suggest(trial)?;
let drop = dropout.suggest(trial)?;
let bs = batch_size.suggest(trial)?;
// Simulate training with intermediate reporting.
let mut loss = 1.0;
for epoch in 0..10 {
loss *= 0.7 + 0.3 * lr_val.ln().abs() / 12.0;
loss += drop * 0.05;
loss += (1.0 / bs as f64) * 0.1;
loss -= layers as f64 * 0.02;
trial.report(epoch, loss);
if trial.should_prune() {
return Err(TrialPruned.into());
}
}
Ok::<_, Error>(loss)
})
.unwrap();
println!("{}", study.summary());
let path = "optimization_report.html";
generate_html_report(&study, path).unwrap();
println!("\nReport saved to {path}");
}
+7
View File
@@ -190,6 +190,7 @@
//! - `serde`: Enable `Serialize`/`Deserialize` on public types and `Study::save()`/`Study::load()`
//! - `sobol`: Enable the Sobol quasi-random sampler for better space coverage
//! - `cma-es`: Enable the CMA-ES sampler for continuous optimization
//! - `visualization`: Generate self-contained HTML reports with interactive Plotly.js charts
//! - `tracing`: Emit structured log events via the [`tracing`](https://docs.rs/tracing) crate at key optimization points
/// Emit a `tracing::info!` event when the `tracing` feature is enabled.
@@ -229,6 +230,8 @@ pub mod sampler;
mod study;
mod trial;
mod types;
#[cfg(feature = "visualization")]
mod visualization;
pub use error::{Error, Result, TrialPruned};
pub use multi_objective::{MultiObjectiveSampler, MultiObjectiveStudy, MultiObjectiveTrial};
@@ -258,6 +261,8 @@ pub use study::Study;
pub use study::StudySnapshot;
pub use trial::{AttrValue, Trial};
pub use types::{Direction, TrialState};
#[cfg(feature = "visualization")]
pub use visualization::generate_html_report;
/// Convenient wildcard import for the most common types.
///
@@ -294,4 +299,6 @@ pub mod prelude {
pub use crate::study::StudySnapshot;
pub use crate::trial::{AttrValue, Trial};
pub use crate::types::Direction;
#[cfg(feature = "visualization")]
pub use crate::visualization::generate_html_report;
}
+16
View File
@@ -2154,6 +2154,22 @@ impl Study<f64> {
}
}
#[cfg(feature = "visualization")]
impl Study<f64> {
/// Generates an HTML report with interactive Plotly.js charts.
///
/// Creates a self-contained HTML file that can be opened in any browser.
/// See [`generate_html_report`](crate::visualization::generate_html_report)
/// for details on the included charts.
///
/// # Errors
///
/// Returns an I/O error if the file cannot be created or written.
pub fn export_html(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
crate::visualization::generate_html_report(self, path)
}
}
/// A serializable snapshot of a study's state.
///
/// Since [`Study`] contains non-serializable fields (samplers, atomics, etc.),
+458
View File
@@ -0,0 +1,458 @@
//! HTML report generation for optimization visualization.
//!
//! Generates self-contained HTML files with embedded Plotly.js charts
//! for offline visualization of optimization results.
use core::fmt::Write as _;
use std::collections::BTreeMap;
use std::path::Path;
use crate::distribution::Distribution;
use crate::param::ParamValue;
use crate::parameter::ParamId;
use crate::sampler::CompletedTrial;
use crate::types::{Direction, TrialState};
/// Generate an HTML report with interactive Plotly.js charts.
///
/// Creates a self-contained HTML file at `path` containing:
/// - **Optimization history**: Objective value vs trial number with best-so-far line
/// - **Slice plots**: Objective value vs each parameter (1D scatter)
/// - **Parallel coordinates**: Multi-parameter relationship view
/// - **Trial timeline**: Duration index of each trial (horizontal bar)
/// - **Intermediate values**: Learning curves per trial (if pruning data available)
/// - **Parameter importance**: Bar chart (if enough completed trials)
///
/// # Errors
///
/// Returns an I/O error if the file cannot be created or written.
pub fn generate_html_report(
study: &crate::Study<f64>,
path: impl AsRef<Path>,
) -> std::io::Result<()> {
let trials = study.trials();
let direction = study.direction();
let importance = study.param_importance();
let html = build_html(&trials, direction, &importance);
std::fs::write(path, html)
}
fn build_html(
trials: &[CompletedTrial<f64>],
direction: Direction,
importance: &[(String, f64)],
) -> String {
let mut html = String::with_capacity(8192);
let dir_label = match direction {
Direction::Minimize => "Minimize",
Direction::Maximize => "Maximize",
};
// Collect parameter metadata.
let param_info = collect_param_info(trials);
let has_intermediate = trials.iter().any(|t| !t.intermediate_values.is_empty());
let _ = write!(
html,
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Optimization Report</title>
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f6fa; color: #2c3e50; padding: 24px; }}
h1 {{ text-align: center; margin-bottom: 8px; font-size: 1.8em; }}
.subtitle {{ text-align: center; color: #7f8c8d; margin-bottom: 24px; }}
.chart {{ background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 24px; padding: 16px; }}
.chart-title {{ font-size: 1.1em; font-weight: 600; margin-bottom: 8px; }}
</style>
</head>
<body>
<h1>Optimization Report</h1>
<p class="subtitle">{dir_label} &middot; {n} trials</p>
"#,
n = trials.len(),
);
// Optimization history chart.
html.push_str("<div class=\"chart\"><div class=\"chart-title\">Optimization History</div><div id=\"history\"></div></div>\n");
write_history_chart(&mut html, trials, direction);
// Slice plots.
if !param_info.is_empty() {
html.push_str("<div class=\"chart\"><div class=\"chart-title\">Slice Plots</div><div id=\"slices\"></div></div>\n");
write_slice_charts(&mut html, trials, &param_info);
}
// Parallel coordinates.
if param_info.len() >= 2 {
html.push_str("<div class=\"chart\"><div class=\"chart-title\">Parallel Coordinates</div><div id=\"parcoords\"></div></div>\n");
write_parallel_coordinates(&mut html, trials, &param_info, direction);
}
// Parameter importance.
if !importance.is_empty() {
html.push_str("<div class=\"chart\"><div class=\"chart-title\">Parameter Importance</div><div id=\"importance\"></div></div>\n");
write_importance_chart(&mut html, importance);
}
// Trial timeline.
html.push_str("<div class=\"chart\"><div class=\"chart-title\">Trial Timeline</div><div id=\"timeline\"></div></div>\n");
write_timeline_chart(&mut html, trials);
// Intermediate values.
if has_intermediate {
html.push_str("<div class=\"chart\"><div class=\"chart-title\">Intermediate Values</div><div id=\"intermediate\"></div></div>\n");
write_intermediate_chart(&mut html, trials);
}
html.push_str("</body>\n</html>\n");
html
}
/// Metadata about each parameter seen across trials.
struct ParamMeta {
label: String,
#[allow(dead_code)]
dist: Option<Distribution>,
}
/// Collect parameter labels and distributions across all trials.
fn collect_param_info(trials: &[CompletedTrial<f64>]) -> BTreeMap<ParamId, ParamMeta> {
let mut info: BTreeMap<ParamId, ParamMeta> = BTreeMap::new();
for trial in trials {
for &id in trial.params.keys() {
info.entry(id).or_insert_with(|| {
let label = trial
.param_labels
.get(&id)
.cloned()
.unwrap_or_else(|| id.to_string());
let dist = trial.distributions.get(&id).cloned();
ParamMeta { label, dist }
});
}
}
info
}
// ---------------------------------------------------------------------------
// Chart generators
// ---------------------------------------------------------------------------
fn write_history_chart(html: &mut String, trials: &[CompletedTrial<f64>], direction: Direction) {
let complete: Vec<_> = trials
.iter()
.filter(|t| t.state == TrialState::Complete)
.collect();
if complete.is_empty() {
return;
}
let mut ids = Vec::with_capacity(complete.len());
let mut vals = Vec::with_capacity(complete.len());
let mut best_vals = Vec::with_capacity(complete.len());
let mut best = complete[0].value;
for t in &complete {
ids.push(t.id);
vals.push(t.value);
best = match direction {
Direction::Minimize => best.min(t.value),
Direction::Maximize => best.max(t.value),
};
best_vals.push(best);
}
let _ = write!(
html,
r##"<script>
Plotly.newPlot("history", [
{{ x: {ids:?}, y: {vals:?}, mode: "markers", name: "Objective", type: "scatter",
marker: {{ color: "#3498db", size: 6 }} }},
{{ x: {ids:?}, y: {best_vals:?}, mode: "lines", name: "Best so far", type: "scatter",
line: {{ color: "#e74c3c", width: 2 }} }}
], {{ xaxis: {{ title: "Trial" }}, yaxis: {{ title: "Objective Value" }},
margin: {{ t: 10 }}, legend: {{ x: 1, xanchor: "right", y: 1 }} }},
{{ responsive: true }});
</script>
"##,
);
}
fn write_slice_charts(
html: &mut String,
trials: &[CompletedTrial<f64>],
param_info: &BTreeMap<ParamId, ParamMeta>,
) {
let complete: Vec<_> = trials
.iter()
.filter(|t| t.state == TrialState::Complete)
.collect();
if complete.is_empty() {
return;
}
let n_params = param_info.len();
let cols = if n_params <= 2 { n_params } else { 2 };
let rows = n_params.div_ceil(cols);
// Build subplot titles and data.
let mut subplot_titles = Vec::new();
let mut traces = String::new();
for (i, (id, meta)) in param_info.iter().enumerate() {
subplot_titles.push(format!("\"{}\"", escape_js(&meta.label)));
let mut x_vals = Vec::new();
let mut y_vals = Vec::new();
for t in &complete {
if let Some(pv) = t.params.get(id) {
x_vals.push(param_value_to_f64(pv));
y_vals.push(t.value);
}
}
let subplot_idx = i + 1;
let xa = if subplot_idx == 1 {
"x".to_string()
} else {
format!("x{subplot_idx}")
};
let ya = if subplot_idx == 1 {
"y".to_string()
} else {
format!("y{subplot_idx}")
};
let _ = write!(
traces,
r##"{{ x: {x_vals:?}, y: {y_vals:?}, mode: "markers", type: "scatter",
xaxis: "{xa}", yaxis: "{ya}",
marker: {{ color: "#3498db", size: 5 }}, showlegend: false }},"##,
);
}
let _ = write!(
html,
r#"<script>
Plotly.newPlot("slices", [{traces}],
{{ grid: {{ rows: {rows}, columns: {cols}, pattern: "independent" }},
annotations: [{annotations}],
margin: {{ t: 30 }}, showlegend: false }},
{{ responsive: true }});
</script>
"#,
annotations = build_subplot_annotations(&subplot_titles, rows, cols),
);
}
fn write_parallel_coordinates(
html: &mut String,
trials: &[CompletedTrial<f64>],
param_info: &BTreeMap<ParamId, ParamMeta>,
direction: Direction,
) {
let complete: Vec<_> = trials
.iter()
.filter(|t| t.state == TrialState::Complete)
.collect();
if complete.is_empty() {
return;
}
let mut dimensions = String::new();
// Add objective value as the first dimension.
let obj_vals: Vec<f64> = complete.iter().map(|t| t.value).collect();
let _ = write!(
dimensions,
r#"{{ label: "Objective", values: {obj_vals:?} }},"#,
);
// Add each parameter as a dimension.
for (id, meta) in param_info {
let vals: Vec<f64> = complete
.iter()
.map(|t| t.params.get(id).map_or(f64::NAN, param_value_to_f64))
.collect();
let _ = write!(
dimensions,
r#"{{ label: "{label}", values: {vals:?} }},"#,
label = escape_js(&meta.label),
);
}
// Color by objective value: green = good, red = bad.
let (cmin, cmax) = min_max(&obj_vals);
let colorscale = match direction {
Direction::Minimize => r##"[[0,"#2ecc71"],[1,"#e74c3c"]]"##,
Direction::Maximize => r##"[[0,"#e74c3c"],[1,"#2ecc71"]]"##,
};
let _ = write!(
html,
r#"<script>
Plotly.newPlot("parcoords", [{{
type: "parcoords",
line: {{ color: {obj_vals:?}, colorscale: {colorscale},
cmin: {cmin}, cmax: {cmax}, showscale: true }},
dimensions: [{dimensions}]
}}], {{ margin: {{ t: 10 }} }}, {{ responsive: true }});
</script>
"#,
);
}
fn write_importance_chart(html: &mut String, importance: &[(String, f64)]) {
let names: Vec<_> = importance.iter().map(|(n, _)| format!("\"{n}\"")).collect();
let values: Vec<f64> = importance.iter().map(|(_, v)| *v).collect();
let _ = write!(
html,
r##"<script>
Plotly.newPlot("importance", [{{
x: {values:?}, y: [{names}], type: "bar", orientation: "h",
marker: {{ color: "#9b59b6" }}
}}], {{ xaxis: {{ title: "Importance (|Spearman correlation|)" }},
yaxis: {{ automargin: true }}, margin: {{ t: 10, l: 120 }} }},
{{ responsive: true }});
</script>
"##,
names = names.join(","),
);
}
fn write_timeline_chart(html: &mut String, trials: &[CompletedTrial<f64>]) {
let mut ids = Vec::with_capacity(trials.len());
let mut colors = Vec::with_capacity(trials.len());
let mut labels = Vec::with_capacity(trials.len());
for t in trials {
ids.push(format!("\"Trial {}\"", t.id));
let (color, label) = match t.state {
TrialState::Complete => ("#2ecc71", "Complete"),
TrialState::Pruned => ("#f39c12", "Pruned"),
TrialState::Failed => ("#e74c3c", "Failed"),
TrialState::Running => ("#3498db", "Running"),
};
colors.push(format!("\"{color}\""));
labels.push(format!("\"{label}\""));
}
// Use trial index as a proxy for duration (no wallclock data available).
let indices: Vec<usize> = (0..trials.len()).collect();
let _ = write!(
html,
r#"<script>
Plotly.newPlot("timeline", [{{
y: [{ids}], x: {indices:?}, type: "bar", orientation: "h",
text: [{labels}], textposition: "auto",
marker: {{ color: [{colors}] }}
}}], {{ xaxis: {{ title: "Trial Index" }}, yaxis: {{ automargin: true, autorange: "reversed" }},
margin: {{ t: 10, l: 80 }}, showlegend: false }},
{{ responsive: true }});
</script>
"#,
ids = ids.join(","),
colors = colors.join(","),
labels = labels.join(","),
);
}
fn write_intermediate_chart(html: &mut String, trials: &[CompletedTrial<f64>]) {
let trials_with_iv: Vec<_> = trials
.iter()
.filter(|t| !t.intermediate_values.is_empty())
.collect();
if trials_with_iv.is_empty() {
return;
}
let mut traces = String::new();
for t in &trials_with_iv {
let steps: Vec<u64> = t.intermediate_values.iter().map(|(s, _)| *s).collect();
let values: Vec<f64> = t.intermediate_values.iter().map(|(_, v)| *v).collect();
let color = match t.state {
TrialState::Pruned => "#f39c12",
_ => "#3498db",
};
let _ = write!(
traces,
r#"{{ x: {steps:?}, y: {values:?}, mode: "lines+markers", name: "Trial {id}",
line: {{ color: "{color}", width: 1 }}, marker: {{ size: 3 }} }},"#,
id = t.id,
);
}
let _ = write!(
html,
r#"<script>
Plotly.newPlot("intermediate", [{traces}],
{{ xaxis: {{ title: "Step" }}, yaxis: {{ title: "Intermediate Value" }},
margin: {{ t: 10 }}, showlegend: true }},
{{ responsive: true }});
</script>
"#,
);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
#[allow(clippy::cast_precision_loss)]
fn param_value_to_f64(pv: &ParamValue) -> f64 {
match *pv {
ParamValue::Float(v) => v,
ParamValue::Int(v) => v as f64,
ParamValue::Categorical(v) => v as f64,
}
}
fn escape_js(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
fn min_max(vals: &[f64]) -> (f64, f64) {
let mut mn = f64::INFINITY;
let mut mx = f64::NEG_INFINITY;
for &v in vals {
if v < mn {
mn = v;
}
if v > mx {
mx = v;
}
}
(mn, mx)
}
/// Build Plotly annotation objects to act as subplot titles.
#[allow(clippy::cast_precision_loss)]
fn build_subplot_annotations(titles: &[String], rows: usize, cols: usize) -> String {
let mut anns = Vec::new();
for (i, title) in titles.iter().enumerate() {
let row = i / cols;
let col = i % cols;
// Compute x/y anchor in paper coordinates.
let x = if cols == 1 {
0.5
} else {
(f64::from(u32::try_from(col).unwrap_or(0))) / (cols as f64 - 1.0)
};
let y = 1.0 - (f64::from(u32::try_from(row).unwrap_or(0))) / (rows as f64).max(1.0) + 0.02;
anns.push(format!(
r#"{{ text: {title}, x: {x:.3}, y: {y:.3}, xref: "paper", yref: "paper",
showarrow: false, font: {{ size: 12 }} }}"#,
));
}
anns.join(",")
}
+182
View File
@@ -0,0 +1,182 @@
#![cfg(feature = "visualization")]
use optimizer::parameter::{FloatParam, IntParam, Parameter};
use optimizer::sampler::random::RandomSampler;
use optimizer::{Direction, Study, generate_html_report};
#[test]
fn html_report_creates_file() {
let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42));
let x = FloatParam::new(0.0, 10.0).name("x");
let y = IntParam::new(1, 5).name("y");
study
.optimize(10, |trial| {
let xv = x.suggest(trial)?;
let yv = y.suggest(trial)?;
Ok::<_, optimizer::Error>(xv + yv as f64)
})
.unwrap();
let path = std::env::temp_dir().join("test_report_creates_file.html");
generate_html_report(&study, &path).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("<!DOCTYPE html>"));
assert!(content.contains("plotly"));
std::fs::remove_file(&path).ok();
}
#[test]
fn html_report_contains_all_chart_sections() {
let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42));
let x = FloatParam::new(0.0, 10.0).name("x");
let y = FloatParam::new(-5.0, 5.0).name("y");
study
.optimize(20, |trial| {
let xv = x.suggest(trial)?;
let yv = y.suggest(trial)?;
Ok::<_, optimizer::Error>(xv * xv + yv * yv)
})
.unwrap();
let path = std::env::temp_dir().join("test_report_all_charts.html");
generate_html_report(&study, &path).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
// Should contain all chart divs.
assert!(content.contains("id=\"history\""));
assert!(content.contains("id=\"slices\""));
assert!(content.contains("id=\"parcoords\""));
assert!(content.contains("id=\"importance\""));
assert!(content.contains("id=\"timeline\""));
// Should contain chart titles.
assert!(content.contains("Optimization History"));
assert!(content.contains("Slice Plots"));
assert!(content.contains("Parallel Coordinates"));
assert!(content.contains("Parameter Importance"));
assert!(content.contains("Trial Timeline"));
// Should show direction and trial count.
assert!(content.contains("Minimize"));
assert!(content.contains("20 trials"));
std::fs::remove_file(&path).ok();
}
#[test]
fn html_report_empty_study() {
let study: Study<f64> = Study::new(Direction::Minimize);
let path = std::env::temp_dir().join("test_report_empty.html");
generate_html_report(&study, &path).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("<!DOCTYPE html>"));
assert!(content.contains("0 trials"));
std::fs::remove_file(&path).ok();
}
#[test]
fn html_report_single_param_no_parcoords() {
let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42));
let x = FloatParam::new(0.0, 10.0).name("x");
study
.optimize(5, |trial| {
let xv = x.suggest(trial)?;
Ok::<_, optimizer::Error>(xv * xv)
})
.unwrap();
let path = std::env::temp_dir().join("test_report_single_param.html");
generate_html_report(&study, &path).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
// Should have slice plot but not parallel coordinates (needs >= 2 params).
assert!(content.contains("id=\"slices\""));
assert!(!content.contains("id=\"parcoords\""));
std::fs::remove_file(&path).ok();
}
#[test]
fn html_report_maximize_direction() {
let study: Study<f64> = Study::with_sampler(Direction::Maximize, RandomSampler::with_seed(42));
let x = FloatParam::new(0.0, 10.0).name("x");
study
.optimize(5, |trial| {
let xv = x.suggest(trial)?;
Ok::<_, optimizer::Error>(xv)
})
.unwrap();
let path = std::env::temp_dir().join("test_report_maximize.html");
generate_html_report(&study, &path).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("Maximize"));
std::fs::remove_file(&path).ok();
}
#[test]
fn export_html_convenience_method() {
let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42));
let x = FloatParam::new(0.0, 10.0).name("x");
study
.optimize(5, |trial| {
let xv = x.suggest(trial)?;
Ok::<_, optimizer::Error>(xv * xv)
})
.unwrap();
let path = std::env::temp_dir().join("test_export_html.html");
study.export_html(&path).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("<!DOCTYPE html>"));
assert!(content.contains("id=\"history\""));
std::fs::remove_file(&path).ok();
}
#[test]
fn html_report_with_intermediate_values() {
use optimizer::pruner::MedianPruner;
let mut study: Study<f64> =
Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42));
study.set_pruner(MedianPruner::new(Direction::Minimize));
let x = FloatParam::new(0.0, 10.0).name("x");
study
.optimize(10, |trial| {
let xv = x.suggest(trial)?;
for step in 0..5 {
let val = xv * xv + step as f64;
trial.report(step, val);
if trial.should_prune() {
return Err(optimizer::TrialPruned.into());
}
}
Ok::<_, optimizer::Error>(xv * xv)
})
.unwrap();
let path = std::env::temp_dir().join("test_report_intermediate.html");
generate_html_report(&study, &path).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("id=\"intermediate\""));
assert!(content.contains("Intermediate Values"));
std::fs::remove_file(&path).ok();
}