fugue_evo/fitness/multi_objective.rs
1//! Multi-objective fitness abstraction
2//!
3//! The [`MultiObjectiveFitness`] trait is shared by the classic NSGA-II
4//! algorithm (`algorithms::nsga2`, `classic` feature) and the Pareto-posterior
5//! inference layer (`inference::pareto`, `ppl` feature), so it lives here in
6//! the always-available core rather than in either layer.
7
8/// Multi-objective fitness function trait
9///
10/// `Send + Sync` only with the `parallel` feature, for the same reason and
11/// with the same caveat as [`Fitness`](crate::fitness::traits::Fitness): a
12/// deliberate, known non-additive feature that keeps `!Send` (JavaScript
13/// callback) fitnesses valid in single-threaded WASM builds.
14#[cfg(feature = "parallel")]
15pub trait MultiObjectiveFitness<G>: Send + Sync {
16 /// Number of objectives
17 fn num_objectives(&self) -> usize;
18
19 /// Evaluate all objectives (all to be minimized by convention)
20 fn evaluate(&self, genome: &G) -> Vec<f64>;
21}
22
23/// Multi-objective fitness function trait (non-parallel version; no
24/// `Send + Sync` supertrait — see the `parallel` variant).
25#[cfg(not(feature = "parallel"))]
26pub trait MultiObjectiveFitness<G> {
27 /// Number of objectives
28 fn num_objectives(&self) -> usize;
29
30 /// Evaluate all objectives (all to be minimized by convention)
31 fn evaluate(&self, genome: &G) -> Vec<f64>;
32}
33
34/// Adapts a closure into a [`MultiObjectiveFitness`] with an explicit objective
35/// count.
36///
37/// A bare `Fn(&G) -> Vec<f64>` cannot report how many objectives it produces,
38/// so the previous blanket impl hardcoded `num_objectives() == 2`, silently
39/// mis-reporting the count for any 3+ objective problem (EV-85). This wrapper
40/// requires the caller to state the true objective count at construction.
41///
42/// ```
43/// use fugue_evo::fitness::multi_objective::{ClosureMultiObjective, MultiObjectiveFitness};
44/// use fugue_evo::genome::real_vector::RealVector;
45/// use fugue_evo::genome::traits::RealValuedGenome;
46///
47/// let fitness = ClosureMultiObjective::new(3, |g: &RealVector| {
48/// let x = g.genes()[0];
49/// vec![x, x * x, x + 1.0]
50/// });
51/// assert_eq!(fitness.num_objectives(), 3);
52/// ```
53pub struct ClosureMultiObjective<G, F> {
54 num_objectives: usize,
55 f: F,
56 _phantom: std::marker::PhantomData<fn() -> G>,
57}
58
59impl<G, F> ClosureMultiObjective<G, F>
60where
61 F: Fn(&G) -> Vec<f64>,
62{
63 /// Wrap `f`, declaring that it returns `num_objectives` objective values.
64 pub fn new(num_objectives: usize, f: F) -> Self {
65 Self {
66 num_objectives,
67 f,
68 _phantom: std::marker::PhantomData,
69 }
70 }
71}
72
73#[cfg(feature = "parallel")]
74impl<G, F> MultiObjectiveFitness<G> for ClosureMultiObjective<G, F>
75where
76 F: Fn(&G) -> Vec<f64> + Send + Sync,
77{
78 fn num_objectives(&self) -> usize {
79 self.num_objectives
80 }
81
82 fn evaluate(&self, genome: &G) -> Vec<f64> {
83 (self.f)(genome)
84 }
85}
86
87#[cfg(not(feature = "parallel"))]
88impl<G, F> MultiObjectiveFitness<G> for ClosureMultiObjective<G, F>
89where
90 F: Fn(&G) -> Vec<f64>,
91{
92 fn num_objectives(&self) -> usize {
93 self.num_objectives
94 }
95
96 fn evaluate(&self, genome: &G) -> Vec<f64> {
97 (self.f)(genome)
98 }
99}