use num_traits::Float;
use std::f64::consts::PI;
use super::helpers::{compute_sum, generate_simplex_points};
use super::TestProblem;
#[derive(Debug, Clone)]
pub struct DTLZ1 {
n_objectives: usize,
n_variables: usize,
pub k: usize,
}
impl DTLZ1 {
pub fn new(n_objectives: usize, n_variables: usize) -> Self {
assert!(n_objectives >= 2, "DTLZ1 requires at least 2 objectives");
assert!(
n_variables >= n_objectives,
"Number of variables must be >= number of objectives"
);
let k = n_variables - n_objectives + 1;
Self {
n_objectives,
n_variables,
k,
}
}
pub fn with_default_k(n_objectives: usize) -> Self {
let k = 5;
let n_variables = n_objectives + k - 1;
Self::new(n_objectives, n_variables)
}
fn g_function<T: Float>(&self, xm: &[T]) -> T {
let k_float = T::from(self.k).expect("k to Float");
let sum = xm.iter().fold(T::zero(), |acc, &xi| {
let centered = xi - T::from(0.5).expect("0.5 to Float");
let twenty_pi = T::from(20.0 * PI).expect("20π to Float");
acc + centered * centered - (twenty_pi * centered).cos()
});
T::from(100.0).expect("100 to Float") * (k_float + sum)
}
}
impl<T: Float> TestProblem<T> for DTLZ1 {
fn n_objectives(&self) -> usize {
self.n_objectives
}
fn n_variables(&self) -> usize {
self.n_variables
}
fn evaluate(&self, x: &[T]) -> Vec<T> {
assert_eq!(
x.len(),
self.n_variables,
"Input must have {} variables",
self.n_variables
);
let m = self.n_objectives;
let xm = &x[m - 1..];
let g = self.g_function(xm);
let mut objectives = Vec::with_capacity(m);
let one_plus_g = T::one() + g;
let half = T::from(0.5).expect("0.5 to Float");
for i in 0..m - 1 {
let mut prod = one_plus_g * half;
for j in 0..m - 1 - i {
prod = prod * x[j];
}
if i > 0 {
prod = prod * (T::one() - x[m - 1 - i]);
}
objectives.push(prod);
}
let mut prod = one_plus_g * half;
if m > 1 {
prod = prod * (T::one() - x[0]);
}
objectives.push(prod);
objectives
}
fn generate_pareto_front(&self, n_points: usize) -> Vec<Vec<T>> {
let simplex_points = generate_simplex_points(self.n_objectives, n_points);
let half = T::from(0.5).expect("0.5 to Float");
simplex_points
.into_iter()
.map(|point: Vec<T>| point.into_iter().map(|fi| fi * half).collect())
.collect()
}
}
#[derive(Debug, Clone)]
pub struct DTLZ2 {
n_objectives: usize,
n_variables: usize,
pub k: usize,
}
impl DTLZ2 {
pub fn new(n_objectives: usize, n_variables: usize) -> Self {
assert!(n_objectives >= 2, "DTLZ2 requires at least 2 objectives");
assert!(
n_variables >= n_objectives,
"Number of variables must be >= number of objectives"
);
let k = n_variables - n_objectives + 1;
Self {
n_objectives,
n_variables,
k,
}
}
pub fn with_default_k(n_objectives: usize) -> Self {
let k = 10;
let n_variables = n_objectives + k - 1;
Self::new(n_objectives, n_variables)
}
fn g_function<T: Float>(&self, xm: &[T]) -> T {
xm.iter().fold(T::zero(), |acc, &xi| {
let centered = xi - T::from(0.5).expect("0.5 to Float");
acc + centered * centered
})
}
}
impl<T: Float> TestProblem<T> for DTLZ2 {
fn n_objectives(&self) -> usize {
self.n_objectives
}
fn n_variables(&self) -> usize {
self.n_variables
}
fn evaluate(&self, x: &[T]) -> Vec<T> {
assert_eq!(
x.len(),
self.n_variables,
"Input must have {} variables",
self.n_variables
);
let m = self.n_objectives;
let pi_half = T::from(PI / 2.0).expect("π/2 to Float");
let xm = &x[m - 1..];
let g = self.g_function(xm);
let one_plus_g = T::one() + g;
let mut objectives = Vec::with_capacity(m);
for i in 0..m - 1 {
let mut val = one_plus_g;
for j in 0..m - 1 - i {
val = val * (x[j] * pi_half).cos();
}
if i > 0 {
val = val * (x[m - 1 - i] * pi_half).sin();
}
objectives.push(val);
}
let val = one_plus_g * (x[0] * pi_half).sin();
objectives.push(val);
objectives
}
fn generate_pareto_front(&self, n_points: usize) -> Vec<Vec<T>> {
let simplex_points = generate_simplex_points(self.n_objectives, n_points);
let pi_half = T::from(PI / 2.0).expect("π/2 to Float");
simplex_points
.into_iter()
.map(|point: Vec<T>| {
let mut objectives = Vec::with_capacity(self.n_objectives);
let m = self.n_objectives;
for i in 0..m - 1 {
let mut val = T::one();
for j in 0..m - 1 - i {
let angle: T = point[j] * pi_half;
val = val * angle.cos();
}
if i > 0 {
let angle: T = point[m - 1 - i] * pi_half;
val = val * angle.sin();
}
objectives.push(val);
}
let angle: T = point[0] * pi_half;
let val = angle.sin();
objectives.push(val);
objectives
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct DTLZ3 {
n_objectives: usize,
n_variables: usize,
pub k: usize,
}
impl DTLZ3 {
pub fn new(n_objectives: usize, n_variables: usize) -> Self {
assert!(n_objectives >= 2, "DTLZ3 requires at least 2 objectives");
assert!(
n_variables >= n_objectives,
"Number of variables must be >= number of objectives"
);
let k = n_variables - n_objectives + 1;
Self {
n_objectives,
n_variables,
k,
}
}
pub fn with_default_k(n_objectives: usize) -> Self {
let k = 10;
let n_variables = n_objectives + k - 1;
Self::new(n_objectives, n_variables)
}
fn g_function<T: Float>(&self, xm: &[T]) -> T {
let k_float = T::from(self.k).expect("k to Float");
let sum = xm.iter().fold(T::zero(), |acc, &xi| {
let centered = xi - T::from(0.5).expect("0.5 to Float");
let twenty_pi = T::from(20.0 * PI).expect("20π to Float");
acc + centered * centered - (twenty_pi * centered).cos()
});
T::from(100.0).expect("100 to Float") * (k_float + sum)
}
}
impl<T: Float> TestProblem<T> for DTLZ3 {
fn n_objectives(&self) -> usize {
self.n_objectives
}
fn n_variables(&self) -> usize {
self.n_variables
}
fn evaluate(&self, x: &[T]) -> Vec<T> {
assert_eq!(
x.len(),
self.n_variables,
"Input must have {} variables",
self.n_variables
);
let m = self.n_objectives;
let pi_half = T::from(PI / 2.0).expect("π/2 to Float");
let xm = &x[m - 1..];
let g = self.g_function(xm);
let one_plus_g = T::one() + g;
let mut objectives = Vec::with_capacity(m);
for i in 0..m - 1 {
let mut val = one_plus_g;
for j in 0..m - 1 - i {
val = val * (x[j] * pi_half).cos();
}
if i > 0 {
val = val * (x[m - 1 - i] * pi_half).sin();
}
objectives.push(val);
}
let val = one_plus_g * (x[0] * pi_half).sin();
objectives.push(val);
objectives
}
fn generate_pareto_front(&self, n_points: usize) -> Vec<Vec<T>> {
let simplex_points = generate_simplex_points(self.n_objectives, n_points);
let pi_half = T::from(PI / 2.0).expect("π/2 to Float");
simplex_points
.into_iter()
.map(|point: Vec<T>| {
let mut objectives = Vec::with_capacity(self.n_objectives);
let m = self.n_objectives;
for i in 0..m - 1 {
let mut val = T::one();
for j in 0..m - 1 - i {
let angle: T = point[j] * pi_half;
val = val * angle.cos();
}
if i > 0 {
let angle: T = point[m - 1 - i] * pi_half;
val = val * angle.sin();
}
objectives.push(val);
}
let angle: T = point[0] * pi_half;
let val = angle.sin();
objectives.push(val);
objectives
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct DTLZ7 {
n_objectives: usize,
n_variables: usize,
pub k: usize,
}
impl DTLZ7 {
pub fn new(n_objectives: usize, n_variables: usize) -> Self {
assert!(n_objectives >= 2, "DTLZ7 requires at least 2 objectives");
assert!(
n_variables >= n_objectives,
"Number of variables must be >= number of objectives"
);
let k = n_variables - n_objectives + 1;
Self {
n_objectives,
n_variables,
k,
}
}
pub fn with_default_k(n_objectives: usize) -> Self {
let k = 20;
let n_variables = n_objectives + k - 1;
Self::new(n_objectives, n_variables)
}
fn g_function<T: Float>(&self, xm: &[T]) -> T {
let k_float = T::from(self.k).expect("k to Float");
let nine = T::from(9.0).expect("9 to Float");
let sum = compute_sum(xm);
T::one() + (nine / k_float) * sum
}
fn h_function<T: Float>(&self, f: &[T], g: T) -> T {
let m = T::from(self.n_objectives).expect("M to Float");
let one_plus_g = T::one() + g;
let three_pi = T::from(3.0 * PI).expect("3π to Float");
let sum = f.iter().fold(T::zero(), |acc, &fi| {
let term = fi / one_plus_g;
acc + term * (T::one() + (three_pi * fi).sin())
});
m - sum
}
}
impl<T: Float> TestProblem<T> for DTLZ7 {
fn n_objectives(&self) -> usize {
self.n_objectives
}
fn n_variables(&self) -> usize {
self.n_variables
}
fn evaluate(&self, x: &[T]) -> Vec<T> {
assert_eq!(
x.len(),
self.n_variables,
"Input must have {} variables",
self.n_variables
);
let m = self.n_objectives;
let mut objectives: Vec<T> = x[..m - 1].to_vec();
let xm = &x[m - 1..];
let g = self.g_function(xm);
let h = self.h_function(&objectives, g);
let one_plus_g = T::one() + g;
objectives.push(one_plus_g * h);
objectives
}
fn generate_pareto_front(&self, n_points: usize) -> Vec<Vec<T>> {
let m = self.n_objectives;
let three_pi = T::from(3.0 * PI).expect("3π to Float");
let m_float = T::from(m).expect("M to Float");
let mut points = Vec::new();
let divisions = (n_points as f64).powf(1.0 / (m - 1) as f64).ceil() as usize;
if m == 2 {
for i in 0..n_points {
let f1 =
T::from(i).expect("i to Float") / T::from(n_points - 1).expect("n-1 to Float");
let h = m_float - f1 * (T::one() + (three_pi * f1).sin());
points.push(vec![f1, h]);
}
} else {
generate_dtlz7_recursive::<T>(m, divisions, Vec::new(), &mut points, three_pi, m_float);
}
points
}
}
fn generate_dtlz7_recursive<T: Float>(
m: usize,
divisions: usize,
current: Vec<T>,
points: &mut Vec<Vec<T>>,
three_pi: T,
m_float: T,
) {
if current.len() == m - 1 {
let sum = current.iter().fold(T::zero(), |acc, &fi| {
acc + fi * (T::one() + (three_pi * fi).sin())
});
let h = m_float - sum;
let mut point = current;
point.push(h);
points.push(point);
return;
}
for i in 0..=divisions {
let val = T::from(i).expect("i to Float") / T::from(divisions).expect("divisions to Float");
let mut next = current.clone();
next.push(val);
generate_dtlz7_recursive(m, divisions, next, points, three_pi, m_float);
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_dtlz1_construction() {
let problem = DTLZ1::new(3, 7);
assert_eq!(TestProblem::<f64>::n_objectives(&problem), 3);
assert_eq!(TestProblem::<f64>::n_variables(&problem), 7);
assert_eq!(problem.k, 5);
}
#[test]
fn test_dtlz1_with_default_k() {
let problem = DTLZ1::with_default_k(3);
assert_eq!(TestProblem::<f64>::n_objectives(&problem), 3);
assert_eq!(TestProblem::<f64>::n_variables(&problem), 7); assert_eq!(problem.k, 5);
}
#[test]
fn test_dtlz1_evaluate_dimensions() {
let problem = DTLZ1::new(3, 7);
let x = vec![0.5; 7];
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
assert_eq!(objectives.len(), 3);
}
#[test]
fn test_dtlz1_optimal_point() {
let problem = DTLZ1::new(3, 7);
let mut x = vec![0.0; 7];
x[0] = 0.5;
x[1] = 0.5;
for i in 2..7 {
x[i] = 0.5;
}
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
let sum: f64 = objectives.iter().sum();
assert_relative_eq!(sum, 0.5, epsilon = 1e-6);
}
#[test]
fn test_dtlz1_scalability_objectives() {
for m in [2, 3, 5, 10] {
let problem = DTLZ1::with_default_k(m);
let x = vec![0.5; TestProblem::<f64>::n_variables(&problem)];
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
assert_eq!(objectives.len(), m);
}
}
#[test]
fn test_dtlz1_bounds() {
let problem = DTLZ1::new(3, 7);
let bounds = TestProblem::<f64>::bounds(&problem);
assert_eq!(bounds.len(), 7);
for (lb, ub) in bounds {
assert_relative_eq!(lb, 0.0, epsilon = 1e-10);
assert_relative_eq!(ub, 1.0, epsilon = 1e-10);
}
}
#[test]
fn test_dtlz1_pareto_front_generation() {
let problem = DTLZ1::new(3, 7);
let pareto_front = TestProblem::<f64>::generate_pareto_front(&problem, 50);
assert!(!pareto_front.is_empty());
for point in &pareto_front {
assert_eq!(point.len(), 3);
let sum: f64 = point.iter().sum();
assert_relative_eq!(sum, 0.5, epsilon = 1e-6);
for &fi in point {
assert!(fi >= 0.0, "Objective should be non-negative");
}
}
}
#[test]
fn test_dtlz2_construction() {
let problem = DTLZ2::new(3, 12);
assert_eq!(TestProblem::<f64>::n_objectives(&problem), 3);
assert_eq!(TestProblem::<f64>::n_variables(&problem), 12);
assert_eq!(problem.k, 10);
}
#[test]
fn test_dtlz2_with_default_k() {
let problem = DTLZ2::with_default_k(3);
assert_eq!(TestProblem::<f64>::n_objectives(&problem), 3);
assert_eq!(TestProblem::<f64>::n_variables(&problem), 12); assert_eq!(problem.k, 10);
}
#[test]
fn test_dtlz2_evaluate_dimensions() {
let problem = DTLZ2::new(3, 12);
let x = vec![0.5; 12];
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
assert_eq!(objectives.len(), 3);
}
#[test]
fn test_dtlz2_optimal_point() {
let problem = DTLZ2::new(3, 12);
let mut x = vec![0.0; 12];
x[0] = 0.5; x[1] = 0.5; for i in 2..12 {
x[i] = 0.5;
}
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
let sum_sq: f64 = objectives.iter().map(|&f| f * f).sum();
assert_relative_eq!(sum_sq, 1.0, epsilon = 1e-6);
}
#[test]
fn test_dtlz2_scalability_objectives() {
for m in [2, 3, 5, 10] {
let problem = DTLZ2::with_default_k(m);
let x = vec![0.5; TestProblem::<f64>::n_variables(&problem)];
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
assert_eq!(objectives.len(), m);
}
}
#[test]
fn test_dtlz2_pareto_front_generation() {
let problem = DTLZ2::new(3, 12);
let pareto_front = TestProblem::<f64>::generate_pareto_front(&problem, 50);
assert!(!pareto_front.is_empty());
for point in &pareto_front {
assert_eq!(point.len(), 3);
let sum_sq: f64 = point.iter().map(|&f| f * f).sum();
assert_relative_eq!(sum_sq, 1.0, epsilon = 1e-6);
for &fi in point {
assert!(fi >= 0.0, "Objective should be non-negative");
}
}
}
#[test]
fn test_dtlz3_construction() {
let problem = DTLZ3::new(3, 12);
assert_eq!(TestProblem::<f64>::n_objectives(&problem), 3);
assert_eq!(TestProblem::<f64>::n_variables(&problem), 12);
assert_eq!(problem.k, 10);
}
#[test]
fn test_dtlz3_multi_modal() {
let problem = DTLZ3::new(3, 12);
let mut x = vec![0.5; 12];
let obj1 = TestProblem::<f64>::evaluate(&problem, &x);
for i in 2..12 {
x[i] = 0.6;
}
let obj2 = TestProblem::<f64>::evaluate(&problem, &x);
let sum1: f64 = obj1.iter().map(|&f| f * f).sum();
let sum2: f64 = obj2.iter().map(|&f| f * f).sum();
assert!(
sum2 > sum1,
"Moving from optimum should increase objectives"
);
}
#[test]
fn test_dtlz3_scalability_objectives() {
for m in [2, 3, 5] {
let problem = DTLZ3::with_default_k(m);
let x = vec![0.5; TestProblem::<f64>::n_variables(&problem)];
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
assert_eq!(objectives.len(), m);
}
}
#[test]
fn test_dtlz3_pareto_front_same_as_dtlz2() {
let problem = DTLZ3::new(3, 12);
let pareto_front = TestProblem::<f64>::generate_pareto_front(&problem, 50);
assert!(!pareto_front.is_empty());
for point in &pareto_front {
assert_eq!(point.len(), 3);
let sum_sq: f64 = point.iter().map(|&f| f * f).sum();
assert_relative_eq!(sum_sq, 1.0, epsilon = 1e-6);
}
}
#[test]
fn test_dtlz7_construction() {
let problem = DTLZ7::new(3, 22);
assert_eq!(TestProblem::<f64>::n_objectives(&problem), 3);
assert_eq!(TestProblem::<f64>::n_variables(&problem), 22);
assert_eq!(problem.k, 20);
}
#[test]
fn test_dtlz7_with_default_k() {
let problem = DTLZ7::with_default_k(3);
assert_eq!(TestProblem::<f64>::n_objectives(&problem), 3);
assert_eq!(TestProblem::<f64>::n_variables(&problem), 22); assert_eq!(problem.k, 20);
}
#[test]
fn test_dtlz7_evaluate_dimensions() {
let problem = DTLZ7::new(3, 22);
let x = vec![0.5; 22];
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
assert_eq!(objectives.len(), 3);
}
#[test]
fn test_dtlz7_first_objectives_equal_variables() {
let problem = DTLZ7::new(3, 22);
let mut x = vec![0.0; 22];
x[0] = 0.3;
x[1] = 0.7;
for i in 2..22 {
x[i] = 0.0;
}
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
assert_relative_eq!(objectives[0], 0.3, epsilon = 1e-10);
assert_relative_eq!(objectives[1], 0.7, epsilon = 1e-10);
}
#[test]
fn test_dtlz7_scalability_objectives() {
for m in [2, 3, 5] {
let problem = DTLZ7::with_default_k(m);
let x = vec![0.5; TestProblem::<f64>::n_variables(&problem)];
let objectives = TestProblem::<f64>::evaluate(&problem, &x);
assert_eq!(objectives.len(), m);
}
}
#[test]
fn test_dtlz7_pareto_front_generation() {
let problem = DTLZ7::new(3, 22);
let pareto_front = TestProblem::<f64>::generate_pareto_front(&problem, 50);
assert!(!pareto_front.is_empty());
for point in &pareto_front {
assert_eq!(point.len(), 3);
for i in 0..2 {
assert!(point[i] >= 0.0 && point[i] <= 1.0);
}
}
}
#[test]
fn test_dtlz7_disconnected_regions() {
let problem = DTLZ7::new(2, 21);
let pareto_front = TestProblem::<f64>::generate_pareto_front(&problem, 100);
let mut f2_values: Vec<f64> = pareto_front.iter().map(|p| p[1]).collect();
f2_values.sort_by(|a, b| a.partial_cmp(b).expect("No NaN values"));
let min_f2 = f2_values.first().copied().expect("Non-empty");
let max_f2 = f2_values.last().copied().expect("Non-empty");
assert!(max_f2 > min_f2, "Should have variation in f2");
}
#[test]
fn test_all_problems_bounds_in_unit_hypercube() {
let problems: Vec<Box<dyn TestProblem<f64>>> = vec![
Box::new(DTLZ1::new(3, 7)),
Box::new(DTLZ2::new(3, 12)),
Box::new(DTLZ3::new(3, 12)),
Box::new(DTLZ7::new(3, 22)),
];
for problem in problems {
let bounds = TestProblem::<f64>::bounds(&*problem);
for (lb, ub) in bounds {
assert_relative_eq!(lb, 0.0, epsilon = 1e-10);
assert_relative_eq!(ub, 1.0, epsilon = 1e-10);
}
}
}
#[test]
fn test_variable_scalability() {
let test_cases = vec![
(2, 6), (3, 7), (5, 14), (10, 19), ];
for (m, n) in test_cases {
let p1 = DTLZ1::new(m, n);
let p2 = DTLZ2::new(m, n);
let p3 = DTLZ3::new(m, n);
let p7 = DTLZ7::new(m, n);
let x = vec![0.5; n];
assert_eq!(TestProblem::<f64>::evaluate(&p1, &x).len(), m);
assert_eq!(TestProblem::<f64>::evaluate(&p2, &x).len(), m);
assert_eq!(TestProblem::<f64>::evaluate(&p3, &x).len(), m);
assert_eq!(TestProblem::<f64>::evaluate(&p7, &x).len(), m);
}
}
}