141 lines
4.5 KiB
Rust
141 lines
4.5 KiB
Rust
//! Pareto dominance enum and pairwise dominance comparison.
|
|
|
|
use crate::core::evaluation::Evaluation;
|
|
use crate::core::objective::ObjectiveSpace;
|
|
|
|
#[cfg(feature = "serde")]
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// The Pareto relationship between two evaluations.
|
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Dominance {
|
|
/// `a` dominates `b`.
|
|
Dominates,
|
|
/// `a` is dominated by `b`.
|
|
DominatedBy,
|
|
/// Neither dominates the other.
|
|
NonDominated,
|
|
/// All objective values are equal after orientation conversion.
|
|
Equal,
|
|
}
|
|
|
|
/// Pareto-compare two evaluations under the given objective space.
|
|
///
|
|
/// Behavior (spec §9.2):
|
|
///
|
|
/// 1. Feasible candidates dominate infeasible candidates.
|
|
/// 2. If both are infeasible, the candidate with smaller
|
|
/// `constraint_violation` dominates.
|
|
/// 3. Otherwise compare objective values after converting both to
|
|
/// minimization orientation via [`ObjectiveSpace::as_minimization`].
|
|
pub fn pareto_compare(a: &Evaluation, b: &Evaluation, objectives: &ObjectiveSpace) -> Dominance {
|
|
let a_feasible = a.is_feasible();
|
|
let b_feasible = b.is_feasible();
|
|
match (a_feasible, b_feasible) {
|
|
(true, false) => return Dominance::Dominates,
|
|
(false, true) => return Dominance::DominatedBy,
|
|
(false, false) => {
|
|
return if a.constraint_violation < b.constraint_violation {
|
|
Dominance::Dominates
|
|
} else if a.constraint_violation > b.constraint_violation {
|
|
Dominance::DominatedBy
|
|
} else {
|
|
Dominance::Equal
|
|
};
|
|
}
|
|
(true, true) => {}
|
|
}
|
|
|
|
let am = objectives.as_minimization(&a.objectives);
|
|
let bm = objectives.as_minimization(&b.objectives);
|
|
|
|
let mut a_better_anywhere = false;
|
|
let mut b_better_anywhere = false;
|
|
for (av, bv) in am.iter().zip(bm.iter()) {
|
|
if av < bv {
|
|
a_better_anywhere = true;
|
|
} else if av > bv {
|
|
b_better_anywhere = true;
|
|
}
|
|
}
|
|
|
|
match (a_better_anywhere, b_better_anywhere) {
|
|
(true, false) => Dominance::Dominates,
|
|
(false, true) => Dominance::DominatedBy,
|
|
(false, false) => Dominance::Equal,
|
|
(true, true) => Dominance::NonDominated,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::core::objective::Objective;
|
|
|
|
fn space_min2() -> ObjectiveSpace {
|
|
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
|
|
}
|
|
|
|
#[test]
|
|
fn a_dominates_b() {
|
|
let s = space_min2();
|
|
let a = Evaluation::new(vec![1.0, 1.0]);
|
|
let b = Evaluation::new(vec![2.0, 2.0]);
|
|
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Dominates);
|
|
}
|
|
|
|
#[test]
|
|
fn a_dominated_by_b() {
|
|
let s = space_min2();
|
|
let a = Evaluation::new(vec![3.0, 4.0]);
|
|
let b = Evaluation::new(vec![1.0, 2.0]);
|
|
assert_eq!(pareto_compare(&a, &b, &s), Dominance::DominatedBy);
|
|
}
|
|
|
|
#[test]
|
|
fn non_dominated_pair() {
|
|
let s = space_min2();
|
|
let a = Evaluation::new(vec![1.0, 4.0]);
|
|
let b = Evaluation::new(vec![3.0, 2.0]);
|
|
assert_eq!(pareto_compare(&a, &b, &s), Dominance::NonDominated);
|
|
}
|
|
|
|
#[test]
|
|
fn equal_pair() {
|
|
let s = space_min2();
|
|
let a = Evaluation::new(vec![2.0, 2.0]);
|
|
let b = Evaluation::new(vec![2.0, 2.0]);
|
|
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Equal);
|
|
}
|
|
|
|
#[test]
|
|
fn feasible_beats_infeasible() {
|
|
let s = space_min2();
|
|
let a = Evaluation::new(vec![10.0, 10.0]);
|
|
let b = Evaluation::constrained(vec![1.0, 1.0], 5.0);
|
|
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Dominates);
|
|
assert_eq!(pareto_compare(&b, &a, &s), Dominance::DominatedBy);
|
|
}
|
|
|
|
#[test]
|
|
fn smaller_violation_wins_among_infeasible() {
|
|
let s = space_min2();
|
|
let a = Evaluation::constrained(vec![100.0, 100.0], 1.0);
|
|
let b = Evaluation::constrained(vec![0.0, 0.0], 5.0);
|
|
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Dominates);
|
|
}
|
|
|
|
#[test]
|
|
fn maximize_axis_handled_correctly() {
|
|
let s = ObjectiveSpace::new(vec![
|
|
Objective::minimize("cost"),
|
|
Objective::maximize("accuracy"),
|
|
]);
|
|
// a: cost=1 (better), accuracy=0.9 (better) → dominates b
|
|
let a = Evaluation::new(vec![1.0, 0.9]);
|
|
let b = Evaluation::new(vec![2.0, 0.8]);
|
|
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Dominates);
|
|
}
|
|
}
|