use super::solutions::MultiObjectiveSolution;
use std::cmp::Ordering;
pub fn dominates(a: &[f64], b: &[f64]) -> bool {
debug_assert_eq!(a.len(), b.len(), "Objective vectors must have equal length");
let mut at_least_one_strict = false;
for (ai, bi) in a.iter().zip(b.iter()) {
if ai > bi {
return false;
}
if ai < bi {
at_least_one_strict = true;
}
}
at_least_one_strict
}
pub fn solution_dominates(a: &MultiObjectiveSolution, b: &MultiObjectiveSolution) -> bool {
a.dominates(b)
}
pub fn fast_non_dominated_sort(objectives: &[Vec<f64>]) -> Vec<Vec<usize>> {
let n = objectives.len();
if n == 0 {
return vec![];
}
let mut domination_counts = vec![0usize; n];
let mut dominated_sets: Vec<Vec<usize>> = vec![Vec::new(); n];
for i in 0..n {
for j in (i + 1)..n {
if dominates(&objectives[i], &objectives[j]) {
dominated_sets[i].push(j);
domination_counts[j] += 1;
} else if dominates(&objectives[j], &objectives[i]) {
dominated_sets[j].push(i);
domination_counts[i] += 1;
}
}
}
let mut fronts: Vec<Vec<usize>> = Vec::new();
let mut current_front: Vec<usize> = Vec::new();
for i in 0..n {
if domination_counts[i] == 0 {
current_front.push(i);
}
}
while !current_front.is_empty() {
let mut next_front = Vec::new();
for &i in ¤t_front {
for &j in &dominated_sets[i] {
domination_counts[j] -= 1;
if domination_counts[j] == 0 {
next_front.push(j);
}
}
}
fronts.push(current_front);
current_front = next_front;
}
fronts
}
pub fn non_dominated_sort_solutions(solutions: &mut [MultiObjectiveSolution]) -> Vec<Vec<usize>> {
let objectives: Vec<Vec<f64>> = solutions.iter().map(|s| s.objectives.to_vec()).collect();
let fronts = fast_non_dominated_sort(&objectives);
for (rank, front) in fronts.iter().enumerate() {
for &idx in front {
solutions[idx].rank = rank;
}
}
fronts
}
pub fn crowding_distance(
solutions: &[MultiObjectiveSolution],
front_indices: &[usize],
) -> Vec<f64> {
let n = front_indices.len();
if n == 0 {
return vec![];
}
if n <= 2 {
return vec![f64::INFINITY; n];
}
let n_objectives = solutions[front_indices[0]].objectives.len();
let mut distances = vec![0.0_f64; n];
for obj in 0..n_objectives {
let mut sorted: Vec<(usize, f64)> = (0..n)
.map(|local_idx| {
let global_idx = front_indices[local_idx];
(local_idx, solutions[global_idx].objectives[obj])
})
.collect();
sorted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
let obj_min = sorted[0].1;
let obj_max = sorted[n - 1].1;
let obj_range = obj_max - obj_min;
distances[sorted[0].0] = f64::INFINITY;
distances[sorted[n - 1].0] = f64::INFINITY;
if obj_range > 0.0 {
for i in 1..n - 1 {
let local_idx = sorted[i].0;
if distances[local_idx].is_finite() {
let neighbor_dist = (sorted[i + 1].1 - sorted[i - 1].1) / obj_range;
distances[local_idx] += neighbor_dist;
}
}
}
}
distances
}
pub fn assign_crowding_distances(
solutions: &mut [MultiObjectiveSolution],
front_indices: &[usize],
) {
let distances = crowding_distance(solutions, front_indices);
for (local_idx, &global_idx) in front_indices.iter().enumerate() {
solutions[global_idx].crowding_distance = distances[local_idx];
}
}
pub fn pareto_front(solutions: &[MultiObjectiveSolution]) -> Vec<MultiObjectiveSolution> {
if solutions.is_empty() {
return vec![];
}
let mut front: Vec<MultiObjectiveSolution> = Vec::new();
for candidate in solutions {
let mut is_dominated = false;
for existing in &front {
if existing.dominates(candidate) {
is_dominated = true;
break;
}
}
if !is_dominated {
front.retain(|existing| !candidate.dominates(existing));
front.push(candidate.clone());
}
}
front
}
pub fn hypervolume(solutions: &[MultiObjectiveSolution], reference_point: &[f64]) -> f64 {
if solutions.is_empty() {
return 0.0;
}
let n_objectives = reference_point.len();
let valid_solutions: Vec<&MultiObjectiveSolution> = solutions
.iter()
.filter(|sol| {
sol.objectives
.iter()
.zip(reference_point.iter())
.all(|(&obj, &rp)| obj < rp)
})
.collect();
if valid_solutions.is_empty() {
return 0.0;
}
match n_objectives {
1 => hypervolume_1d(&valid_solutions, reference_point),
2 => hypervolume_2d(&valid_solutions, reference_point),
_ => hypervolume_wfg(&valid_solutions, reference_point),
}
}
fn hypervolume_1d(solutions: &[&MultiObjectiveSolution], reference_point: &[f64]) -> f64 {
let min_obj = solutions
.iter()
.map(|s| s.objectives[0])
.fold(f64::INFINITY, f64::min);
reference_point[0] - min_obj
}
fn hypervolume_2d(solutions: &[&MultiObjectiveSolution], reference_point: &[f64]) -> f64 {
let mut points: Vec<(f64, f64)> = solutions
.iter()
.map(|s| (s.objectives[0], s.objectives[1]))
.collect();
points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
let mut staircase: Vec<(f64, f64)> = Vec::new();
let mut min_y_so_far = f64::INFINITY;
for &(x, y) in &points {
if y < min_y_so_far {
staircase.push((x, y));
min_y_so_far = y;
}
}
if staircase.is_empty() {
return 0.0;
}
let mut volume = 0.0;
for i in 0..staircase.len() {
let (x, y) = staircase[i];
let x_next = if i + 1 < staircase.len() {
staircase[i + 1].0
} else {
reference_point[0]
};
volume += (x_next - x) * (reference_point[1] - y);
}
volume
}
fn hypervolume_wfg(solutions: &[&MultiObjectiveSolution], reference_point: &[f64]) -> f64 {
let n_objectives = reference_point.len();
let points: Vec<Vec<f64>> = solutions.iter().map(|s| s.objectives.to_vec()).collect();
wfg_hv_recursive(&points, reference_point, n_objectives)
}
fn wfg_hv_recursive(points: &[Vec<f64>], reference_point: &[f64], dim: usize) -> f64 {
if points.is_empty() {
return 0.0;
}
if dim == 1 {
let min_val = points.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
return reference_point[0] - min_val;
}
if dim == 2 {
let mut pts: Vec<(f64, f64)> = points.iter().map(|p| (p[0], p[1])).collect();
pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
let mut staircase: Vec<(f64, f64)> = Vec::new();
for &(x, y) in &pts {
while let Some(&(_, prev_y)) = staircase.last() {
if prev_y >= y {
staircase.pop();
} else {
break;
}
}
staircase.push((x, y));
}
let mut vol = 0.0;
for i in 0..staircase.len() {
let x_next = if i + 1 < staircase.len() {
staircase[i + 1].0
} else {
reference_point[0]
};
vol += (x_next - staircase[i].0) * (reference_point[1] - staircase[i].1);
}
return vol;
}
let last_dim = dim - 1;
let mut sorted_points = points.to_vec();
sorted_points.sort_by(|a, b| {
b[last_dim]
.partial_cmp(&a[last_dim])
.unwrap_or(Ordering::Equal)
});
let mut volume = 0.0;
let mut prev_slice_level = reference_point[last_dim];
let mut contributing: Vec<Vec<f64>> = Vec::new();
for point in &sorted_points {
let slice_level = point[last_dim];
let slice_height = prev_slice_level - slice_level;
if slice_height > 0.0 && !contributing.is_empty() {
let projected: Vec<Vec<f64>> = contributing
.iter()
.map(|p| p[..last_dim].to_vec())
.collect();
let ref_projected = &reference_point[..last_dim];
let slice_hv = wfg_hv_recursive(&projected, ref_projected, last_dim);
volume += slice_hv * slice_height;
}
prev_slice_level = slice_level;
let new_point_projected: Vec<f64> = point[..last_dim].to_vec();
let mut is_dominated = false;
for existing in &contributing {
let existing_projected: Vec<f64> = existing[..last_dim].to_vec();
if dominates(&existing_projected, &new_point_projected) {
is_dominated = true;
break;
}
}
if !is_dominated {
contributing.retain(|existing| {
let existing_projected: Vec<f64> = existing[..last_dim].to_vec();
!dominates(&new_point_projected, &existing_projected)
});
contributing.push(point.clone());
}
}
if !contributing.is_empty() && prev_slice_level > 0.0 {
let projected: Vec<Vec<f64>> = contributing
.iter()
.map(|p| p[..last_dim].to_vec())
.collect();
let ref_projected = &reference_point[..last_dim];
let slice_hv = wfg_hv_recursive(&projected, ref_projected, last_dim);
volume += slice_hv * prev_slice_level;
}
volume
}
pub fn hypervolume_from_objectives(objectives: &[Vec<f64>], reference_point: &[f64]) -> f64 {
use scirs2_core::ndarray::Array1;
let solutions: Vec<MultiObjectiveSolution> = objectives
.iter()
.map(|objs| {
MultiObjectiveSolution::new(
Array1::zeros(1), Array1::from_vec(objs.clone()),
)
})
.collect();
hypervolume(&solutions, reference_point)
}
pub fn crowded_comparison(a: &MultiObjectiveSolution, b: &MultiObjectiveSolution) -> Ordering {
match a.rank.cmp(&b.rank) {
Ordering::Less => Ordering::Less, Ordering::Greater => Ordering::Greater, Ordering::Equal => {
b.crowding_distance
.partial_cmp(&a.crowding_distance)
.unwrap_or(Ordering::Equal)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_dominates_strict() {
assert!(dominates(&[1.0, 2.0], &[2.0, 3.0]));
}
#[test]
fn test_dominates_weak_one_equal() {
assert!(dominates(&[1.0, 2.0], &[1.0, 3.0]));
}
#[test]
fn test_dominates_equal_returns_false() {
assert!(!dominates(&[1.0, 2.0], &[1.0, 2.0]));
}
#[test]
fn test_dominates_trade_off_returns_false() {
assert!(!dominates(&[1.0, 3.0], &[2.0, 2.0]));
assert!(!dominates(&[2.0, 2.0], &[1.0, 3.0]));
}
#[test]
fn test_dominates_three_objectives() {
assert!(dominates(&[1.0, 2.0, 3.0], &[2.0, 3.0, 4.0]));
assert!(!dominates(&[1.0, 2.0, 5.0], &[2.0, 3.0, 4.0]));
}
#[test]
fn test_solution_dominates_with_constraints() {
let feasible =
MultiObjectiveSolution::new_with_constraints(array![1.0], array![5.0, 5.0], 0.0);
let infeasible =
MultiObjectiveSolution::new_with_constraints(array![1.0], array![1.0, 1.0], 1.0);
assert!(solution_dominates(&feasible, &infeasible));
assert!(!solution_dominates(&infeasible, &feasible));
}
#[test]
fn test_fast_non_dominated_sort_single_front() {
let objectives = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
let fronts = fast_non_dominated_sort(&objectives);
assert_eq!(fronts.len(), 1);
assert_eq!(fronts[0].len(), 3);
}
#[test]
fn test_fast_non_dominated_sort_two_fronts() {
let objectives = vec![
vec![1.0, 3.0], vec![3.0, 1.0], vec![2.0, 2.0], vec![3.0, 3.0], ];
let fronts = fast_non_dominated_sort(&objectives);
assert_eq!(fronts.len(), 2);
assert_eq!(fronts[0].len(), 3);
assert_eq!(fronts[1].len(), 1);
assert!(fronts[1].contains(&3));
}
#[test]
fn test_fast_non_dominated_sort_three_fronts() {
let objectives = vec![
vec![1.0, 4.0], vec![4.0, 1.0], vec![2.0, 3.0], vec![3.0, 3.0], vec![4.0, 4.0], ];
let fronts = fast_non_dominated_sort(&objectives);
assert_eq!(fronts.len(), 3);
assert_eq!(fronts[0].len(), 3); assert_eq!(fronts[1].len(), 1); assert_eq!(fronts[2].len(), 1); }
#[test]
fn test_fast_non_dominated_sort_empty() {
let objectives: Vec<Vec<f64>> = vec![];
let fronts = fast_non_dominated_sort(&objectives);
assert!(fronts.is_empty());
}
#[test]
fn test_non_dominated_sort_solutions_assigns_ranks() {
let mut solutions = vec![
MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]),
MultiObjectiveSolution::new(array![2.0], array![3.0, 1.0]),
MultiObjectiveSolution::new(array![3.0], array![4.0, 4.0]),
];
let fronts = non_dominated_sort_solutions(&mut solutions);
assert_eq!(fronts.len(), 2);
assert_eq!(solutions[0].rank, 0);
assert_eq!(solutions[1].rank, 0);
assert_eq!(solutions[2].rank, 1);
}
#[test]
fn test_crowding_distance_boundary_infinite() {
let solutions = vec![
MultiObjectiveSolution::new(array![1.0], array![1.0, 5.0]),
MultiObjectiveSolution::new(array![2.0], array![3.0, 3.0]),
MultiObjectiveSolution::new(array![3.0], array![5.0, 1.0]),
];
let front = vec![0, 1, 2];
let distances = crowding_distance(&solutions, &front);
assert_eq!(distances[0], f64::INFINITY);
assert_eq!(distances[2], f64::INFINITY);
assert!(distances[1].is_finite());
assert!(distances[1] > 0.0);
}
#[test]
fn test_crowding_distance_two_solutions() {
let solutions = vec![
MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]),
MultiObjectiveSolution::new(array![2.0], array![3.0, 1.0]),
];
let front = vec![0, 1];
let distances = crowding_distance(&solutions, &front);
assert_eq!(distances[0], f64::INFINITY);
assert_eq!(distances[1], f64::INFINITY);
}
#[test]
fn test_crowding_distance_uniform_spacing() {
let solutions = vec![
MultiObjectiveSolution::new(array![0.0], array![0.0, 4.0]),
MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]),
MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]),
MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]),
MultiObjectiveSolution::new(array![4.0], array![4.0, 0.0]),
];
let front = vec![0, 1, 2, 3, 4];
let distances = crowding_distance(&solutions, &front);
assert!(distances[0].is_infinite());
assert!(distances[4].is_infinite());
let d1 = distances[1];
let d2 = distances[2];
let d3 = distances[3];
assert!((d1 - d2).abs() < 1e-10);
assert!((d2 - d3).abs() < 1e-10);
}
#[test]
fn test_crowding_distance_empty() {
let solutions: Vec<MultiObjectiveSolution> = vec![];
let front: Vec<usize> = vec![];
let distances = crowding_distance(&solutions, &front);
assert!(distances.is_empty());
}
#[test]
fn test_assign_crowding_distances() {
let mut solutions = vec![
MultiObjectiveSolution::new(array![1.0], array![1.0, 5.0]),
MultiObjectiveSolution::new(array![2.0], array![3.0, 3.0]),
MultiObjectiveSolution::new(array![3.0], array![5.0, 1.0]),
];
let front = vec![0, 1, 2];
assign_crowding_distances(&mut solutions, &front);
assert_eq!(solutions[0].crowding_distance, f64::INFINITY);
assert_eq!(solutions[2].crowding_distance, f64::INFINITY);
assert!(solutions[1].crowding_distance.is_finite());
}
#[test]
fn test_pareto_front_extraction() {
let solutions = vec![
MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]),
MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]),
MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]),
MultiObjectiveSolution::new(array![4.0], array![2.5, 2.5]), ];
let front = pareto_front(&solutions);
assert_eq!(front.len(), 3);
}
#[test]
fn test_pareto_front_all_non_dominated() {
let solutions = vec![
MultiObjectiveSolution::new(array![1.0], array![1.0, 5.0]),
MultiObjectiveSolution::new(array![2.0], array![3.0, 3.0]),
MultiObjectiveSolution::new(array![3.0], array![5.0, 1.0]),
];
let front = pareto_front(&solutions);
assert_eq!(front.len(), 3);
}
#[test]
fn test_pareto_front_single_dominant() {
let solutions = vec![
MultiObjectiveSolution::new(array![1.0], array![1.0, 1.0]),
MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]),
MultiObjectiveSolution::new(array![3.0], array![3.0, 3.0]),
];
let front = pareto_front(&solutions);
assert_eq!(front.len(), 1);
assert_eq!(front[0].objectives[0], 1.0);
}
#[test]
fn test_pareto_front_empty() {
let solutions: Vec<MultiObjectiveSolution> = vec![];
let front = pareto_front(&solutions);
assert!(front.is_empty());
}
#[test]
fn test_pareto_front_three_objectives() {
let solutions = vec![
MultiObjectiveSolution::new(array![1.0], array![1.0, 5.0, 5.0]),
MultiObjectiveSolution::new(array![2.0], array![5.0, 1.0, 5.0]),
MultiObjectiveSolution::new(array![3.0], array![5.0, 5.0, 1.0]),
MultiObjectiveSolution::new(array![4.0], array![3.0, 3.0, 3.0]), MultiObjectiveSolution::new(array![5.0], array![6.0, 6.0, 6.0]), ];
let front = pareto_front(&solutions);
assert_eq!(front.len(), 4); }
#[test]
fn test_hypervolume_2d_simple() {
let solutions = vec![MultiObjectiveSolution::new(array![0.0], array![1.0, 1.0])];
let hv = hypervolume(&solutions, &[2.0, 2.0]);
assert!((hv - 1.0).abs() < 1e-10, "Expected 1.0, got {}", hv);
}
#[test]
fn test_hypervolume_2d_two_points() {
let solutions = vec![
MultiObjectiveSolution::new(array![0.0], array![1.0, 3.0]),
MultiObjectiveSolution::new(array![0.0], array![3.0, 1.0]),
];
let hv = hypervolume(&solutions, &[4.0, 4.0]);
assert!((hv - 5.0).abs() < 1e-10, "Expected 5.0, got {}", hv);
}
#[test]
fn test_hypervolume_2d_three_points() {
let solutions = vec![
MultiObjectiveSolution::new(array![0.0], array![1.0, 3.0]),
MultiObjectiveSolution::new(array![0.0], array![2.0, 2.0]),
MultiObjectiveSolution::new(array![0.0], array![3.0, 1.0]),
];
let hv = hypervolume(&solutions, &[4.0, 4.0]);
assert!((hv - 6.0).abs() < 1e-10, "Expected 6.0, got {}", hv);
}
#[test]
fn test_hypervolume_empty() {
let solutions: Vec<MultiObjectiveSolution> = vec![];
let hv = hypervolume(&solutions, &[4.0, 4.0]);
assert_eq!(hv, 0.0);
}
#[test]
fn test_hypervolume_point_at_reference_boundary() {
let solutions = vec![MultiObjectiveSolution::new(array![0.0], array![4.0, 4.0])];
let hv = hypervolume(&solutions, &[4.0, 4.0]);
assert_eq!(hv, 0.0);
}
#[test]
fn test_hypervolume_1d() {
let solutions = vec![
MultiObjectiveSolution::new(array![0.0], array![2.0]),
MultiObjectiveSolution::new(array![0.0], array![3.0]),
];
let hv = hypervolume(&solutions, &[5.0]);
assert!((hv - 3.0).abs() < 1e-10);
}
#[test]
fn test_hypervolume_3d() {
let solutions = vec![MultiObjectiveSolution::new(
array![0.0],
array![1.0, 1.0, 1.0],
)];
let hv = hypervolume(&solutions, &[2.0, 2.0, 2.0]);
assert!((hv - 1.0).abs() < 1e-10, "Expected 1.0, got {}", hv);
}
#[test]
fn test_hypervolume_from_objectives_convenience() {
let objectives = vec![vec![1.0, 3.0], vec![3.0, 1.0]];
let hv = hypervolume_from_objectives(&objectives, &[4.0, 4.0]);
assert!((hv - 5.0).abs() < 1e-10);
}
#[test]
fn test_crowded_comparison_different_ranks() {
let mut a = MultiObjectiveSolution::new(array![1.0], array![1.0, 1.0]);
a.rank = 0;
a.crowding_distance = 1.0;
let mut b = MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]);
b.rank = 1;
b.crowding_distance = 5.0;
assert_eq!(crowded_comparison(&a, &b), Ordering::Less);
}
#[test]
fn test_crowded_comparison_same_rank_different_distance() {
let mut a = MultiObjectiveSolution::new(array![1.0], array![1.0, 1.0]);
a.rank = 0;
a.crowding_distance = 5.0;
let mut b = MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]);
b.rank = 0;
b.crowding_distance = 1.0;
assert_eq!(crowded_comparison(&a, &b), Ordering::Less);
}
#[test]
fn test_crowded_comparison_equal() {
let mut a = MultiObjectiveSolution::new(array![1.0], array![1.0, 1.0]);
a.rank = 0;
a.crowding_distance = 3.0;
let mut b = MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]);
b.rank = 0;
b.crowding_distance = 3.0;
assert_eq!(crowded_comparison(&a, &b), Ordering::Equal);
}
}