use crate::error::{NumRs2Error, Result};
use num_traits::Float;
use std::cmp::Ordering;
pub(crate) fn euclidean_distance<T: Float + std::iter::Sum>(a: &[T], b: &[T]) -> T {
a.iter()
.zip(b.iter())
.map(|(ai, bi)| (*ai - *bi) * (*ai - *bi))
.sum::<T>()
.sqrt()
}
fn min_distance_to_front<T: Float + std::iter::Sum>(point: &[T], front: &[Vec<T>]) -> T {
front
.iter()
.map(|front_point| euclidean_distance(point, front_point))
.fold(
T::infinity(),
|min_dist, dist| {
if dist < min_dist {
dist
} else {
min_dist
}
},
)
}
pub fn calculate_igd<T: Float + std::fmt::Display + std::iter::Sum>(
obtained_front: &[Vec<T>],
reference_front: &[Vec<T>],
) -> Result<T> {
if obtained_front.is_empty() {
return Err(NumRs2Error::ValueError(
"Obtained front cannot be empty".to_string(),
));
}
if reference_front.is_empty() {
return Err(NumRs2Error::ValueError(
"Reference front cannot be empty".to_string(),
));
}
let n_obj = obtained_front[0].len();
for point in obtained_front {
if point.len() != n_obj {
return Err(NumRs2Error::ValueError(
"All obtained points must have the same dimension".to_string(),
));
}
}
for point in reference_front {
if point.len() != n_obj {
return Err(NumRs2Error::ValueError(
"All reference points must have the same dimension".to_string(),
));
}
}
let sum_squared_distances: T = reference_front
.iter()
.map(|ref_point| {
let min_dist = min_distance_to_front(ref_point, obtained_front);
min_dist * min_dist
})
.sum();
let n_ref = T::from(reference_front.len()).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert reference front size to Float".to_string())
})?;
Ok((sum_squared_distances / n_ref).sqrt())
}
pub fn calculate_gd<T: Float + std::fmt::Display + std::iter::Sum>(
obtained_front: &[Vec<T>],
reference_front: &[Vec<T>],
p: Option<T>,
) -> Result<T> {
if obtained_front.is_empty() {
return Err(NumRs2Error::ValueError(
"Obtained front cannot be empty".to_string(),
));
}
if reference_front.is_empty() {
return Err(NumRs2Error::ValueError(
"Reference front cannot be empty".to_string(),
));
}
let p_val = p.unwrap_or_else(|| T::from(2.0).expect("Default p=2.0 should convert to Float"));
if p_val <= T::zero() {
return Err(NumRs2Error::ValueError(
"Power parameter p must be positive".to_string(),
));
}
let n_obj = obtained_front[0].len();
for point in obtained_front {
if point.len() != n_obj {
return Err(NumRs2Error::ValueError(
"All obtained points must have the same dimension".to_string(),
));
}
}
for point in reference_front {
if point.len() != n_obj {
return Err(NumRs2Error::ValueError(
"All reference points must have the same dimension".to_string(),
));
}
}
let sum_powered_distances: T = obtained_front
.iter()
.map(|obtained_point| {
let min_dist = min_distance_to_front(obtained_point, reference_front);
min_dist.powf(p_val)
})
.sum();
let n_obtained = T::from(obtained_front.len()).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert obtained front size to Float".to_string())
})?;
Ok((sum_powered_distances / n_obtained).powf(T::one() / p_val))
}
pub fn calculate_spacing<T: Float + std::fmt::Display + std::iter::Sum>(
front: &[Vec<T>],
) -> Result<T> {
if front.len() < 2 {
return Err(NumRs2Error::ValueError(
"Spacing requires at least 2 points".to_string(),
));
}
let n = front.len();
let n_obj = front[0].len();
for point in front {
if point.len() != n_obj {
return Err(NumRs2Error::ValueError(
"All points must have the same dimension".to_string(),
));
}
}
let mut min_distances = Vec::with_capacity(n);
for i in 0..n {
let mut min_dist = T::infinity();
for j in 0..n {
if i != j {
let dist = euclidean_distance(&front[i], &front[j]);
if dist < min_dist {
min_dist = dist;
}
}
}
min_distances.push(min_dist);
}
let mean_dist = min_distances.iter().fold(T::zero(), |acc, &d| acc + d)
/ T::from(n).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert n to Float".to_string())
})?;
let variance = min_distances
.iter()
.map(|&d| (d - mean_dist) * (d - mean_dist))
.sum::<T>()
/ T::from(n - 1).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert n-1 to Float".to_string())
})?;
Ok(variance.sqrt())
}
fn find_extreme_points<T: Float + Clone>(front: &[Vec<T>]) -> Vec<Vec<T>> {
if front.is_empty() {
return Vec::new();
}
let n_obj = front[0].len();
let mut extremes = Vec::with_capacity(n_obj);
for obj_idx in 0..n_obj {
let mut min_point = &front[0];
let mut min_val = front[0][obj_idx];
for point in front {
if point[obj_idx] < min_val {
min_val = point[obj_idx];
min_point = point;
}
}
extremes.push(min_point.clone());
}
extremes
}
#[cfg(test)]
pub fn find_extreme_points_pub<T: Float + Clone>(front: &[Vec<T>]) -> Vec<Vec<T>> {
find_extreme_points(front)
}
#[cfg(test)]
pub fn min_distance_to_front_pub<T: Float + std::iter::Sum>(point: &[T], front: &[Vec<T>]) -> T {
min_distance_to_front(point, front)
}
pub fn calculate_spread<T: Float + std::fmt::Display + std::iter::Sum>(
front: &[Vec<T>],
extreme_points: Option<&[Vec<T>]>,
) -> Result<T> {
if front.len() < 2 {
return Err(NumRs2Error::ValueError(
"Spread requires at least 2 points".to_string(),
));
}
let n = front.len();
let n_obj = front[0].len();
for point in front {
if point.len() != n_obj {
return Err(NumRs2Error::ValueError(
"All points must have the same dimension".to_string(),
));
}
}
let extremes = if let Some(ext) = extreme_points {
ext.to_vec()
} else {
find_extreme_points(front)
};
let mut sorted_front = front.to_vec();
sorted_front.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap_or(Ordering::Equal));
let mut consecutive_distances = Vec::with_capacity(n - 1);
for i in 0..(n - 1) {
let dist = euclidean_distance(&sorted_front[i], &sorted_front[i + 1]);
consecutive_distances.push(dist);
}
let d_mean = consecutive_distances.iter().copied().sum::<T>()
/ T::from(consecutive_distances.len()).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert length to Float".to_string())
})?;
let d_f = euclidean_distance(&sorted_front[0], &extremes[0]);
let d_l = euclidean_distance(&sorted_front[n - 1], &extremes[n_obj - 1]);
let sum_deviations: T = consecutive_distances
.iter()
.map(|&d| (d - d_mean).abs())
.sum();
let numerator = d_f + d_l + sum_deviations;
let denominator = d_f
+ d_l
+ d_mean
* T::from(n - 1).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert n-1 to Float".to_string())
})?;
if denominator == T::zero() {
return Ok(T::zero());
}
Ok(numerator / denominator)
}