use std::io::Write;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::core::candidate::Candidate;
use crate::core::decision_variable::DecisionVariable;
use crate::core::objective::Objective;
use crate::core::problem::Problem;
use crate::core::result::OptimizationResult;
use crate::pareto::sort::non_dominated_sort;
use crate::traits::AlgorithmInfo;
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExplorerExport {
pub schema_version: u32,
pub run: RunMeta,
pub objectives: Vec<Objective>,
pub decision_variables: Vec<DecisionVariable>,
pub candidates: Vec<ExplorerCandidate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExplorerCandidate {
pub decision: Vec<serde_json::Value>,
pub objectives: Vec<f64>,
pub constraint_violation: f64,
pub feasible: bool,
pub front_rank: usize,
pub in_pareto_front: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RunMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub problem_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub algorithm: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub algorithm_full_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wall_clock_seconds: Option<f64>,
pub evaluations: usize,
pub generations: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
}
pub trait ToDecisionValues {
fn to_decision_values(&self) -> Vec<serde_json::Value>;
}
impl ToDecisionValues for Vec<f64> {
fn to_decision_values(&self) -> Vec<serde_json::Value> {
self.iter()
.map(|v| {
serde_json::Number::from_f64(*v)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null)
})
.collect()
}
}
impl ToDecisionValues for Vec<bool> {
fn to_decision_values(&self) -> Vec<serde_json::Value> {
self.iter().map(|b| serde_json::Value::Bool(*b)).collect()
}
}
impl ToDecisionValues for Vec<usize> {
fn to_decision_values(&self) -> Vec<serde_json::Value> {
self.iter()
.map(|i| serde_json::Value::Number(serde_json::Number::from(*i as u64)))
.collect()
}
}
impl ToDecisionValues for Vec<i64> {
fn to_decision_values(&self) -> Vec<serde_json::Value> {
self.iter()
.map(|i| serde_json::Value::Number(serde_json::Number::from(*i)))
.collect()
}
}
impl ExplorerExport {
pub fn from_result<P>(problem: &P, result: &OptimizationResult<P::Decision>) -> Self
where
P: Problem,
P::Decision: ToDecisionValues,
{
let objective_space = problem.objectives();
let n_obj = objective_space.objectives.len();
let user_schema = problem.decision_schema();
let decision_arity = result
.population
.candidates
.first()
.map(|c| c.decision.to_decision_values().len())
.unwrap_or(user_schema.len());
let decision_variables = pad_decision_schema(user_schema, decision_arity);
let pop_slice: &[Candidate<P::Decision>] = &result.population.candidates;
let fronts = non_dominated_sort(pop_slice, &objective_space);
let mut rank_of: Vec<usize> = vec![0; pop_slice.len()];
for (rank, front) in fronts.iter().enumerate() {
for &idx in front {
rank_of[idx] = rank;
}
}
let candidates = pop_slice
.iter()
.enumerate()
.map(|(i, c)| candidate_to_export(c, rank_of[i], n_obj))
.collect();
Self {
schema_version: SCHEMA_VERSION,
run: RunMeta {
evaluations: result.evaluations,
generations: result.generations,
..RunMeta::default()
},
objectives: objective_space.objectives,
decision_variables,
candidates,
}
}
pub fn with_algorithm_info<A: AlgorithmInfo>(mut self, algorithm: &A) -> Self {
self.run.algorithm = Some(algorithm.name().to_owned());
self.run.algorithm_full_name = Some(algorithm.full_name().to_owned());
self.run.seed = algorithm.seed();
self
}
pub fn with_problem_name(mut self, name: impl Into<String>) -> Self {
self.run.problem_name = Some(name.into());
self
}
pub fn with_wall_clock(mut self, seconds: f64) -> Self {
self.run.wall_clock_seconds = Some(seconds);
self
}
pub fn with_timestamp(mut self, timestamp: impl Into<String>) -> Self {
self.run.timestamp = Some(timestamp.into());
self
}
pub fn to_json(&self) -> serde_json::Result<String> {
serde_json::to_string_pretty(self)
}
pub fn to_writer<W: Write>(&self, writer: W) -> serde_json::Result<()> {
serde_json::to_writer_pretty(writer, self)
}
pub fn to_file<Q: AsRef<Path>>(&self, path: Q) -> std::io::Result<()> {
let file = std::fs::File::create(path)?;
let writer = std::io::BufWriter::new(file);
self.to_writer(writer)
.map_err(|e| std::io::Error::other(e.to_string()))
}
}
pub fn to_json<P, A>(
problem: &P,
algorithm: &A,
result: &OptimizationResult<P::Decision>,
) -> serde_json::Result<String>
where
P: Problem,
P::Decision: ToDecisionValues,
A: AlgorithmInfo,
{
ExplorerExport::from_result(problem, result)
.with_algorithm_info(algorithm)
.to_json()
}
pub fn to_writer<W, P, A>(
writer: W,
problem: &P,
algorithm: &A,
result: &OptimizationResult<P::Decision>,
) -> serde_json::Result<()>
where
W: Write,
P: Problem,
P::Decision: ToDecisionValues,
A: AlgorithmInfo,
{
ExplorerExport::from_result(problem, result)
.with_algorithm_info(algorithm)
.to_writer(writer)
}
pub fn to_file<Q, P, A>(
path: Q,
problem: &P,
algorithm: &A,
result: &OptimizationResult<P::Decision>,
) -> std::io::Result<()>
where
Q: AsRef<Path>,
P: Problem,
P::Decision: ToDecisionValues,
A: AlgorithmInfo,
{
ExplorerExport::from_result(problem, result)
.with_algorithm_info(algorithm)
.to_file(path)
}
fn candidate_to_export<D: ToDecisionValues>(
c: &Candidate<D>,
front_rank: usize,
n_obj: usize,
) -> ExplorerCandidate {
let objectives = if c.evaluation.objectives.len() == n_obj {
c.evaluation.objectives.clone()
} else {
let mut v = c.evaluation.objectives.clone();
v.resize(n_obj, f64::NAN);
v
};
ExplorerCandidate {
decision: c.decision.to_decision_values(),
objectives,
constraint_violation: c.evaluation.constraint_violation,
feasible: c.evaluation.constraint_violation <= 0.0,
front_rank,
in_pareto_front: front_rank == 0,
}
}
fn pad_decision_schema(
mut schema: Vec<DecisionVariable>,
decision_arity: usize,
) -> Vec<DecisionVariable> {
if schema.len() < decision_arity {
let start = schema.len();
for i in start..decision_arity {
schema.push(DecisionVariable::new(format!("x[{i}]")));
}
}
schema
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::candidate::Candidate;
use crate::core::evaluation::Evaluation;
use crate::core::objective::{Direction, Objective, ObjectiveSpace};
use crate::core::population::Population;
use crate::core::problem::Problem;
use crate::core::result::OptimizationResult;
struct TwoObjMin;
impl Problem for TwoObjMin {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![
Objective::minimize("a")
.with_label("Apples")
.with_unit("count"),
Objective::maximize("b").with_unit("score"),
])
}
fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
Evaluation::new(vec![x[0], x[1]])
}
}
struct EnrichedProblem;
impl Problem for EnrichedProblem {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("a")])
}
fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
Evaluation::new(vec![x[0]])
}
fn decision_schema(&self) -> Vec<DecisionVariable> {
vec![
DecisionVariable::new("alpha")
.with_label("Alpha")
.with_unit("u")
.with_bounds(0.0, 1.0),
DecisionVariable::new("beta"),
]
}
}
struct DummyAlgo;
impl AlgorithmInfo for DummyAlgo {
fn name(&self) -> &'static str {
"DummyAlgo"
}
fn full_name(&self) -> &'static str {
"Dummy Test Algorithm"
}
fn seed(&self) -> Option<u64> {
Some(123)
}
}
fn make_result(
decisions: Vec<Vec<f64>>,
eval: impl Fn(&[f64]) -> Vec<f64>,
) -> OptimizationResult<Vec<f64>> {
let cands: Vec<Candidate<Vec<f64>>> = decisions
.into_iter()
.map(|d| {
let objs = eval(&d);
Candidate::new(d, Evaluation::new(objs))
})
.collect();
let n = cands.len();
OptimizationResult::new(Population::new(cands.clone()), cands, None, n, 1)
}
#[test]
fn schema_version_is_one() {
assert_eq!(SCHEMA_VERSION, 1);
}
struct SingleObjMin;
impl Problem for SingleObjMin {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f")])
}
fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
Evaluation::new(vec![x[0]])
}
}
#[test]
fn zero_config_export_uses_fallback_decision_names() {
let problem = TwoObjMin;
let result = make_result(vec![vec![0.0, 1.0], vec![1.0, 0.0]], |d| d.to_vec());
let export = ExplorerExport::from_result(&problem, &result);
assert_eq!(export.schema_version, SCHEMA_VERSION);
assert_eq!(export.decision_variables.len(), 2);
assert_eq!(export.decision_variables[0].name, "x[0]");
assert_eq!(export.decision_variables[1].name, "x[1]");
assert!(export.decision_variables[0].label.is_none());
}
#[test]
fn objectives_carry_label_and_unit_through_export() {
let problem = TwoObjMin;
let result = make_result(vec![vec![0.0, 1.0]], |d| d.to_vec());
let export = ExplorerExport::from_result(&problem, &result);
assert_eq!(export.objectives.len(), 2);
assert_eq!(export.objectives[0].label.as_deref(), Some("Apples"));
assert_eq!(export.objectives[0].unit.as_deref(), Some("count"));
assert_eq!(export.objectives[1].direction, Direction::Maximize);
}
#[test]
fn enriched_decision_schema_passes_through() {
let problem = EnrichedProblem; let result = make_result(vec![vec![0.5, 0.5]], |d| vec![d[0]]);
let export = ExplorerExport::from_result(&problem, &result);
assert_eq!(export.decision_variables.len(), 2);
assert_eq!(export.decision_variables[0].name, "alpha");
assert_eq!(export.decision_variables[0].label.as_deref(), Some("Alpha"));
assert_eq!(export.decision_variables[0].min, Some(0.0));
assert_eq!(export.decision_variables[1].name, "beta");
assert!(export.decision_variables[1].min.is_none());
}
#[test]
fn front_rank_zero_for_pareto_front_members() {
let problem = SingleObjMin;
let result = make_result(vec![vec![3.0], vec![1.0], vec![2.0]], |d| vec![d[0]]);
let export = ExplorerExport::from_result(&problem, &result);
assert_eq!(export.candidates[1].front_rank, 0);
assert!(export.candidates[1].in_pareto_front);
assert_eq!(export.candidates[2].front_rank, 1);
assert!(!export.candidates[2].in_pareto_front);
assert_eq!(export.candidates[0].front_rank, 2);
assert!(!export.candidates[0].in_pareto_front);
}
#[test]
fn algorithm_info_populates_run_meta() {
let problem = TwoObjMin;
let result = make_result(vec![vec![0.0, 1.0]], |d| d.to_vec());
let export = ExplorerExport::from_result(&problem, &result).with_algorithm_info(&DummyAlgo);
assert_eq!(export.run.algorithm.as_deref(), Some("DummyAlgo"));
assert_eq!(
export.run.algorithm_full_name.as_deref(),
Some("Dummy Test Algorithm"),
);
assert_eq!(export.run.seed, Some(123));
}
#[test]
fn round_trip_serde() {
let problem = TwoObjMin;
let result = make_result(vec![vec![0.0, 1.0], vec![1.0, 0.0]], |d| d.to_vec());
let export = ExplorerExport::from_result(&problem, &result)
.with_algorithm_info(&DummyAlgo)
.with_problem_name("Toy")
.with_wall_clock(0.001);
let json = export.to_json().unwrap();
let back: ExplorerExport = serde_json::from_str(&json).unwrap();
assert_eq!(back.schema_version, SCHEMA_VERSION);
assert_eq!(back.run.algorithm.as_deref(), Some("DummyAlgo"));
assert_eq!(back.candidates.len(), 2);
assert_eq!(back.objectives.len(), 2);
}
#[test]
fn vec_bool_decisions_serialize_as_bool_array() {
let v: Vec<bool> = vec![true, false, true];
let values = v.to_decision_values();
assert_eq!(values.len(), 3);
assert_eq!(values[0], serde_json::Value::Bool(true));
assert_eq!(values[1], serde_json::Value::Bool(false));
}
#[test]
fn vec_usize_decisions_serialize_as_int_array() {
let v: Vec<usize> = vec![3, 1, 4];
let values = v.to_decision_values();
assert_eq!(values.len(), 3);
assert_eq!(
values[0],
serde_json::Value::Number(serde_json::Number::from(3u64))
);
}
#[test]
fn nan_decision_renders_as_null() {
let v: Vec<f64> = vec![1.0, f64::NAN, 2.0];
let values = v.to_decision_values();
assert_eq!(values[0].as_f64(), Some(1.0));
assert_eq!(values[1], serde_json::Value::Null);
assert_eq!(values[2].as_f64(), Some(2.0));
}
#[test]
fn vec_f64_to_decision_values_exact_output() {
let v: Vec<f64> = vec![0.5, -1.25, 2.0];
let got = v.to_decision_values();
assert_eq!(got.len(), 3);
assert_eq!(got[0].as_f64(), Some(0.5));
assert_eq!(got[1].as_f64(), Some(-1.25));
assert_eq!(got[2].as_f64(), Some(2.0));
}
#[test]
fn vec_i64_to_decision_values_exact_output() {
let v: Vec<i64> = vec![-3, 0, 7];
let got = v.to_decision_values();
assert_eq!(got.len(), 3);
assert_eq!(
got[0],
serde_json::Value::Number(serde_json::Number::from(-3i64))
);
assert_eq!(
got[1],
serde_json::Value::Number(serde_json::Number::from(0i64))
);
assert_eq!(
got[2],
serde_json::Value::Number(serde_json::Number::from(7i64))
);
}
#[test]
fn vec_bool_to_decision_values_exact_output() {
let v: Vec<bool> = vec![true, false, true, false];
let got = v.to_decision_values();
assert_eq!(
got,
vec![
serde_json::Value::Bool(true),
serde_json::Value::Bool(false),
serde_json::Value::Bool(true),
serde_json::Value::Bool(false),
],
);
}
#[test]
fn vec_usize_to_decision_values_exact_output() {
let v: Vec<usize> = vec![0, 5, 42, 7];
let got = v.to_decision_values();
assert_eq!(
got,
vec![
serde_json::Value::Number(serde_json::Number::from(0u64)),
serde_json::Value::Number(serde_json::Number::from(5u64)),
serde_json::Value::Number(serde_json::Number::from(42u64)),
serde_json::Value::Number(serde_json::Number::from(7u64)),
],
);
}
#[test]
fn from_result_propagates_evaluation_and_generation_counts() {
let problem = SingleObjMin;
let cands = vec![Candidate::new(vec![1.0], Evaluation::new(vec![1.0]))];
let result = OptimizationResult::new(Population::new(cands.clone()), cands, None, 137, 9);
let export = ExplorerExport::from_result(&problem, &result);
assert_eq!(export.run.evaluations, 137);
assert_eq!(export.run.generations, 9);
}
#[test]
fn with_problem_name_sets_field_and_preserves_other_state() {
let problem = SingleObjMin;
let result = make_result(vec![vec![1.0]], |d| vec![d[0]]);
let export =
ExplorerExport::from_result(&problem, &result).with_problem_name("Toy Problem");
assert_eq!(export.run.problem_name.as_deref(), Some("Toy Problem"));
assert_eq!(export.candidates.len(), 1);
assert_eq!(export.objectives.len(), 1);
}
#[test]
fn with_wall_clock_sets_field_and_preserves_other_state() {
let problem = SingleObjMin;
let result = make_result(vec![vec![1.0]], |d| vec![d[0]]);
let export = ExplorerExport::from_result(&problem, &result).with_wall_clock(2.5);
assert_eq!(export.run.wall_clock_seconds, Some(2.5));
assert_eq!(export.candidates.len(), 1);
}
#[test]
fn with_timestamp_sets_field_and_preserves_other_state() {
let problem = SingleObjMin;
let result = make_result(vec![vec![1.0]], |d| vec![d[0]]);
let export =
ExplorerExport::from_result(&problem, &result).with_timestamp("2025-01-01T00:00:00Z");
assert_eq!(
export.run.timestamp.as_deref(),
Some("2025-01-01T00:00:00Z")
);
assert_eq!(export.candidates.len(), 1);
}
#[test]
fn to_json_emits_full_export_with_expected_fields() {
let problem = SingleObjMin;
let result = make_result(vec![vec![1.0]], |d| vec![d[0]]);
let export = ExplorerExport::from_result(&problem, &result)
.with_algorithm_info(&DummyAlgo)
.with_problem_name("MyProblem");
let json = export.to_json().unwrap();
assert!(json.contains("\"schema_version\""), "json: {json}");
assert!(json.contains("\"candidates\""), "json: {json}");
assert!(json.contains("\"MyProblem\""), "json: {json}");
assert!(json.contains("\"DummyAlgo\""), "json: {json}");
}
#[test]
fn to_writer_emits_full_export() {
let problem = SingleObjMin;
let result = make_result(vec![vec![1.0]], |d| vec![d[0]]);
let export = ExplorerExport::from_result(&problem, &result).with_problem_name("MyProblem");
let mut buf: Vec<u8> = Vec::new();
export.to_writer(&mut buf).unwrap();
assert!(!buf.is_empty());
let json = String::from_utf8(buf).unwrap();
assert!(json.contains("\"MyProblem\""));
assert_eq!(json, export.to_json().unwrap());
}
#[test]
fn to_file_writes_parseable_json() {
use std::io::Read;
let problem = SingleObjMin;
let result = make_result(vec![vec![1.0]], |d| vec![d[0]]);
let export = ExplorerExport::from_result(&problem, &result).with_problem_name("OnDisk");
let dir = std::env::temp_dir();
let path = dir.join(format!("heuropt-explorer-test-{}.json", std::process::id()));
export.to_file(&path).unwrap();
let mut s = String::new();
std::fs::File::open(&path)
.unwrap()
.read_to_string(&mut s)
.unwrap();
let _ = std::fs::remove_file(&path);
let back: ExplorerExport = serde_json::from_str(&s).unwrap();
assert_eq!(back.run.problem_name.as_deref(), Some("OnDisk"));
}
#[test]
fn free_to_json_includes_algorithm_info() {
let problem = SingleObjMin;
let result = make_result(vec![vec![1.0]], |d| vec![d[0]]);
let json = super::to_json(&problem, &DummyAlgo, &result).unwrap();
assert!(json.contains("\"DummyAlgo\""), "json: {json}");
assert!(json.contains("\"schema_version\""), "json: {json}");
}
#[test]
fn free_to_writer_writes_bytes() {
let problem = SingleObjMin;
let result = make_result(vec![vec![1.0]], |d| vec![d[0]]);
let mut buf: Vec<u8> = Vec::new();
super::to_writer(&mut buf, &problem, &DummyAlgo, &result).unwrap();
let json = String::from_utf8(buf).unwrap();
assert!(json.contains("\"DummyAlgo\""));
let expected = super::to_json(&problem, &DummyAlgo, &result).unwrap();
assert_eq!(json, expected);
}
#[test]
fn free_to_file_writes_parseable_json() {
use std::io::Read;
let problem = SingleObjMin;
let result = make_result(vec![vec![1.0]], |d| vec![d[0]]);
let dir = std::env::temp_dir();
let path = dir.join(format!(
"heuropt-explorer-test-free-{}.json",
std::process::id()
));
super::to_file(&path, &problem, &DummyAlgo, &result).unwrap();
let mut s = String::new();
std::fs::File::open(&path)
.unwrap()
.read_to_string(&mut s)
.unwrap();
let _ = std::fs::remove_file(&path);
let back: ExplorerExport = serde_json::from_str(&s).unwrap();
assert_eq!(back.run.algorithm.as_deref(), Some("DummyAlgo"));
}
#[test]
fn pad_decision_schema_extends_when_short() {
let in_schema = vec![DecisionVariable::new("alpha")];
let out = pad_decision_schema(in_schema, 3);
assert_eq!(out.len(), 3);
assert_eq!(out[0].name, "alpha");
assert_eq!(out[1].name, "x[1]");
assert_eq!(out[2].name, "x[2]");
}
#[test]
fn pad_decision_schema_unchanged_at_exact_length() {
let in_schema = vec![
DecisionVariable::new("alpha"),
DecisionVariable::new("beta"),
];
let out = pad_decision_schema(in_schema, 2);
assert_eq!(out.len(), 2);
assert_eq!(out[0].name, "alpha");
assert_eq!(out[1].name, "beta");
}
#[test]
fn pad_decision_schema_unchanged_when_longer_than_arity() {
let in_schema = vec![
DecisionVariable::new("alpha"),
DecisionVariable::new("beta"),
DecisionVariable::new("gamma"),
];
let out = pad_decision_schema(in_schema, 2);
assert_eq!(out.len(), 3);
assert_eq!(out[2].name, "gamma");
}
#[test]
fn candidate_to_export_front_rank_zero_is_in_pareto_front() {
let c: Candidate<Vec<f64>> = Candidate::new(vec![1.0], Evaluation::new(vec![1.0]));
let exported = candidate_to_export(&c, 0, 1);
assert!(exported.in_pareto_front);
assert_eq!(exported.front_rank, 0);
}
#[test]
fn candidate_to_export_front_rank_one_is_not_in_pareto_front() {
let c: Candidate<Vec<f64>> = Candidate::new(vec![1.0], Evaluation::new(vec![1.0]));
let exported = candidate_to_export(&c, 1, 1);
assert!(!exported.in_pareto_front);
assert_eq!(exported.front_rank, 1);
}
#[test]
fn candidate_to_export_feasibility_at_zero_violation() {
let mut ev = Evaluation::new(vec![1.0]);
ev.constraint_violation = 0.0;
let c: Candidate<Vec<f64>> = Candidate::new(vec![1.0], ev);
let exported = candidate_to_export(&c, 0, 1);
assert!(exported.feasible);
}
#[test]
fn candidate_to_export_feasibility_negative_violation() {
let mut ev = Evaluation::new(vec![1.0]);
ev.constraint_violation = -0.1;
let c: Candidate<Vec<f64>> = Candidate::new(vec![1.0], ev);
let exported = candidate_to_export(&c, 0, 1);
assert!(exported.feasible);
}
#[test]
fn candidate_to_export_infeasibility_positive_violation() {
let mut ev = Evaluation::new(vec![1.0]);
ev.constraint_violation = 0.5;
let c: Candidate<Vec<f64>> = Candidate::new(vec![1.0], ev);
let exported = candidate_to_export(&c, 0, 1);
assert!(!exported.feasible);
assert_eq!(exported.constraint_violation, 0.5);
}
#[test]
fn candidate_to_export_pads_short_objectives_with_nan() {
let c: Candidate<Vec<f64>> = Candidate::new(vec![1.0], Evaluation::new(vec![1.0]));
let exported = candidate_to_export(&c, 0, 3);
assert_eq!(exported.objectives.len(), 3);
assert_eq!(exported.objectives[0], 1.0);
assert!(exported.objectives[1].is_nan());
assert!(exported.objectives[2].is_nan());
}
#[test]
fn candidate_to_export_truncates_long_objectives() {
let c: Candidate<Vec<f64>> =
Candidate::new(vec![1.0], Evaluation::new(vec![1.0, 2.0, 3.0]));
let exported = candidate_to_export(&c, 0, 2);
assert_eq!(exported.objectives.len(), 2);
assert_eq!(exported.objectives[0], 1.0);
assert_eq!(exported.objectives[1], 2.0);
}
}