From c1bc3b05283e55d6614ae9724237fecb67d1c1b8 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Wed, 6 May 2026 08:08:39 -0600 Subject: [PATCH] docs(rustdoc): add runnable examples across operators, metrics, and Pareto utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the rustdoc audit — every public item now has at least one ```rust example block in its docstring, exercised by `cargo test --doc` (55 doctests, all passing). - Operators: BitFlipMutation, SwapMutation, RealBounds, GaussianMutation, BoundedGaussianMutation, SimulatedBinaryCrossover, PolynomialMutation, LevyMutation, ClampToBounds, ProjectToSimplex. - Metrics: hypervolume_2d, hypervolume_nd, spacing. - Pareto utilities: pareto_compare, pareto_front, best_candidate, non_dominated_sort, crowding_distance, das_dennis, ParetoArchive. Each example is short (5-15 lines) and self-contained — copy-paste into a fresh project and it runs. --- src/metrics/hypervolume.rs | 38 +++++++++++++ src/metrics/spacing.rs | 20 +++++++ src/operators/binary.rs | 13 +++++ src/operators/permutation.rs | 16 ++++++ src/operators/real.rs | 101 +++++++++++++++++++++++++++++++++ src/operators/repair.rs | 24 ++++++++ src/pareto/archive.rs | 17 ++++++ src/pareto/crowding.rs | 22 +++++++ src/pareto/dominance.rs | 15 +++++ src/pareto/front.rs | 34 +++++++++++ src/pareto/reference_points.rs | 15 +++++ src/pareto/sort.rs | 20 +++++++ 12 files changed, 335 insertions(+) diff --git a/src/metrics/hypervolume.rs b/src/metrics/hypervolume.rs index c3bcfc3..c6a4449 100644 --- a/src/metrics/hypervolume.rs +++ b/src/metrics/hypervolume.rs @@ -14,6 +14,26 @@ use crate::core::objective::ObjectiveSpace; /// /// # Panics /// If `objectives` does not have exactly two objectives. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// use heuropt::metrics::hypervolume_2d; +/// +/// let space = ObjectiveSpace::new(vec![ +/// Objective::minimize("f1"), +/// Objective::minimize("f2"), +/// ]); +/// // Reference (4, 4); front at (1,3), (2,2), (3,1) → dominated area = 6. +/// let front = [ +/// Candidate::new((), Evaluation::new(vec![1.0, 3.0])), +/// Candidate::new((), Evaluation::new(vec![2.0, 2.0])), +/// Candidate::new((), Evaluation::new(vec![3.0, 1.0])), +/// ]; +/// let hv = hypervolume_2d(&front, &space, [4.0, 4.0]); +/// assert!((hv - 6.0).abs() < 1e-12); +/// ``` pub fn hypervolume_2d( front: &[Candidate], objectives: &ObjectiveSpace, @@ -147,6 +167,24 @@ mod tests { /// /// # Panics /// If `objectives.len() != reference_point.len()`, or if either is zero. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// use heuropt::metrics::hypervolume_nd; +/// +/// let space = ObjectiveSpace::new(vec![ +/// Objective::minimize("f1"), +/// Objective::minimize("f2"), +/// Objective::minimize("f3"), +/// ]); +/// // Single corner point at the origin against a unit-cube reference: +/// // dominated volume = 1. +/// let front = [Candidate::new((), Evaluation::new(vec![0.0, 0.0, 0.0]))]; +/// let hv = hypervolume_nd(&front, &space, &[1.0, 1.0, 1.0]); +/// assert!((hv - 1.0).abs() < 1e-12); +/// ``` pub fn hypervolume_nd( front: &[Candidate], objectives: &ObjectiveSpace, diff --git a/src/metrics/spacing.rs b/src/metrics/spacing.rs index 84713e1..add9af0 100644 --- a/src/metrics/spacing.rs +++ b/src/metrics/spacing.rs @@ -11,6 +11,26 @@ use crate::core::objective::ObjectiveSpace; /// uniform front has spacing 0. /// /// Returns `0.0` for empty or single-point fronts (spec §14.1). +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// use heuropt::metrics::spacing; +/// +/// let space = ObjectiveSpace::new(vec![ +/// Objective::minimize("f1"), +/// Objective::minimize("f2"), +/// ]); +/// // Five points evenly spaced on a line — spacing should be 0. +/// let front: Vec> = (0..5) +/// .map(|i| { +/// let t = i as f64; +/// Candidate::new((), Evaluation::new(vec![t, 4.0 - t])) +/// }) +/// .collect(); +/// assert!(spacing(&front, &space) < 1e-12); +/// ``` pub fn spacing(front: &[Candidate], objectives: &ObjectiveSpace) -> f64 { let n = front.len(); if n < 2 { diff --git a/src/operators/binary.rs b/src/operators/binary.rs index e39c8b8..f5e20d1 100644 --- a/src/operators/binary.rs +++ b/src/operators/binary.rs @@ -9,6 +9,19 @@ use crate::traits::Variation; /// /// Always returns exactly one child (spec §11.3). Panics if `probability` is /// outside `[0.0, 1.0]` or if no parents are provided. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(42); +/// let mut m = BitFlipMutation { probability: 0.5 }; +/// let parent = vec![true, false, true, false]; +/// let children = m.vary(std::slice::from_ref(&parent), &mut rng); +/// assert_eq!(children.len(), 1); +/// assert_eq!(children[0].len(), parent.len()); +/// ``` #[derive(Debug, Clone)] pub struct BitFlipMutation { /// Per-bit flip probability. Must lie in `[0.0, 1.0]`. diff --git a/src/operators/permutation.rs b/src/operators/permutation.rs index e200ff3..9b88c20 100644 --- a/src/operators/permutation.rs +++ b/src/operators/permutation.rs @@ -8,6 +8,22 @@ use crate::traits::Variation; /// Swap two distinct random indices in the first parent (spec §11.4). /// /// If the parent has length `< 2` the child is returned unchanged. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(42); +/// let mut m = SwapMutation; +/// let parent: Vec = (0..6).collect(); +/// let children = m.vary(std::slice::from_ref(&parent), &mut rng); +/// assert_eq!(children.len(), 1); +/// // Still a permutation of [0, 1, 2, 3, 4, 5]: +/// let mut sorted = children[0].clone(); +/// sorted.sort(); +/// assert_eq!(sorted, vec![0, 1, 2, 3, 4, 5]); +/// ``` #[derive(Debug, Clone, Copy, Default)] pub struct SwapMutation; diff --git a/src/operators/real.rs b/src/operators/real.rs index b18bf3b..c3d7665 100644 --- a/src/operators/real.rs +++ b/src/operators/real.rs @@ -10,6 +10,23 @@ use crate::traits::{Initializer, Variation}; /// /// Bounds are inclusive `(lo, hi)` ranges per dimension. Panics if any bound /// has `lo > hi` (spec §11.1). +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(42); +/// let mut init = RealBounds::new(vec![(-1.0, 1.0); 3]); +/// let decisions = init.initialize(5, &mut rng); +/// assert_eq!(decisions.len(), 5); +/// for d in &decisions { +/// assert_eq!(d.len(), 3); +/// for &v in d { +/// assert!(v >= -1.0 && v <= 1.0); +/// } +/// } +/// ``` #[derive(Debug, Clone)] pub struct RealBounds { /// Per-variable inclusive bounds in decision order. @@ -54,6 +71,19 @@ impl Initializer> for RealBounds { /// Add `Normal(0, sigma)` noise to every variable of the first parent. /// /// Always returns exactly one child. Does not enforce bounds in v1 (spec §11.2). +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(42); +/// let mut m = GaussianMutation { sigma: 0.1 }; +/// let parent = vec![0.0; 4]; +/// let children = m.vary(std::slice::from_ref(&parent), &mut rng); +/// assert_eq!(children.len(), 1); +/// assert_eq!(children[0].len(), parent.len()); +/// ``` #[derive(Debug, Clone)] pub struct GaussianMutation { /// Standard deviation of the Gaussian noise. Must be positive. @@ -88,6 +118,26 @@ impl Variation> for GaussianMutation { /// /// Panics on construction if any bound has `lo > hi`, or at run time if /// `parents.len() < 2` or any parent length differs from `bounds.len()`. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let bounds = vec![(-1.0, 1.0); 3]; +/// let mut sbx = SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5); +/// let mut rng = rng_from_seed(42); +/// let parents = [vec![-0.5, 0.0, 0.5], vec![0.5, 0.5, -0.5]]; +/// let children = sbx.vary(&parents, &mut rng); +/// assert_eq!(children.len(), 2); +/// // Children stay in bounds. +/// for c in &children { +/// for (j, &v) in c.iter().enumerate() { +/// let (lo, hi) = bounds[j]; +/// assert!(v >= lo && v <= hi); +/// } +/// } +/// ``` #[derive(Debug, Clone)] pub struct SimulatedBinaryCrossover { /// Per-variable inclusive bounds. Length must match the parent decisions. @@ -180,6 +230,23 @@ impl Variation> for SimulatedBinaryCrossover { /// /// This is the simple bound-rescale form; the bound-aware `δ_q` variant from /// the full paper is left as a future refinement. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let bounds = vec![(-1.0, 1.0); 3]; +/// let mut pm = PolynomialMutation::new(bounds.clone(), 20.0, 1.0 / 3.0); +/// let mut rng = rng_from_seed(42); +/// let parent = vec![0.0, 0.5, -0.5]; +/// let children = pm.vary(std::slice::from_ref(&parent), &mut rng); +/// assert_eq!(children.len(), 1); +/// for (j, &v) in children[0].iter().enumerate() { +/// let (lo, hi) = bounds[j]; +/// assert!(v >= lo && v <= hi); +/// } +/// ``` #[derive(Debug, Clone)] pub struct PolynomialMutation { /// Per-variable inclusive bounds. Length must match the parent decision. @@ -254,6 +321,23 @@ impl Variation> for PolynomialMutation { /// Always returns exactly one child. Use this when you want feasibility /// maintained across generations without leaning on /// clamp-inside-`Problem::evaluate`. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let bounds = vec![(-1.0, 1.0); 3]; +/// let mut m = BoundedGaussianMutation::new(0.3, bounds.clone()); +/// let mut rng = rng_from_seed(42); +/// let parent = vec![0.0; 3]; +/// let children = m.vary(std::slice::from_ref(&parent), &mut rng); +/// assert_eq!(children.len(), 1); +/// for (j, &v) in children[0].iter().enumerate() { +/// let (lo, hi) = bounds[j]; +/// assert!(v >= lo && v <= hi); +/// } +/// ``` #[derive(Debug, Clone)] pub struct BoundedGaussianMutation { /// Standard deviation of the Gaussian noise. Must be positive. @@ -315,6 +399,23 @@ impl Variation> for BoundedGaussianMutation { /// produce a Lévy(α) sample. `alpha` is the tail exponent in `(0, 2]`; /// typical value is `1.5`. `1.0` gives the Cauchy distribution (very /// heavy); `2.0` collapses to the Normal. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let bounds = vec![(-1.0, 1.0); 3]; +/// let mut m = LevyMutation::new(1.5, 0.1, bounds.clone()); +/// let mut rng = rng_from_seed(42); +/// let parent = vec![0.0; 3]; +/// let children = m.vary(std::slice::from_ref(&parent), &mut rng); +/// assert_eq!(children.len(), 1); +/// for (j, &v) in children[0].iter().enumerate() { +/// let (lo, hi) = bounds[j]; +/// assert!(v >= lo && v <= hi); +/// } +/// ``` #[derive(Debug, Clone)] pub struct LevyMutation { /// Tail exponent `α ∈ (0, 2]`. Smaller = heavier tail. diff --git a/src/operators/repair.rs b/src/operators/repair.rs index 10288a1..cc26f7a 100644 --- a/src/operators/repair.rs +++ b/src/operators/repair.rs @@ -8,6 +8,17 @@ use crate::traits::Repair; /// The simplest possible repair — pair with `GaussianMutation` (which /// doesn't enforce bounds in v1) to produce a bounds-respecting variant /// without writing a custom Variation impl. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut r = ClampToBounds::new(vec![(-1.0, 1.0); 3]); +/// let mut x = vec![-2.0, 0.5, 5.0]; +/// r.repair(&mut x); +/// assert_eq!(x, vec![-1.0, 0.5, 1.0]); +/// ``` #[derive(Debug, Clone)] pub struct ClampToBounds { /// Per-variable inclusive bounds. @@ -46,6 +57,19 @@ impl Repair> for ClampToBounds { /// Perpiñán 2013. Useful for portfolio-style problems where the /// decision must sum to a budget, and for normalizing reference /// directions onto the unit simplex. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut r = ProjectToSimplex::new(1.0); +/// let mut x = vec![0.6, 0.5, -0.1, 0.3]; +/// r.repair(&mut x); +/// let sum: f64 = x.iter().sum(); +/// assert!((sum - 1.0).abs() < 1e-12); +/// assert!(x.iter().all(|&v| v >= 0.0)); +/// ``` #[derive(Debug, Clone)] pub struct ProjectToSimplex { /// Target sum (the simplex's "size"). Standard probability simplex diff --git a/src/pareto/archive.rs b/src/pareto/archive.rs index 7ea1d04..8c90b12 100644 --- a/src/pareto/archive.rs +++ b/src/pareto/archive.rs @@ -9,6 +9,23 @@ use crate::core::objective::ObjectiveSpace; /// archive insert/extend operations maintain the non-domination property among /// members; `truncate` enforces a maximum size by simple tail-truncation in /// v1. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let s = ObjectiveSpace::new(vec![ +/// Objective::minimize("f1"), +/// Objective::minimize("f2"), +/// ]); +/// let mut a: ParetoArchive = ParetoArchive::new(s); +/// a.insert(Candidate::new(1, Evaluation::new(vec![1.0, 4.0]))); +/// a.insert(Candidate::new(2, Evaluation::new(vec![3.0, 2.0]))); +/// // Dominated by both — should be discarded: +/// a.insert(Candidate::new(3, Evaluation::new(vec![5.0, 5.0]))); +/// assert_eq!(a.members().len(), 2); +/// ``` #[derive(Debug, Clone)] pub struct ParetoArchive { /// The current approximate non-dominated set. diff --git a/src/pareto/crowding.rs b/src/pareto/crowding.rs index 795b789..db23ee5 100644 --- a/src/pareto/crowding.rs +++ b/src/pareto/crowding.rs @@ -11,6 +11,28 @@ use crate::core::objective::ObjectiveSpace; /// `f64::INFINITY`. If the front has 0 entries an empty vector is returned; /// 1 or 2 entries return all `f64::INFINITY`. All comparisons happen on /// minimization-oriented objective values (spec §9.6). +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let s = ObjectiveSpace::new(vec![ +/// Objective::minimize("f1"), +/// Objective::minimize("f2"), +/// ]); +/// // Three points along a Pareto-like trade-off; the interior point gets +/// // a finite crowding distance, the boundaries get +∞. +/// let pop = [ +/// Candidate::new((), Evaluation::new(vec![0.0, 4.0])), +/// Candidate::new((), Evaluation::new(vec![2.0, 2.0])), +/// Candidate::new((), Evaluation::new(vec![4.0, 0.0])), +/// ]; +/// let d = crowding_distance(&pop, &[0, 1, 2], &s); +/// assert!(d[0].is_infinite()); +/// assert!(d[1].is_finite() && d[1] > 0.0); +/// assert!(d[2].is_infinite()); +/// ``` pub fn crowding_distance( population: &[Candidate], front: &[usize], diff --git a/src/pareto/dominance.rs b/src/pareto/dominance.rs index aea7db0..119470e 100644 --- a/src/pareto/dominance.rs +++ b/src/pareto/dominance.rs @@ -29,6 +29,21 @@ pub enum Dominance { /// `constraint_violation` dominates. /// 3. Otherwise compare objective values after converting both to /// minimization orientation via [`ObjectiveSpace::as_minimization`]. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let s = ObjectiveSpace::new(vec![ +/// Objective::minimize("f1"), +/// Objective::minimize("f2"), +/// ]); +/// let a = Evaluation::new(vec![1.0, 1.0]); +/// let b = Evaluation::new(vec![2.0, 2.0]); +/// assert_eq!(pareto_compare(&a, &b, &s), Dominance::Dominates); +/// assert_eq!(pareto_compare(&b, &a, &s), Dominance::DominatedBy); +/// ``` pub fn pareto_compare(a: &Evaluation, b: &Evaluation, objectives: &ObjectiveSpace) -> Dominance { let a_feasible = a.is_feasible(); let b_feasible = b.is_feasible(); diff --git a/src/pareto/front.rs b/src/pareto/front.rs index bd8b973..9f5f7d4 100644 --- a/src/pareto/front.rs +++ b/src/pareto/front.rs @@ -8,6 +8,25 @@ use crate::pareto::dominance::{Dominance, pareto_compare}; /// /// O(N²·M) in v1 (spec §9.3). Input order is preserved among returned /// candidates. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let s = ObjectiveSpace::new(vec![ +/// Objective::minimize("f1"), +/// Objective::minimize("f2"), +/// ]); +/// let pop = [ +/// Candidate::new(1u32, Evaluation::new(vec![1.0, 4.0])), // non-dominated +/// Candidate::new(2u32, Evaluation::new(vec![3.0, 2.0])), // non-dominated +/// Candidate::new(3u32, Evaluation::new(vec![5.0, 5.0])), // dominated +/// ]; +/// let front = pareto_front(&pop, &s); +/// let kept: Vec = front.iter().map(|c| c.decision).collect(); +/// assert_eq!(kept, vec![1, 2]); +/// ``` pub fn pareto_front( population: &[Candidate], objectives: &ObjectiveSpace, @@ -34,6 +53,21 @@ pub fn pareto_front( /// /// Returns `None` if there is not exactly one objective, if the population is /// empty, or if every candidate is infeasible (spec §9.4). +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let s = ObjectiveSpace::new(vec![Objective::minimize("f")]); +/// let pop = [ +/// Candidate::new(1u32, Evaluation::new(vec![3.0])), +/// Candidate::new(2u32, Evaluation::new(vec![1.0])), +/// Candidate::new(3u32, Evaluation::new(vec![2.0])), +/// ]; +/// let best = best_candidate(&pop, &s).unwrap(); +/// assert_eq!(best.decision, 2); +/// ``` pub fn best_candidate( population: &[Candidate], objectives: &ObjectiveSpace, diff --git a/src/pareto/reference_points.rs b/src/pareto/reference_points.rs index 9da10eb..dbbc36a 100644 --- a/src/pareto/reference_points.rs +++ b/src/pareto/reference_points.rs @@ -10,6 +10,21 @@ /// /// # Panics /// If `num_objectives == 0`. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// // 3 objectives, 4 divisions → binomial(6, 2) = 15 points. +/// let pts = das_dennis(3, 4); +/// assert_eq!(pts.len(), 15); +/// for w in &pts { +/// assert_eq!(w.len(), 3); +/// let sum: f64 = w.iter().sum(); +/// assert!((sum - 1.0).abs() < 1e-12); +/// } +/// ``` pub fn das_dennis(num_objectives: usize, divisions: usize) -> Vec> { assert!( num_objectives > 0, diff --git a/src/pareto/sort.rs b/src/pareto/sort.rs index d030fed..e1c7f32 100644 --- a/src/pareto/sort.rs +++ b/src/pareto/sort.rs @@ -9,6 +9,26 @@ use crate::core::objective::ObjectiveSpace; /// non-dominated after removing `fronts[0]`, and so on. Each entry is an index /// into the input population. Equal-objective candidates land on the same /// front. O(N²·M) is acceptable for v1 (spec §9.5). +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let s = ObjectiveSpace::new(vec![ +/// Objective::minimize("f1"), +/// Objective::minimize("f2"), +/// ]); +/// let pop = [ +/// Candidate::new((), Evaluation::new(vec![1.0, 5.0])), // front 0 +/// Candidate::new((), Evaluation::new(vec![2.0, 3.0])), // front 0 +/// Candidate::new((), Evaluation::new(vec![4.0, 1.0])), // front 0 +/// Candidate::new((), Evaluation::new(vec![3.0, 4.0])), // front 1 +/// Candidate::new((), Evaluation::new(vec![5.0, 6.0])), // front 2 +/// ]; +/// let fronts = non_dominated_sort(&pop, &s); +/// assert_eq!(fronts.len(), 3); +/// ``` pub fn non_dominated_sort( population: &[Candidate], objectives: &ObjectiveSpace,