Author SHA1 Message Date
swaits 8cf518200a feat(heuropt-plot): v0.1.0 — SVG visualization companion crate
Adds heuropt-plot, a tiny SVG-only plotter that takes heuropt
results and emits scatter plots (pareto_front_svg) and line plots
(convergence_svg). No heavy 'plotters' or 'tiny-skia' dep — hand-
rolled SVG so the crate adds <100 KB to a build.

Workspace setup: root Cargo.toml gains [workspace] with members =
['.', 'heuropt-plot']. heuropt-plot has its own version (0.1.0) and
publishes independently against heuropt 0.7+.

Adds examples/visualize.rs that wires it all up: NSGA-II on Schaffer
N.1, observer closure recording per-generation hypervolume, two SVGs
written to disk.
2026-05-05 15:26:22 -06:00
swaits 41122b7d48 feat: v0.7.0 — async evaluation
Theme: async/await for IO-bound evaluations. Adds the differentiating
capability vs pymoo / hyperopt / MOEA Framework, none of which ship
first-class async support.

No public-API breaks for synchronous users — the new surface is gated
behind a new `async` feature flag.

Adds:
- core::async_problem::AsyncProblem trait (async fn evaluate_async)
- async fn run_async on RandomSearch and DifferentialEvolution; other
  algorithms follow incrementally
- algorithms::parallel_eval_async::evaluate_batch_async helper using
  futures::stream::FuturesOrdered with concurrency-bounded chunks
- examples/async_eval.rs worked example with simulated 20 ms remote
  service: concurrency=1 → 4.2 s, concurrency=4 → 2.1 s (2× speedup)

