feat(explorer): JSON export module + supporting metadata + example
Adds a tiny additive surface that turns any OptimizationResult into
a self-describing JSON file the heuropt-explorer webapp can load.
Real Pareto fronts have 50–200+ candidates spanning 2–7+ objectives;
reading them as numbers in a terminal scales badly. This commit
ships the heuropt-side of the explorer — the schema and the export
API. The webapp itself lives in a separate repo on its own cadence.
Three trait/type extensions, all with working defaults so existing
impls compile untouched:
- Objective gains optional `label: Option<String>` and
`unit: Option<String>` fields, plus fluent builders
`.with_label("Price").with_unit(\"\$k\")`. Existing
`Objective::minimize(name)` / `Objective::maximize(name)` are
unchanged. Both fields are #[serde(default,
skip_serializing_if = \"Option::is_none\")] so existing JSON
round-trips cleanly.
- Problem trait gains an optional
`fn decision_schema(&self) -> Vec<DecisionVariable>` with default
empty impl. Override it to provide pretty names / labels / units /
bounds for the explorer; the default produces fallback x[0],
x[1], … names. New DecisionVariable type at
`heuropt::core::DecisionVariable` with builder methods.
- New `heuropt::traits::AlgorithmInfo` trait with `name()`
(required) and `seed()` (default None). Every built-in algorithm
— all 33 — implements it. Separate from Optimizer<P> so
multi-fidelity Hyperband (which uses PartialProblem) implements
it uniformly.
The new explorer module:
- `heuropt::explorer::ExplorerExport` envelope with versioned
schema (SCHEMA_VERSION = 1).
- ExplorerCandidate per row, with front_rank from
non_dominated_sort attached at export time so downstream tools
don't re-derive it.
- ToDecisionValues adapter trait with provided impls for Vec<f64>,
Vec<bool>, Vec<usize>, Vec<i64>; custom decision types implement
one method.
- Free functions to_json / to_writer / to_file plus a builder API
(with_algorithm_info, with_problem_name, with_wall_clock,
with_timestamp).
- Gated on the existing `serde` feature, which now also pulls in
`serde_json` as a dep.
The example:
- `examples/pick_a_car.rs` — promotes the README's PickACar to a
real example, fully enriched with Objective labels/units and a
decision_schema. Runs NSGA-III for 200 generations, prints a
sample slice, writes pick_a_car.json. Gated on `serde`.
10 new explorer unit tests cover round-trip serde, fallback
decision-variable names, enriched export, AlgorithmInfo flow,
front-rank correctness, and the ToDecisionValues impls. Lib test
count went from 229 to 242.
This commit is contained in:
@@ -424,6 +424,15 @@ fn estimate_p(front_indices: &[usize], translated: &[Vec<f64>], m: usize) -> f64
|
||||
best_p
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for AgeMoea<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"AgeMoea"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -429,6 +429,15 @@ fn better_than_so(
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for AntColonyTsp {
|
||||
fn name(&self) -> &'static str {
|
||||
"AntColonyTsp"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -540,6 +540,15 @@ impl BayesianOpt {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for BayesianOpt {
|
||||
fn name(&self) -> &'static str {
|
||||
"BayesianOpt"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -628,6 +628,15 @@ fn better_than_so(
|
||||
compare_so(a, b, direction) == std::cmp::Ordering::Less
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for CmaEs {
|
||||
fn name(&self) -> &'static str {
|
||||
"CmaEs"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -303,6 +303,15 @@ fn pick_three_distinct(
|
||||
(a, b, c)
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for DifferentialEvolution {
|
||||
fn name(&self) -> &'static str {
|
||||
"DifferentialEvolution"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -396,6 +396,15 @@ fn box_dominates(a: &[i64], b: &[i64]) -> bool {
|
||||
strictly_less
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for EpsilonMoea<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"EpsilonMoea"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -317,6 +317,15 @@ fn compare_for_fitness<D>(
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for GeneticAlgorithm<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"GeneticAlgorithm"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -334,6 +334,15 @@ fn environmental_selection<D: Clone>(
|
||||
selected.into_iter().map(|i| combined[i].clone()).collect()
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Grea<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Grea"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -224,6 +224,15 @@ impl<I, V> HillClimber<I, V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for HillClimber<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"HillClimber"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -459,6 +459,15 @@ fn binary_tournament(fitness: &[f64], rng: &mut Rng) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Hype<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Hype"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -329,6 +329,19 @@ fn better(a: &Evaluation, b: &Evaluation, direction: Direction) -> bool {
|
||||
compare(a, b, direction) == std::cmp::Ordering::Less
|
||||
}
|
||||
|
||||
impl<I, D> crate::traits::AlgorithmInfo for Hyperband<I, D>
|
||||
where
|
||||
D: Clone,
|
||||
I: Initializer<D>,
|
||||
{
|
||||
fn name(&self) -> &'static str {
|
||||
"Hyperband"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -385,6 +385,15 @@ fn binary_tournament(fitness: &[f64], rng: &mut Rng) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Ibea<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Ibea"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -287,6 +287,15 @@ fn better(a: &Evaluation, b: &Evaluation, direction: Direction) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for IpopCmaEs {
|
||||
fn name(&self) -> &'static str {
|
||||
"IpopCmaEs"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -328,6 +328,15 @@ fn perpendicular_distance(point: &[f64], extremes: &[usize], oriented: &[Vec<f64
|
||||
(dot - b).abs() / norm
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Knea<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Knea"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -363,6 +363,15 @@ fn weight_distance(a: &[f64], b: &[f64]) -> f64 {
|
||||
.sqrt()
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Moead<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Moead"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -330,6 +330,15 @@ impl Mopso {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for Mopso {
|
||||
fn name(&self) -> &'static str {
|
||||
"Mopso"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -450,6 +450,12 @@ impl NelderMead {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for NelderMead {
|
||||
fn name(&self) -> &'static str {
|
||||
"NelderMead"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -364,6 +364,15 @@ fn binary_tournament<D>(entries: &[Nsga2Entry<D>], rng: &mut Rng) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Nsga2<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Nsga2"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -542,6 +542,15 @@ fn associate(
|
||||
(assoc, dist)
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Nsga3<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Nsga3"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -285,6 +285,15 @@ impl OnePlusOneEs {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for OnePlusOneEs {
|
||||
fn name(&self) -> &'static str {
|
||||
"OnePlusOneEs"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -242,6 +242,15 @@ impl<I, V> Paes<I, V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Paes<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Paes"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -357,6 +357,15 @@ fn best_index(values: &[f64], direction: Direction) -> usize {
|
||||
idx
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for ParticleSwarm {
|
||||
fn name(&self) -> &'static str {
|
||||
"ParticleSwarm"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -409,6 +409,15 @@ fn truncate_by_grid<D: Clone>(archive: &mut ParetoArchive<D>, max_size: usize, d
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for PesaII<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"PesaII"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -158,6 +158,15 @@ impl<I> RandomSearch<I> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> crate::traits::AlgorithmInfo for RandomSearch<I> {
|
||||
fn name(&self) -> &'static str {
|
||||
"RandomSearch"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -466,6 +466,15 @@ fn smallest_neighbor_angle(references: &[Vec<f64>]) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Rvea<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Rvea"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -333,6 +333,15 @@ impl<I, V> SimulatedAnnealing<I, V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for SimulatedAnnealing<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"SimulatedAnnealing"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -289,6 +289,15 @@ fn pick_drop_index<D>(
|
||||
worst_front[worst_idx_in_front]
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for SmsEmoa<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"SmsEmoa"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -389,6 +389,15 @@ fn better(a: &Evaluation, b: &Evaluation, direction: Direction) -> bool {
|
||||
compare(a, b, direction) == std::cmp::Ordering::Less
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for SeparableNes {
|
||||
fn name(&self) -> &'static str {
|
||||
"SeparableNes"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -503,6 +503,15 @@ fn binary_tournament(fitness: &[f64], rng: &mut Rng) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, V> crate::traits::AlgorithmInfo for Spea2<I, V> {
|
||||
fn name(&self) -> &'static str {
|
||||
"Spea2"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -331,6 +331,20 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, I, N> crate::traits::AlgorithmInfo for TabuSearch<D, I, N>
|
||||
where
|
||||
D: Clone + Hash + Eq,
|
||||
I: Initializer<D>,
|
||||
N: FnMut(&D, &mut Rng) -> Vec<D>,
|
||||
{
|
||||
fn name(&self) -> &'static str {
|
||||
"TabuSearch"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -325,6 +325,15 @@ fn better(a: &Evaluation, b: &Evaluation, direction: Direction) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for Tlbo {
|
||||
fn name(&self) -> &'static str {
|
||||
"Tlbo"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -479,6 +479,15 @@ impl Tpe {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for Tpe {
|
||||
fn name(&self) -> &'static str {
|
||||
"Tpe"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -352,6 +352,15 @@ fn better_than_so(
|
||||
compare_so(a, b, direction) == std::cmp::Ordering::Less
|
||||
}
|
||||
|
||||
impl crate::traits::AlgorithmInfo for Umda {
|
||||
fn name(&self) -> &'static str {
|
||||
"Umda"
|
||||
}
|
||||
fn seed(&self) -> Option<u64> {
|
||||
Some(self.config.seed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user