//! `pick_a_car` — designing a car along four objectives at once. //! //! Three decision variables (engine displacement, curb weight, //! aerodynamic drag) and four objectives (price, 0-60 acceleration, //! fuel consumption, idle noise) coupled by non-linear cost //! relationships, so the Pareto front is a real surface in 3D //! decision space — not a 1D sweep that any human could enumerate. //! //! Run it: //! //! ```text //! cargo run --release --example pick_a_car --features serde //! ``` //! //! It writes a `pick_a_car.json` file in the current directory that //! you can drop into to //! filter, brush, pin, and rank the 100-car Pareto front //! interactively. use heuropt::prelude::*; struct PickACar; impl Problem for PickACar { type Decision = Vec; // [engine_liters, weight_kg, drag_cd] fn objectives(&self) -> ObjectiveSpace { ObjectiveSpace::new(vec![ Objective::minimize("price") .with_label("Price") .with_unit("$k"), Objective::minimize("zero_to_sixty") .with_label("0-60 mph") .with_unit("s"), Objective::minimize("fuel") .with_label("Fuel") .with_unit("gal/100mi"), Objective::minimize("noise") .with_label("Idle noise") .with_unit("dB"), ]) } fn decision_schema(&self) -> Vec { vec![ DecisionVariable::new("displacement") .with_label("Engine size") .with_unit("L") .with_bounds(1.0, 6.0), DecisionVariable::new("weight") .with_label("Curb weight") .with_unit("kg") .with_bounds(1100.0, 2200.0), DecisionVariable::new("drag") .with_label("Drag coefficient") .with_unit("Cd") .with_bounds(0.20, 0.40), ] } fn evaluate(&self, x: &Vec) -> Evaluation { let displacement = x[0]; let weight = x[1]; let drag = x[2]; // Price ($k): engine cost grows superlinearly; weight reduction // below 1500 kg and drag reduction below 0.35 Cd both cost extra. let engine_cost = 3.0 * displacement.powf(1.6); let weight_cost = ((1500.0 - weight).max(0.0) / 100.0).powi(2) * 2.0; let aero_cost = ((0.35 - drag).max(0.0) * 100.0).powf(1.5) * 0.4; let price = 10.0 + engine_cost + weight_cost + aero_cost; // 0-60 (s): heavier = slower; bigger engine = quicker but // with diminishing returns. let weight_factor = (weight - 1100.0) / 1000.0; let engine_factor = ((displacement - 1.0) / 5.0).max(0.0).powf(0.7); let zero_to_sixty = 5.0 + 5.0 * weight_factor - 4.0 * engine_factor; // Fuel consumption (gal/100 mi): all three decision vars matter. let fuel = 0.5 + 0.5 * displacement + 0.5 * weight / 1000.0 + 4.0 * drag; // Idle noise (dB): engine dominates, mildly non-linear. let noise = 60.0 + 3.0 * displacement.powf(1.2); Evaluation::new(vec![price, zero_to_sixty, fuel, noise]) } } fn main() { let bounds = vec![ (1.0_f64, 6.0_f64), // engine (1100.0_f64, 2200.0_f64), // weight (0.20_f64, 0.40_f64), // drag ]; let started = std::time::Instant::now(); let mut optimizer = Nsga3::new( Nsga3Config { population_size: 100, generations: 200, reference_divisions: 5, seed: 42, }, RealBounds::new(bounds.clone()), CompositeVariation { crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.9), mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / 3.0), }, ); let result = optimizer.run(&PickACar); let elapsed = started.elapsed().as_secs_f64(); // Print a short summary across the front so the user can see what // they got without leaving the terminal. let mut front: Vec<_> = result.pareto_front.iter().collect(); front.sort_by(|a, b| { a.evaluation.objectives[0] .partial_cmp(&b.evaluation.objectives[0]) .unwrap() }); println!( "Pareto front: {} cars (took {:.3} s)\n", front.len(), elapsed, ); println!( "{:>5} {:>5} {:>4} {:>6} {:>5} {:>5} {:>5}", "L", "kg", "Cd", "$k", "0-60", "fuel", "dB" ); let n = front.len(); let sample_indices = if n <= 6 { (0..n).collect::>() } else { // Six representative rows: first, ~20%, ~40%, ~60%, ~80%, last vec![0, n / 5, (2 * n) / 5, (3 * n) / 5, (4 * n) / 5, n - 1] }; for &i in &sample_indices { let c = front[i]; let d = &c.decision; let o = &c.evaluation.objectives; println!( "{:>5.2} {:>5.0} {:>4.2} {:>6.1} {:>5.1} {:>5.2} {:>5.1}", d[0], d[1], d[2], o[0], o[1], o[2], o[3] ); } // Write the explorer JSON. With the metadata the Problem provides // (objective labels + units + decision schema) plus the algorithm's // own AlgorithmInfo, this is genuinely zero-config: one call. let path = "pick_a_car.json"; let export = heuropt::explorer::ExplorerExport::from_result(&PickACar, &result) .with_algorithm_info(&optimizer) .with_problem_name("Pick a car") .with_wall_clock(elapsed); export.to_file(path).expect("failed to write JSON"); println!( "\nWrote {} candidates to {} ({}/{} on the Pareto front).", result.population.candidates.len(), path, result.pareto_front.len(), result.population.candidates.len(), ); println!("Drop it into https://swaits.github.io/heuropt-explorer/ to explore."); }