feat(pareto): add Dominance enum and pareto_compare
Implements spec §9.1–9.2: - `Dominance` enum (Dominates / DominatedBy / NonDominated / Equal). - `pareto_compare(a, b, objectives)` with the four-step rule: feasible beats infeasible; among infeasibles, lower violation wins; among feasibles, compare in minimization orientation via ObjectiveSpace::as_minimization. Unit tests cover dominance, dominated-by, non-dominated, equal, the feasibility tiebreak, and that Maximize objectives are handled correctly (spec §18.2).
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Pareto utilities: dominance, fronts, sorting, crowding, and an archive.
|
||||
|
||||
pub mod dominance;
|
||||
|
||||
pub use dominance::*;
|
||||
Reference in New Issue
Block a user