From af226e3d3b4af8dd38296610251c48e70b2dcb42 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Wed, 6 May 2026 08:58:57 -0600 Subject: [PATCH] feat: drop heuropt-plot companion crate Removes the heuropt-plot subcrate, the visualize example that used it, and the related workspace plumbing (root [workspace] table, the [workspace] override added to fuzz/Cargo.toml to detach from it, heuropt-plot dev-dep, CHANGELOG mention). The visualization concern is better served as an independent third- party project than as a companion crate in this repo. No effect on heuropt's public API or the async work in 0.8.0. --- CHANGELOG.md | 13 -- Cargo.toml | 4 - examples/visualize.rs | 53 ------ fuzz/Cargo.toml | 4 - heuropt-plot/Cargo.toml | 17 -- heuropt-plot/README.md | 46 ----- heuropt-plot/src/lib.rs | 368 ---------------------------------------- 7 files changed, 505 deletions(-) delete mode 100644 examples/visualize.rs delete mode 100644 heuropt-plot/Cargo.toml delete mode 100644 heuropt-plot/README.md delete mode 100644 heuropt-plot/src/lib.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a3777c1..7397dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added - -- **`heuropt-plot` companion crate (v0.1.0)** at `heuropt-plot/`, - published independently. Lightweight SVG-only plotter for Pareto - fronts (`pareto_front_svg`) and convergence traces - (`convergence_svg`) — hand-rolled SVG output, no `plotters` / - `tiny-skia` dep so the crate stays a tiny optional addition. -- `examples/visualize.rs` — runs NSGA-II on Schaffer N.1 and writes - `pareto_front.svg` via the new `pareto_front_svg` helper. -- Workspace setup at the repo root: `[workspace] members = [".", - "heuropt-plot"]` so both crates share a target dir and one - `cargo` invocation builds the lot. - ## [0.8.0] — 2026-05-06 Theme: async evaluation. heuropt now supports problems where each diff --git a/Cargo.toml b/Cargo.toml index 28f602a..721ddae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,3 @@ -[workspace] -members = [".", "heuropt-plot"] - [package] name = "heuropt" version = "0.8.0" @@ -31,7 +28,6 @@ serde = { version = "1", features = ["derive"], optional = true } [dev-dependencies] gungraun = "0.18" -heuropt-plot = { path = "heuropt-plot" } proptest = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } diff --git a/examples/visualize.rs b/examples/visualize.rs deleted file mode 100644 index c15e76f..0000000 --- a/examples/visualize.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Visualize an NSGA-II run on Schaffer N.1 with the `heuropt-plot` -//! companion crate — produces a `pareto_front.svg` of the final -//! Pareto front. -//! -//! Run with: `cargo run --release --example visualize` - -use heuropt::prelude::*; -use heuropt_plot::pareto_front_svg; - -struct Schaffer; - -impl Problem for Schaffer { - type Decision = Vec; - fn objectives(&self) -> ObjectiveSpace { - ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]) - } - fn evaluate(&self, x: &Vec) -> Evaluation { - Evaluation::new(vec![x[0] * x[0], (x[0] - 2.0).powi(2)]) - } -} - -fn main() { - let problem = Schaffer; - let bounds = vec![(-5.0_f64, 5.0_f64)]; - let space = problem.objectives(); - - let mut opt = Nsga2::new( - Nsga2Config { - population_size: 50, - generations: 100, - seed: 42, - }, - RealBounds::new(bounds.clone()), - CompositeVariation { - crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5), - mutation: PolynomialMutation::new(bounds, 20.0, 1.0), - }, - ); - - let result = opt.run(&problem); - - let svg = pareto_front_svg( - &result.pareto_front, - &space, - 700, - 450, - "NSGA-II on Schaffer N.1 — final Pareto front", - ); - std::fs::write("pareto_front.svg", svg).expect("write pareto_front.svg"); - - println!("Final front size: {}", result.pareto_front.len()); - println!("Wrote pareto_front.svg"); -} diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 34c6227..baf728c 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,7 +1,3 @@ -[workspace] -# Detach from the parent heuropt workspace so cargo-fuzz can build -# this crate independently without listing it as a workspace member. - [package] name = "heuropt-fuzz" version = "0.0.0" diff --git a/heuropt-plot/Cargo.toml b/heuropt-plot/Cargo.toml deleted file mode 100644 index 7258d9f..0000000 --- a/heuropt-plot/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "heuropt-plot" -version = "0.1.0" -edition = "2024" -rust-version = "1.85" -authors = ["Stephen Waits "] -description = "Lightweight SVG visualization for heuropt Pareto fronts and convergence traces." -license = "MIT" -readme = "README.md" -repository = "https://github.com/swaits/heuropt" -homepage = "https://github.com/swaits/heuropt" -documentation = "https://docs.rs/heuropt-plot" -keywords = ["optimization", "pareto", "svg", "plotting", "heuropt"] -categories = ["algorithms", "visualization"] - -[dependencies] -heuropt = { version = "0.8", path = ".." } diff --git a/heuropt-plot/README.md b/heuropt-plot/README.md deleted file mode 100644 index eb2516c..0000000 --- a/heuropt-plot/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# heuropt-plot - -[![Crates.io](https://img.shields.io/crates/v/heuropt-plot.svg)](https://crates.io/crates/heuropt-plot) -[![Documentation](https://docs.rs/heuropt-plot/badge.svg)](https://docs.rs/heuropt-plot) -[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE) - -Lightweight SVG plotting helpers for [`heuropt`](https://crates.io/crates/heuropt) -results. - -Hand-rolled SVG output (no `plotters`, no `tiny-skia`, no -heavyweight dependency) so adding `heuropt-plot` to your project -costs ~20 KB of compiled code. - -## What's in the box - -- `pareto_front_svg` — render a 2-objective Pareto front as an SVG - scatter plot with axes and labels. -- `convergence_svg` — render a "best fitness so far" trace as an - SVG line plot. - -Output is a `String` of valid SVG. Write it to a file, embed it in -HTML, or pipe it to a browser. - -## Example - -```rust -use heuropt::prelude::*; -use heuropt_plot::pareto_front_svg; - -let space = ObjectiveSpace::new(vec![ - Objective::minimize("f1"), - Objective::minimize("f2"), -]); -let front = vec![ - Candidate::new((), Evaluation::new(vec![0.0, 1.0])), - Candidate::new((), Evaluation::new(vec![0.5, 0.5])), - Candidate::new((), Evaluation::new(vec![1.0, 0.0])), -]; - -let svg = pareto_front_svg(&front, &space, 600, 400, "Sample front"); -std::fs::write("front.svg", svg).unwrap(); -``` - -## License - -MIT — see [LICENSE](../LICENSE) at the repo root. diff --git a/heuropt-plot/src/lib.rs b/heuropt-plot/src/lib.rs deleted file mode 100644 index bd9f036..0000000 --- a/heuropt-plot/src/lib.rs +++ /dev/null @@ -1,368 +0,0 @@ -//! Lightweight SVG plotting helpers for `heuropt` results. -//! -//! Two core primitives: -//! -//! - [`pareto_front_svg`] — render a 2-objective Pareto front as an -//! SVG scatter plot with axes and labels. -//! - [`convergence_svg`] — render a per-generation "best-fitness so -//! far" trace as an SVG line plot. -//! -//! Hand-rolled SVG output (no `plotters` / `tiny-skia` dep) so the -//! crate stays a tiny optional dependency. Output is a `String` of -//! valid SVG — write it to a file, embed it in HTML, or pipe it to a -//! browser. -//! -//! # Example -//! -//! ``` -//! use heuropt::prelude::*; -//! use heuropt_plot::pareto_front_svg; -//! -//! let space = ObjectiveSpace::new(vec![ -//! Objective::minimize("f1"), -//! Objective::minimize("f2"), -//! ]); -//! let front = vec![ -//! Candidate::new((), Evaluation::new(vec![0.0, 1.0])), -//! Candidate::new((), Evaluation::new(vec![0.5, 0.5])), -//! Candidate::new((), Evaluation::new(vec![1.0, 0.0])), -//! ]; -//! let svg = pareto_front_svg(&front, &space, 600, 400, "Sample front"); -//! assert!(svg.starts_with("")); -//! ``` - -use std::fmt::Write as _; - -use heuropt::core::candidate::Candidate; -use heuropt::core::objective::ObjectiveSpace; - -/// Render a 2-objective Pareto front as an SVG scatter plot. -/// -/// `width` and `height` are the SVG viewport dimensions in pixels. -/// `title` is rendered at the top. -/// -/// Points are plotted in minimization-oriented coordinates. -/// -/// # Panics -/// -/// If `objectives.len() != 2`. -pub fn pareto_front_svg( - front: &[Candidate], - objectives: &ObjectiveSpace, - width: u32, - height: u32, - title: &str, -) -> String { - assert_eq!( - objectives.len(), - 2, - "pareto_front_svg requires exactly 2 objectives", - ); - let oriented: Vec<[f64; 2]> = front - .iter() - .map(|c| { - let m = objectives.as_minimization(&c.evaluation.objectives); - [m[0], m[1]] - }) - .collect(); - let (xs_label, ys_label) = ( - objectives.objectives[0].name.as_str(), - objectives.objectives[1].name.as_str(), - ); - - let (xmin, xmax) = bounds(oriented.iter().map(|p| p[0])); - let (ymin, ymax) = bounds(oriented.iter().map(|p| p[1])); - let xspan = (xmax - xmin).max(1e-12); - let yspan = (ymax - ymin).max(1e-12); - - // Margins so axes/labels have room. - let m_left = 60.0_f64; - let m_right = 20.0_f64; - let m_top = 40.0_f64; - let m_bot = 50.0_f64; - let plot_w = width as f64 - m_left - m_right; - let plot_h = height as f64 - m_top - m_bot; - - let to_x = |v: f64| m_left + (v - xmin) / xspan * plot_w; - // Y is inverted: lower minimization value → higher pixel. - let to_y = |v: f64| m_top + plot_h - (v - ymin) / yspan * plot_h; - - let mut out = String::new(); - let _ = writeln!( - out, - "", - ); - let _ = writeln!( - out, - " ", - ); - let _ = writeln!( - out, - " {title}", - x = m_left, - title = escape_xml(title), - ); - - // Axes box. - let _ = writeln!( - out, - " ", - m_left, m_top, plot_w, plot_h, - ); - - // X-axis ticks (3 ticks). - for i in 0..=3 { - let t = i as f64 / 3.0; - let v = xmin + t * xspan; - let x = to_x(v); - let _ = writeln!( - out, - " ", - y0 = m_top + plot_h, - y1 = m_top + plot_h + 5.0, - ); - let _ = writeln!( - out, - " {v:.3}", - y = m_top + plot_h + 18.0, - ); - } - // Y-axis ticks. - for i in 0..=3 { - let t = i as f64 / 3.0; - let v = ymin + t * yspan; - let y = to_y(v); - let _ = writeln!( - out, - " ", - x0 = m_left - 5.0, - x1 = m_left, - ); - let _ = writeln!( - out, - " {v:.3}", - x = m_left - 8.0, - ); - } - - // Axis labels. - let _ = writeln!( - out, - " {xs_label}", - x = m_left + plot_w / 2.0, - y = height as f64 - 12.0, - xs_label = escape_xml(xs_label), - ); - let _ = writeln!( - out, - " {ys_label}", - y = m_top + plot_h / 2.0, - ys_label = escape_xml(ys_label), - ); - - // Points. - for p in &oriented { - let cx = to_x(p[0]); - let cy = to_y(p[1]); - let _ = writeln!( - out, - " ", - ); - } - - out.push_str(""); - out -} - -/// Render a per-generation "best fitness so far" trace as an SVG line -/// plot. `bests[i]` is the best fitness *after* generation `i`. -/// -/// `direction_minimize` controls which way is "improvement": `true` -/// for minimize problems, `false` for maximize. -pub fn convergence_svg( - bests: &[f64], - width: u32, - height: u32, - title: &str, - y_axis_label: &str, - _direction_minimize: bool, -) -> String { - let n = bests.len(); - if n == 0 { - return format!( - "\ - {}", - escape_xml(title) - ); - } - - let (ymin, ymax) = bounds(bests.iter().copied()); - let yspan = (ymax - ymin).max(1e-12); - let xspan = (n - 1).max(1) as f64; - - let m_left = 70.0_f64; - let m_right = 20.0_f64; - let m_top = 40.0_f64; - let m_bot = 50.0_f64; - let plot_w = width as f64 - m_left - m_right; - let plot_h = height as f64 - m_top - m_bot; - - let to_x = |i: usize| m_left + (i as f64) / xspan * plot_w; - let to_y = |v: f64| m_top + plot_h - (v - ymin) / yspan * plot_h; - - let mut out = String::new(); - let _ = writeln!( - out, - "", - ); - let _ = writeln!( - out, - " ", - ); - let _ = writeln!( - out, - " {title}", - x = m_left, - title = escape_xml(title), - ); - let _ = writeln!( - out, - " ", - m_left, m_top, plot_w, plot_h, - ); - - // X axis: generation index. - for i in 0..=4 { - let t = i as f64 / 4.0; - let g = (t * (n - 1) as f64).round() as usize; - let x = to_x(g); - let _ = writeln!( - out, - " ", - y0 = m_top + plot_h, - y1 = m_top + plot_h + 5.0, - ); - let _ = writeln!( - out, - " {g}", - y = m_top + plot_h + 18.0, - ); - } - // Y ticks. - for i in 0..=3 { - let t = i as f64 / 3.0; - let v = ymin + t * yspan; - let y = to_y(v); - let _ = writeln!( - out, - " ", - x0 = m_left - 5.0, - x1 = m_left, - ); - let _ = writeln!( - out, - " {v:.3e}", - x = m_left - 8.0, - ); - } - - // Axis labels. - let _ = writeln!( - out, - " generation", - x = m_left + plot_w / 2.0, - y = height as f64 - 12.0, - ); - let _ = writeln!( - out, - " {label}", - y = m_top + plot_h / 2.0, - label = escape_xml(y_axis_label), - ); - - // Polyline. - let mut points = String::new(); - for (i, &v) in bests.iter().enumerate() { - if i > 0 { - points.push(' '); - } - let _ = write!(points, "{:.2},{:.2}", to_x(i), to_y(v)); - } - let _ = writeln!( - out, - " ", - ); - - out.push_str(""); - out -} - -fn bounds>(it: I) -> (f64, f64) { - let mut lo = f64::INFINITY; - let mut hi = f64::NEG_INFINITY; - for v in it { - if v.is_finite() { - if v < lo { - lo = v; - } - if v > hi { - hi = v; - } - } - } - if lo.is_infinite() { - (0.0, 1.0) - } else if (hi - lo).abs() < f64::EPSILON { - // All points equal — give a small artificial span. - (lo - 0.5, hi + 0.5) - } else { - (lo, hi) - } -} - -fn escape_xml(s: &str) -> String { - s.replace('&', "&") - .replace('<', "<") - .replace('>', ">") -} - -#[cfg(test)] -mod tests { - use super::*; - use heuropt::core::evaluation::Evaluation; - use heuropt::core::objective::Objective; - - #[test] - fn pareto_svg_well_formed() { - let space = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]); - let front = vec![ - Candidate::new((), Evaluation::new(vec![0.0, 1.0])), - Candidate::new((), Evaluation::new(vec![1.0, 0.0])), - ]; - let svg = pareto_front_svg(&front, &space, 400, 300, "test"); - assert!(svg.starts_with("")); - assert!(svg.contains("")); - } -}