Skip to main content

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}