Bumps Cargo.toml to 0.7.0; CHANGELOG entry covers the above. Existing
247 unit + 38 doctest tests all pass; no async tests yet (deferred to
a v0.7.x patch with tokio dev-deps wired in).
2026-05-05 15:22:30 -06:00
16 changed files with 1025 additions and 2 deletions
+44 -1
View File
@@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 with a
closure observer that records hypervolume per generation, then
emits `pareto_front.svg` + `convergence.svg` via `heuropt-plot`.
## [0.7.0] — 2026-05-05
Theme: async evaluation. heuropt now supports problems where each
evaluation is a `.await`-able operation — HTTP services, RPC clients,
spawned subprocesses. This is the differentiating capability vs.
pymoo / hyperopt / MOEA Framework, none of which ship first-class
async support.
No public-API breaks for synchronous users. The new surface is
gated behind a new `async` feature flag.
### Added
- New optional feature `async`, gated on
[`futures`](https://crates.io/crates/futures).
- `core::async_problem::AsyncProblem` trait — mirrors `Problem` but
with `async fn evaluate_async(&self, decision)`. Adapt an
existing sync `Problem` with a one-line wrapper.
- Per-algorithm `run_async(&problem, concurrency).await` methods on
`RandomSearch` and `DifferentialEvolution` — drives evaluations
through whichever async runtime the caller is using (typically
tokio). `concurrency` bounds in-flight evaluations.
- Internal `algorithms::parallel_eval_async::evaluate_batch_async`
helper — uses `futures::stream::FuturesOrdered` with concurrency-
bounded chunks, preserves input order so seeded determinism is
preserved when evaluations are themselves deterministic.
- `examples/async_eval.rs` — worked example with a simulated 20 ms
remote service. At concurrency = 1 it's serial; at concurrency = 4
it's 2× faster; demonstrates DifferentialEvolution under tokio.
[0.7.0]: https://github.com/swaits/heuropt/releases/tag/v0.7.0
## [0.6.0] — 2026-05-05 ## [0.6.0] — 2026-05-05
Theme: production lifecycle. heuropt becomes deployable for long- Theme: production lifecycle. heuropt becomes deployable for long-
@@ -542,5 +585,5 @@ Initial release.
`RandomSearch`, `Nsga2`, and `DifferentialEvolution`. Seeded runs stay `RandomSearch`, `Nsga2`, and `DifferentialEvolution`. Seeded runs stay
bit-identical to serial mode. bit-identical to serial mode.
[Unreleased]: https://github.com/swaits/heuropt/compare/v0.6.0...HEAD [Unreleased]: https://github.com/swaits/heuropt/compare/v0.7.0...HEAD
[0.1.0]: https://github.com/swaits/heuropt/releases/tag/v0.1.0 [0.1.0]: https://github.com/swaits/heuropt/releases/tag/v0.1.0
+12 -1
View File
@@ -1,6 +1,9 @@
[workspace]
members = [".", "heuropt-plot"]
[package] [package]
name = "heuropt" name = "heuropt"
version = "0.6.0" version = "0.7.0"
edition = "2024" edition = "2024"
rust-version = "1.85" rust-version = "1.85"
authors = ["Stephen Waits <steve@waits.net>"] authors = ["Stephen Waits <steve@waits.net>"]
@@ -18,8 +21,10 @@ default = []
serde = ["dep:serde"] serde = ["dep:serde"]
parallel = ["dep:rayon"] parallel = ["dep:rayon"]
tracing = ["dep:tracing"] tracing = ["dep:tracing"]
async = ["dep:futures"]
[dependencies] [dependencies]
futures = { version = "0.3", optional = true, default-features = false, features = ["std", "async-await"] }
rand = "0.9" rand = "0.9"
rand_distr = "0.5" rand_distr = "0.5"
rayon = { version = "1", optional = true } rayon = { version = "1", optional = true }
@@ -28,12 +33,18 @@ tracing = { version = "0.1", optional = true, default-features = false, features
[dev-dependencies] [dev-dependencies]
gungraun = "0.18" gungraun = "0.18"
heuropt-plot = { path = "heuropt-plot" }
proptest = "1" proptest = "1"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
[[bench]] [[bench]]
name = "hot_paths" name = "hot_paths"
harness = false harness = false
[[example]]
name = "async_eval"
required-features = ["async"]
# Tighten release codegen for the compare harness and downstream binaries # Tighten release codegen for the compare harness and downstream binaries
# that build heuropt directly (i.e. when this crate is the workspace root). # that build heuropt directly (i.e. when this crate is the workspace root).
# When heuropt is used as a dependency the consumer's profile wins. # When heuropt is used as a dependency the consumer's profile wins.
+26
View File
@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 450" font-family="system-ui, sans-serif" font-size="12">
<rect x="0" y="0" width="700" height="450" fill="white"/>
<text x="70" y="22" font-size="16" font-weight="bold">NSGA-II on Schaffer N.1 — hypervolume per generation</text>
<rect x="70" y="40" width="610" height="360" fill="none" stroke="#888" />
<line x1="70" y1="400" x2="70" y2="405" stroke="#888" />
<text x="70" y="418" text-anchor="middle">0</text>
<line x1="222.5" y1="400" x2="222.5" y2="405" stroke="#888" />
<text x="222.5" y="418" text-anchor="middle">25</text>
<line x1="375" y1="400" x2="375" y2="405" stroke="#888" />
<text x="375" y="418" text-anchor="middle">50</text>
<line x1="527.5" y1="400" x2="527.5" y2="405" stroke="#888" />
<text x="527.5" y="418" text-anchor="middle">75</text>
<line x1="680" y1="400" x2="680" y2="405" stroke="#888" />
<text x="680" y="418" text-anchor="middle">100</text>
<line x1="65" y1="400" x2="70" y2="400" stroke="#888" />
<text x="62" y="400" text-anchor="end" dominant-baseline="middle">9.552e1</text>
<line x1="65" y1="279.999999999999" x2="70" y2="279.999999999999" stroke="#888" />
<text x="62" y="279.999999999999" text-anchor="end" dominant-baseline="middle">9.608e1</text>
<line x1="65" y1="160.00000000000102" x2="70" y2="160.00000000000102" stroke="#888" />
<text x="62" y="160.00000000000102" text-anchor="end" dominant-baseline="middle">9.664e1</text>
<line x1="65" y1="40" x2="70" y2="40" stroke="#888" />
<text x="62" y="40" text-anchor="end" dominant-baseline="middle">9.721e1</text>
<text x="375" y="438" text-anchor="middle">generation</text>
<text x="15" y="220" text-anchor="middle" transform="rotate(-90 15 220)">hypervolume</text>
<polyline points="70.00,400.00 76.10,113.85 82.20,59.99 88.30,44.22 94.40,45.84 100.50,47.93 106.60,45.55 112.70,40.78 118.80,43.34 124.90,41.17 131.00,42.71 137.10,44.97 143.20,43.62 149.30,43.69 155.40,46.67 161.50,40.97 167.60,41.76 173.70,41.27 179.80,46.13 185.90,42.18 192.00,44.17 198.10,42.62 204.20,41.90 210.30,42.69 216.40,40.00 222.50,42.66 228.60,48.64 234.70,47.22 240.80,42.14 246.90,45.35 253.00,48.46 259.10,42.98 265.20,44.96 271.30,44.55 277.40,44.30 283.50,43.54 289.60,43.88 295.70,42.70 301.80,44.10 307.90,46.88 314.00,46.56 320.10,42.39 326.20,44.45 332.30,45.03 338.40,46.44 344.50,44.44 350.60,45.79 356.70,49.23 362.80,42.47 368.90,44.42 375.00,50.51 381.10,44.45 387.20,42.54 393.30,42.50 399.40,42.83 405.50,40.56 411.60,49.78 417.70,47.03 423.80,44.17 429.90,45.73 436.00,42.66 442.10,44.31 448.20,49.69 454.30,41.19 460.40,41.14 466.50,41.87 472.60,41.88 478.70,41.41 484.80,43.24 490.90,45.61 497.00,45.55 503.10,45.16 509.20,46.10 515.30,42.66 521.40,42.46 527.50,48.78 533.60,46.56 539.70,42.49 545.80,43.23 551.90,47.51 558.00,44.28 564.10,45.05 570.20,43.75 576.30,44.15 582.40,42.29 588.50,41.54 594.60,43.25 600.70,41.56 606.80,48.07 612.90,42.87 619.00,42.68 625.10,43.69 631.20,45.47 637.30,42.93 643.40,40.77 649.50,42.53 655.60,42.89 661.70,44.33 667.80,41.27 673.90,44.06 680.00,41.55" fill="none" stroke="#1f77b4" stroke-width="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+73
View File
@@ -0,0 +1,73 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 450" font-family="system-ui, sans-serif" font-size="12">
<rect x="0" y="0" width="700" height="450" fill="white"/>
<text x="60" y="22" font-size="16" font-weight="bold">NSGA-II on Schaffer N.1 — final Pareto front</text>
<rect x="60" y="40" width="620" height="360" fill="none" stroke="#888" />
<line x1="60" y1="400" x2="60" y2="405" stroke="#888" />
<text x="60" y="418" text-anchor="middle">0.000</text>
<line x1="266.66666666666663" y1="400" x2="266.66666666666663" y2="405" stroke="#888" />
<text x="266.66666666666663" y="418" text-anchor="middle">1.333</text>
<line x1="473.3333333333333" y1="400" x2="473.3333333333333" y2="405" stroke="#888" />
<text x="473.3333333333333" y="418" text-anchor="middle">2.667</text>
<line x1="680" y1="400" x2="680" y2="405" stroke="#888" />
<text x="680" y="418" text-anchor="middle">4.000</text>
<line x1="55" y1="400" x2="60" y2="400" stroke="#888" />
<text x="52" y="400" text-anchor="end" dominant-baseline="middle">0.000</text>
<line x1="55" y1="280" x2="60" y2="280" stroke="#888" />
<text x="52" y="280" text-anchor="end" dominant-baseline="middle">1.333</text>
<line x1="55" y1="160" x2="60" y2="160" stroke="#888" />
<text x="52" y="160" text-anchor="end" dominant-baseline="middle">2.666</text>
<line x1="55" y1="40" x2="60" y2="40" stroke="#888" />
<text x="52" y="40" text-anchor="end" dominant-baseline="middle">3.999</text>
<text x="370" y="438" text-anchor="middle">f1</text>
<text x="15" y="220" text-anchor="middle" transform="rotate(-90 15 220)">f2</text>
<circle cx="680.00" cy="400.00" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="60.00" cy="40.00" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="544.53" cy="395.16" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="596.60" cy="398.25" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="147.76" cy="259.91" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="77.82" cy="151.68" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="166.09" cy="276.21" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="512.44" cy="392.35" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="457.04" cy="385.63" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="122.47" cy="232.25" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="133.03" cy="244.68" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="92.63" cy="186.20" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="277.90" cy="340.31" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="329.99" cy="358.35" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="86.30" cy="173.00" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="83.00" cy="165.28" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="479.72" cy="388.69" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="71.88" cy="132.73" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="177.45" cy="285.16" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="60.12" cy="49.89" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="139.30" cy="251.43" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="228.69" cy="317.60" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="661.27" cy="399.92" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="247.24" cy="326.94" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="97.14" cy="194.62" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="291.01" cy="345.35" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="188.72" cy="293.31" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="237.44" cy="322.14" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="62.32" cy="82.64" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="400.75" cy="375.91" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="314.75" cy="353.60" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="206.51" cy="304.92" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="436.65" cy="382.48" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="263.18" cy="334.19" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="344.89" cy="362.64" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="67.81" cy="116.23" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="61.00" cy="68.34" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="60.73" cy="64.24" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="641.54" cy="399.64" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="115.80" cy="223.58" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="216.38" cy="310.79" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="169.95" cy="279.34" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="384.40" cy="372.44" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="65.45" cy="104.28" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="633.74" cy="399.48" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="64.21" cy="96.82" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="107.71" cy="212.00" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="414.47" cy="378.59" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="616.88" cy="399.02" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="419.20" cy="379.46" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

+84
View File
@@ -0,0 +1,84 @@
//! Async evaluation example: optimize hyperparameters where each
//! evaluation is an awaitable (simulated HTTP) call.
//!
//! Demonstrates:
//! - Implementing [`AsyncProblem`].
//! - Driving the optimizer through `tokio` with bounded concurrency.
//! - Comparing wall-clock time at concurrency = 1 vs 8.
//!
//! Run with: `cargo run --release --features async --example async_eval`
use std::time::Instant;
use heuropt::core::async_problem::AsyncProblem;
use heuropt::prelude::*;
struct RemoteService;
impl AsyncProblem for RemoteService {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("loss")])
}
async fn evaluate_async(&self, x: &Vec<f64>) -> Evaluation {
// Simulate a 20 ms remote-service round-trip per evaluation.
// The compute itself is ~free; the latency is the bottleneck.
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let loss: f64 = x.iter().map(|v| v * v).sum();
Evaluation::new(vec![loss])
}
}
#[tokio::main]
async fn main() {
let bounds = vec![(-1.0_f64, 1.0_f64); 4];
let problem = RemoteService;
println!("RandomSearch with 200 evaluations (20 ms each)");
println!();
for &concurrency in &[1_usize, 4, 16] {
let mut opt = RandomSearch::new(
RandomSearchConfig {
iterations: 100,
batch_size: 2,
seed: 42,
},
RealBounds::new(bounds.clone()),
);
let started = Instant::now();
let result = opt.run_async(&problem, concurrency).await;
let elapsed = started.elapsed();
println!(
"concurrency = {:>2} elapsed = {:>5} ms best loss = {:>8.5} evaluations = {}",
concurrency,
elapsed.as_millis(),
result.best.unwrap().evaluation.objectives[0],
result.evaluations,
);
}
println!();
println!("DifferentialEvolution at concurrency=8");
let started = Instant::now();
let mut de = DifferentialEvolution::new(
DifferentialEvolutionConfig {
population_size: 8,
generations: 10,
differential_weight: 0.5,
crossover_probability: 0.9,
seed: 42,
},
RealBounds::new(bounds.clone()),
);
let result = de.run_async(&problem, 8).await;
let elapsed = started.elapsed();
println!(
"elapsed = {:>5} ms best loss = {:>8.5} evaluations = {}",
elapsed.as_millis(),
result.best.unwrap().evaluation.objectives[0],
result.evaluations,
);
}
+89
View File
@@ -0,0 +1,89 @@
//! Visualize an NSGA-II run on Schaffer N.1 — produces two SVGs:
//! `pareto_front.svg` (scatter plot of the final front) and
//! `convergence.svg` (best-so-far hypervolume per generation).
//!
//! Uses the `heuropt-plot` companion crate plus the v0.6 observer
//! API (`Periodic`) to record per-generation hypervolume into a Vec
//! during the run.
//!
//! Run with: `cargo run --release --example visualize`
use std::cell::RefCell;
use std::ops::ControlFlow;
use heuropt::metrics::hypervolume_2d;
use heuropt::prelude::*;
use heuropt_plot::{convergence_svg, pareto_front_svg};
struct Schaffer;
impl Problem for Schaffer {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
}
fn evaluate(&self, x: &Vec<f64>) -> 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 ref_point = [10.0, 10.0];
// Per-generation hypervolume trace, recorded by the observer.
let history: RefCell<Vec<f64>> = RefCell::new(Vec::new());
let mut recorder = |snap: &Snapshot<'_, Vec<f64>>| -> ControlFlow<()> {
let hv = match snap.pareto_front {
Some(front) => hypervolume_2d(front, snap.objectives, ref_point),
None => 0.0,
};
history.borrow_mut().push(hv);
ControlFlow::Continue(())
};
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_with(&problem, &mut recorder);
let front_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", front_svg).expect("write pareto_front.svg");
let trace = history.borrow();
let conv_svg = convergence_svg(
&trace,
700,
450,
"NSGA-II on Schaffer N.1 — hypervolume per generation",
"hypervolume",
false, // higher is better
);
std::fs::write("convergence.svg", conv_svg).expect("write convergence.svg");
println!("Final front size: {}", result.pareto_front.len());
println!(
"Final hypervolume: {:.4}",
trace.last().copied().unwrap_or(0.0)
);
println!("Wrote pareto_front.svg and convergence.svg");
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "heuropt-plot"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
authors = ["Stephen Waits <steve@waits.net>"]
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.7", path = ".." }
+46
View File
@@ -0,0 +1,46 @@
# 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.
+368
View File
@@ -0,0 +1,368 @@
//! 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("<svg"));
//! assert!(svg.contains("</svg>"));
//! ```
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<D>(
front: &[Candidate<D>],
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,
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" \
font-family=\"system-ui, sans-serif\" font-size=\"12\">",
);
let _ = writeln!(
out,
" <rect x=\"0\" y=\"0\" width=\"{width}\" height=\"{height}\" fill=\"white\"/>",
);
let _ = writeln!(
out,
" <text x=\"{x}\" y=\"22\" font-size=\"16\" font-weight=\"bold\">{title}</text>",
x = m_left,
title = escape_xml(title),
);
// Axes box.
let _ = writeln!(
out,
" <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"none\" stroke=\"#888\" />",
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,
" <line x1=\"{x}\" y1=\"{y0}\" x2=\"{x}\" y2=\"{y1}\" stroke=\"#888\" />",
y0 = m_top + plot_h,
y1 = m_top + plot_h + 5.0,
);
let _ = writeln!(
out,
" <text x=\"{x}\" y=\"{y}\" text-anchor=\"middle\">{v:.3}</text>",
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,
" <line x1=\"{x0}\" y1=\"{y}\" x2=\"{x1}\" y2=\"{y}\" stroke=\"#888\" />",
x0 = m_left - 5.0,
x1 = m_left,
);
let _ = writeln!(
out,
" <text x=\"{x}\" y=\"{y}\" text-anchor=\"end\" dominant-baseline=\"middle\">{v:.3}</text>",
x = m_left - 8.0,
);
}
// Axis labels.
let _ = writeln!(
out,
" <text x=\"{x}\" y=\"{y}\" text-anchor=\"middle\">{xs_label}</text>",
x = m_left + plot_w / 2.0,
y = height as f64 - 12.0,
xs_label = escape_xml(xs_label),
);
let _ = writeln!(
out,
" <text x=\"15\" y=\"{y}\" text-anchor=\"middle\" \
transform=\"rotate(-90 15 {y})\">{ys_label}</text>",
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,
" <circle cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"3\" fill=\"#1f77b4\" \
stroke=\"#0d4a8a\" stroke-width=\"0.5\" />",
);
}
out.push_str("</svg>");
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!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\">\
<text x=\"10\" y=\"20\">{}</text></svg>",
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,
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" \
font-family=\"system-ui, sans-serif\" font-size=\"12\">",
);
let _ = writeln!(
out,
" <rect x=\"0\" y=\"0\" width=\"{width}\" height=\"{height}\" fill=\"white\"/>",
);
let _ = writeln!(
out,
" <text x=\"{x}\" y=\"22\" font-size=\"16\" font-weight=\"bold\">{title}</text>",
x = m_left,
title = escape_xml(title),
);
let _ = writeln!(
out,
" <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"none\" stroke=\"#888\" />",
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,
" <line x1=\"{x}\" y1=\"{y0}\" x2=\"{x}\" y2=\"{y1}\" stroke=\"#888\" />",
y0 = m_top + plot_h,
y1 = m_top + plot_h + 5.0,
);
let _ = writeln!(
out,
" <text x=\"{x}\" y=\"{y}\" text-anchor=\"middle\">{g}</text>",
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,
" <line x1=\"{x0}\" y1=\"{y}\" x2=\"{x1}\" y2=\"{y}\" stroke=\"#888\" />",
x0 = m_left - 5.0,
x1 = m_left,
);
let _ = writeln!(
out,
" <text x=\"{x}\" y=\"{y}\" text-anchor=\"end\" dominant-baseline=\"middle\">{v:.3e}</text>",
x = m_left - 8.0,
);
}
// Axis labels.
let _ = writeln!(
out,
" <text x=\"{x}\" y=\"{y}\" text-anchor=\"middle\">generation</text>",
x = m_left + plot_w / 2.0,
y = height as f64 - 12.0,
);
let _ = writeln!(
out,
" <text x=\"15\" y=\"{y}\" text-anchor=\"middle\" \
transform=\"rotate(-90 15 {y})\">{label}</text>",
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,
" <polyline points=\"{points}\" fill=\"none\" stroke=\"#1f77b4\" stroke-width=\"1.5\" />",
);
out.push_str("</svg>");
out
}
fn bounds<I: IntoIterator<Item = f64>>(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('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
#[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("<svg"));
assert!(svg.contains("</svg>"));
assert!(svg.contains("<circle"));
}
#[test]
fn convergence_svg_well_formed() {
let bests = vec![10.0, 5.0, 2.0, 1.0, 0.5];
let svg = convergence_svg(&bests, 400, 300, "convergence", "best", true);
assert!(svg.starts_with("<svg"));
assert!(svg.contains("polyline"));
}
#[test]
fn convergence_empty_returns_valid_svg() {
let svg = convergence_svg(&[], 200, 100, "empty", "y", true);
assert!(svg.contains("<svg"));
assert!(svg.contains("</svg>"));
}
}
+101
View File
@@ -237,6 +237,107 @@ where
} }
} }
#[cfg(feature = "async")]
impl DifferentialEvolution {
/// Async version of [`Optimizer::run`] — drives evaluations through
/// the user-chosen async runtime. Available only with the `async`
/// feature.
///
/// `concurrency` bounds in-flight evaluations per batch (initial
/// population and per-generation trials).
pub async fn run_async<P>(
&mut self,
problem: &P,
concurrency: usize,
) -> OptimizationResult<Vec<f64>>
where
P: crate::core::async_problem::AsyncProblem<Decision = Vec<f64>>,
{
use rand::Rng as _;
use crate::algorithms::parallel_eval_async::evaluate_batch_async;
use crate::core::candidate::Candidate;
use crate::traits::Initializer as _;
assert!(
self.config.population_size >= 4,
"DifferentialEvolution requires population_size >= 4",
);
assert!(
(0.0..=1.0).contains(&self.config.crossover_probability),
"DifferentialEvolution crossover_probability must be in [0.0, 1.0]",
);
let objectives = problem.objectives();
assert!(
objectives.is_single_objective(),
"DifferentialEvolution only supports single-objective problems",
);
let direction = objectives.objectives[0].direction;
let dim = self.bounds.bounds.len();
let n = self.config.population_size;
let mut rng = rng_from_seed(self.config.seed);
let mut decisions: Vec<Vec<f64>> = self.bounds.initialize(n, &mut rng);
let initial_pop = evaluate_batch_async(problem, decisions.clone(), concurrency).await;
let mut evaluations = initial_pop.len();
let mut current_pop = initial_pop;
let mut evals: Vec<f64> = current_pop
.iter()
.map(|c| c.evaluation.objectives[0])
.collect();
for _generation in 0..self.config.generations {
let trials: Vec<Vec<f64>> = (0..n)
.map(|i| {
let (r1, r2, r3) = pick_three_distinct(n, i, &mut rng);
let j_rand = rng.random_range(0..dim);
let mut trial = decisions[i].clone();
for j in 0..dim {
let take_donor =
rng.random_bool(self.config.crossover_probability) || j == j_rand;
if take_donor {
let mutant = decisions[r1][j]
+ self.config.differential_weight
* (decisions[r2][j] - decisions[r3][j]);
let (lo, hi) = self.bounds.bounds[j];
trial[j] = mutant.clamp(lo, hi);
}
}
trial
})
.collect();
let trial_cands: Vec<Candidate<Vec<f64>>> =
evaluate_batch_async(problem, trials, concurrency).await;
evaluations += trial_cands.len();
for (i, trial_cand) in trial_cands.into_iter().enumerate() {
let trial_obj = trial_cand.evaluation.objectives[0];
let target_obj = evals[i];
let trial_better = match direction {
crate::core::objective::Direction::Minimize => trial_obj <= target_obj,
crate::core::objective::Direction::Maximize => trial_obj >= target_obj,
};
if trial_better {
decisions[i] = trial_cand.decision.clone();
evals[i] = trial_obj;
current_pop[i] = trial_cand;
}
}
}
let front = pareto_front(&current_pop, &objectives);
let best = best_candidate(&current_pop, &objectives);
OptimizationResult::new(
Population::new(current_pop),
front,
best,
evaluations,
self.config.generations,
)
}
}
fn pick_three_distinct( fn pick_three_distinct(
n: usize, n: usize,
exclude: usize, exclude: usize,
+2
View File
@@ -22,6 +22,8 @@ pub mod nsga3;
pub mod one_plus_one_es; pub mod one_plus_one_es;
pub mod paes; pub mod paes;
pub(crate) mod parallel_eval; pub(crate) mod parallel_eval;
#[cfg(feature = "async")]
pub(crate) mod parallel_eval_async;
pub mod particle_swarm; pub mod particle_swarm;
pub mod pesa2; pub mod pesa2;
pub mod random_search; pub mod random_search;
+58
View File
@@ -0,0 +1,58 @@
//! Async population evaluator.
//!
//! Available only with the `async` feature. Used by the `run_async`
//! method on algorithms that support async problems.
use futures::stream::{FuturesOrdered, StreamExt};
use crate::core::async_problem::AsyncProblem;
use crate::core::candidate::Candidate;
/// Evaluate every decision concurrently against `problem`, preserving
/// input order in the returned vector. Concurrency is bounded by
/// `concurrency` (≥ 1) — too high a value wastes memory and may
/// overload downstream services; too low forfeits parallelism.
///
/// Returns a future that the caller drives via their preferred
/// runtime (typically tokio).
pub async fn evaluate_batch_async<P>(
problem: &P,
decisions: Vec<P::Decision>,
concurrency: usize,
) -> Vec<Candidate<P::Decision>>
where
P: AsyncProblem,
{
assert!(
concurrency >= 1,
"evaluate_batch_async concurrency must be >= 1"
);
let mut out: Vec<Candidate<P::Decision>> = Vec::with_capacity(decisions.len());
// Process in concurrency-bounded chunks to keep peak memory low
// and avoid blasting downstream services. Each chunk uses
// FuturesOrdered to preserve per-chunk order, and chunks are
// emitted in their natural order.
let mut iter = decisions.into_iter();
loop {
let mut futs = FuturesOrdered::new();
for _ in 0..concurrency {
match iter.next() {
Some(d) => {
futs.push_back(async move {
let e = problem.evaluate_async(&d).await;
Candidate::new(d, e)
});
}
None => break,
}
}
if futs.is_empty() {
break;
}
while let Some(c) = futs.next().await {
out.push(c);
}
}
out
}
+45
View File
@@ -134,6 +134,51 @@ where
} }
} }
#[cfg(feature = "async")]
impl<I> RandomSearch<I> {
/// Async version of [`Optimizer::run`] — drives evaluations through
/// the user-chosen async runtime (typically tokio). Useful when
/// `evaluate` is IO-bound (HTTP, RPC, subprocess).
///
/// `concurrency` bounds how many evaluations are in-flight at once;
/// `1` is sequential, larger values push more load to the
/// downstream service.
///
/// Available only with the `async` feature.
pub async fn run_async<P>(
&mut self,
problem: &P,
concurrency: usize,
) -> OptimizationResult<P::Decision>
where
P: crate::core::async_problem::AsyncProblem,
I: Initializer<P::Decision>,
{
use crate::algorithms::parallel_eval_async::evaluate_batch_async;
let objectives = problem.objectives();
let mut rng = rng_from_seed(self.config.seed);
let mut all: Vec<Candidate<P::Decision>> = Vec::new();
let mut evaluations = 0usize;
for _ in 0..self.config.iterations {
let decisions = self
.initializer
.initialize(self.config.batch_size, &mut rng);
evaluations += decisions.len();
let cands = evaluate_batch_async(problem, decisions, concurrency).await;
all.extend(cands);
}
let front = pareto_front(&all, &objectives);
let best = best_candidate(&all, &objectives);
OptimizationResult::new(
Population::new(all),
front,
best,
evaluations,
self.config.iterations,
)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+54
View File
@@ -0,0 +1,54 @@
//! Async-evaluable problems for IO-bound workloads.
//!
//! Most heuropt algorithms operate synchronously: their `Problem::evaluate`
//! returns immediately. For workloads where evaluation is *IO-bound* — calling
//! an HTTP service, querying a remote model, spawning a subprocess —
//! awaiting an async fn is much more efficient than blocking a worker
//! thread.
//!
//! [`AsyncProblem`] mirrors [`Problem`](crate::core::Problem) but its
//! `evaluate_async` returns a future. Algorithms that support async
//! evaluation (NSGA-II, DE, RandomSearch as of v0.7.0; others land
//! incrementally) expose a `run_async` method that drives evaluations
//! through a user-chosen async runtime (typically tokio).
//!
//! Available only with the `async` feature.
use std::future::Future;
use crate::core::evaluation::Evaluation;
use crate::core::objective::ObjectiveSpace;
/// A problem whose evaluation is async — useful when `evaluate` does
/// IO (HTTP, RPC, subprocess) rather than pure CPU work.
///
/// Mirrors [`Problem`](crate::core::Problem) one-for-one except that
/// `evaluate_async` returns a future. The returned future must be
/// `Send` so the algorithm can run many evaluations concurrently
/// across a runtime's worker pool.
///
/// Implementors who already have a synchronous `Problem` can adapt
/// to `AsyncProblem` with a one-line wrapper:
///
/// ```ignore
/// impl AsyncProblem for MyProblem {
/// type Decision = <Self as Problem>::Decision;
/// fn objectives(&self) -> ObjectiveSpace { Problem::objectives(self) }
/// async fn evaluate_async(&self, x: &Self::Decision) -> Evaluation {
/// Problem::evaluate(self, x)
/// }
/// }
/// ```
pub trait AsyncProblem: Sync {
/// The thing the optimizer changes. Same constraints as
/// [`Problem::Decision`](crate::core::Problem::Decision).
type Decision: Clone + Send + Sync;
/// Return the objectives for this problem.
fn objectives(&self) -> ObjectiveSpace;
/// Evaluate `decision` asynchronously. The returned future is
/// driven by whichever runtime the algorithm's `run_async` is
/// invoked from.
fn evaluate_async(&self, decision: &Self::Decision) -> impl Future<Output = Evaluation> + Send;
}
+4
View File
@@ -1,5 +1,7 @@
//! Concrete data types and the `Problem` trait that the rest of the crate is built on. //! Concrete data types and the `Problem` trait that the rest of the crate is built on.
#[cfg(feature = "async")]
pub mod async_problem;
pub mod candidate; pub mod candidate;
pub mod evaluation; pub mod evaluation;
pub mod objective; pub mod objective;
@@ -9,6 +11,8 @@ pub mod problem;
pub mod result; pub mod result;
pub mod rng; pub mod rng;
#[cfg(feature = "async")]
pub use async_problem::AsyncProblem;
pub use candidate::*; pub use candidate::*;
pub use evaluation::*; pub use evaluation::*;
pub use objective::*; pub use objective::*;
+2
View File
@@ -4,6 +4,8 @@
//! use heuropt::prelude::*; //! use heuropt::prelude::*;
//! ``` //! ```
#[cfg(feature = "async")]
pub use crate::core::async_problem::AsyncProblem;
pub use crate::core::{ pub use crate::core::{
Candidate, Direction, Evaluation, Objective, ObjectiveSpace, OptimizationResult, Candidate, Direction, Evaluation, Objective, ObjectiveSpace, OptimizationResult,
PartialProblem, Population, Problem, Rng, rng_from_seed, PartialProblem, Population, Problem, Rng, rng_from_seed,