mod reference_points;
#[cfg(test)]
mod tests;
use reference_points::*;
use crate::error::{NumRs2Error, Result};
use lazy_static::lazy_static;
use num_traits::Float;
use scirs2_core::random::{thread_rng, Distribution, Rng, RngExt, Uniform};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::sync::Mutex;
type ReferencePointCache<T> = Mutex<HashMap<(usize, usize, u8), Vec<ReferencePoint<T>>>>;
lazy_static! {
static ref REFERENCE_POINT_CACHE_F64: ReferencePointCache<f64> = Mutex::new(HashMap::new());
static ref REFERENCE_POINT_CACHE_F32: ReferencePointCache<f32> = Mutex::new(HashMap::new());
}
#[derive(Debug, Clone)]
pub struct NSGA3Config<T: Float> {
pub pop_size: usize,
pub max_generations: usize,
pub crossover_rate: T,
pub mutation_rate: T,
pub eta_c: T,
pub eta_m: T,
pub n_divisions: usize,
}
impl<T: Float> Default for NSGA3Config<T> {
fn default() -> Self {
Self {
pop_size: 92, max_generations: 100,
crossover_rate: T::from(0.9).expect("0.9 should convert to Float"),
mutation_rate: T::from(0.1).expect("0.1 should convert to Float"),
eta_c: T::from(30.0).expect("30.0 should convert to Float"),
eta_m: T::from(20.0).expect("20.0 should convert to Float"),
n_divisions: 12,
}
}
}
#[derive(Clone, Debug)]
pub struct Individual<T: Float> {
pub variables: Vec<T>,
pub objectives: Vec<T>,
pub rank: usize,
pub reference_point_index: Option<usize>,
pub perpendicular_distance: T,
}
#[derive(Clone, Debug)]
pub struct ReferencePoint<T: Float> {
pub position: Vec<T>,
pub niche_count: usize,
}
#[derive(Debug)]
pub struct NSGA3Result<T: Float> {
pub pareto_front: Vec<Individual<T>>,
pub population: Vec<Individual<T>>,
pub generations: usize,
pub reference_points: Vec<ReferencePoint<T>>,
}
pub fn nsga3<T, F>(
objectives: &[F],
bounds: &[(T, T)],
config: Option<NSGA3Config<T>>,
) -> Result<NSGA3Result<T>>
where
T: Float + std::fmt::Display + std::iter::Sum,
F: Fn(&[T]) -> T,
{
let config = config.unwrap_or_default();
let n_obj = objectives.len();
let n_var = bounds.len();
if n_obj < 2 {
return Err(NumRs2Error::ValueError(
"NSGA-III requires at least 2 objectives".to_string(),
));
}
if n_var == 0 {
return Err(NumRs2Error::ValueError(
"Bounds must have at least one dimension".to_string(),
));
}
if config.pop_size < 4 {
return Err(NumRs2Error::ValueError(
"Population size must be at least 4".to_string(),
));
}
let reference_points = generate_reference_points_adaptive(n_obj, config.n_divisions)?;
let mut rng = thread_rng();
let mut population = initialize_population(objectives, bounds, config.pop_size, &mut rng)?;
for _generation in 0..config.max_generations {
let mut offspring = Vec::with_capacity(config.pop_size);
while offspring.len() < config.pop_size {
let parent1 = tournament_selection(&population, &mut rng)?;
let parent2 = tournament_selection(&population, &mut rng)?;
let (mut child1, mut child2) = if T::from(rng.random::<f64>()).ok_or_else(|| {
NumRs2Error::ConversionError("Random value conversion failed".to_string())
})? < config.crossover_rate
{
sbx_crossover(
&parent1.variables,
&parent2.variables,
bounds,
config.eta_c,
&mut rng,
)?
} else {
(parent1.variables.clone(), parent2.variables.clone())
};
if T::from(rng.random::<f64>()).ok_or_else(|| {
NumRs2Error::ConversionError("Random value conversion failed".to_string())
})? < config.mutation_rate
{
polynomial_mutation(&mut child1, bounds, config.eta_m, &mut rng)?;
}
if T::from(rng.random::<f64>()).ok_or_else(|| {
NumRs2Error::ConversionError("Random value conversion failed".to_string())
})? < config.mutation_rate
{
polynomial_mutation(&mut child2, bounds, config.eta_m, &mut rng)?;
}
offspring.push(create_individual(&child1, objectives));
if offspring.len() < config.pop_size {
offspring.push(create_individual(&child2, objectives));
}
}
population.extend(offspring);
population =
environmental_selection(population, &reference_points, config.pop_size, n_obj)?;
}
let pareto_front: Vec<Individual<T>> = population
.iter()
.filter(|ind| ind.rank == 0)
.cloned()
.collect();
Ok(NSGA3Result {
pareto_front,
population,
generations: config.max_generations,
reference_points,
})
}
fn vector_norm<T: Float + std::iter::Sum>(v: &[T]) -> Result<T> {
if v.is_empty() {
return Err(NumRs2Error::ValueError(
"Cannot compute norm of empty vector".to_string(),
));
}
let sum_squares: T = v.iter().map(|&x| x * x).sum();
Ok(sum_squares.sqrt())
}
fn initialize_population<T, F>(
objectives: &[F],
bounds: &[(T, T)],
pop_size: usize,
rng: &mut impl Rng,
) -> Result<Vec<Individual<T>>>
where
T: Float + std::fmt::Display,
F: Fn(&[T]) -> T,
{
let n_var = bounds.len();
let mut population = Vec::with_capacity(pop_size);
for _ in 0..pop_size {
let mut variables = Vec::with_capacity(n_var);
for &(lower, upper) in bounds {
let lower_f64 = lower.to_f64().ok_or_else(|| {
NumRs2Error::ConversionError("Bound conversion failed".to_string())
})?;
let upper_f64 = upper.to_f64().ok_or_else(|| {
NumRs2Error::ConversionError("Bound conversion failed".to_string())
})?;
let uniform = Uniform::new(lower_f64, upper_f64).map_err(|e| {
NumRs2Error::ComputationError(format!("Uniform creation failed: {}", e))
})?;
let value = T::from(uniform.sample(rng)).ok_or_else(|| {
NumRs2Error::ConversionError("Sample conversion failed".to_string())
})?;
variables.push(value);
}
population.push(create_individual(&variables, objectives));
}
Ok(population)
}
fn create_individual<T, F>(variables: &[T], objectives: &[F]) -> Individual<T>
where
T: Float,
F: Fn(&[T]) -> T,
{
let obj_values: Vec<T> = objectives.iter().map(|f| f(variables)).collect();
Individual {
variables: variables.to_vec(),
objectives: obj_values,
rank: 0,
reference_point_index: None,
perpendicular_distance: T::infinity(),
}
}
fn environmental_selection<T: Float + std::iter::Sum>(
mut population: Vec<Individual<T>>,
reference_points: &[ReferencePoint<T>],
target_size: usize,
n_obj: usize,
) -> Result<Vec<Individual<T>>> {
let mut ref_points_copy: Vec<ReferencePoint<T>> = reference_points.to_vec();
fast_non_dominated_sort(&mut population);
let mut selected = Vec::new();
let mut front_idx = 0;
loop {
let current_front: Vec<Individual<T>> = population
.iter()
.filter(|ind| ind.rank == front_idx)
.cloned()
.collect();
if current_front.is_empty() {
break;
}
if selected.len() + current_front.len() <= target_size {
let mut front_to_add = current_front;
normalize_objectives(&mut front_to_add, n_obj)?;
associate_with_reference_points(&mut front_to_add, &ref_points_copy)?;
update_niche_counts(&front_to_add, &mut ref_points_copy);
selected.extend(front_to_add);
front_idx += 1;
} else {
let k = target_size - selected.len();
let mut last_front = current_front;
normalize_objectives(&mut last_front, n_obj)?;
associate_with_reference_points(&mut last_front, &ref_points_copy)?;
let selected_from_last = niche_preservation_selection(last_front, &ref_points_copy, k)?;
selected.extend(selected_from_last);
break;
}
}
Ok(selected)
}
fn fast_non_dominated_sort<T: Float>(population: &mut [Individual<T>]) {
let n = population.len();
let mut fronts: Vec<Vec<usize>> = Vec::new();
let mut domination_count = vec![0; n];
let mut dominated_solutions: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut current_front = Vec::new();
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
if dominates(&population[i].objectives, &population[j].objectives) {
dominated_solutions[i].push(j);
} else if dominates(&population[j].objectives, &population[i].objectives) {
domination_count[i] += 1;
}
}
if domination_count[i] == 0 {
population[i].rank = 0;
current_front.push(i);
}
}
fronts.push(current_front.clone());
let mut rank = 0;
while !fronts[rank].is_empty() {
let mut next_front = Vec::new();
for &i in &fronts[rank] {
for &j in &dominated_solutions[i] {
domination_count[j] -= 1;
if domination_count[j] == 0 {
population[j].rank = rank + 1;
next_front.push(j);
}
}
}
rank += 1;
fronts.push(next_front.clone());
}
}
fn dominates<T: Float>(a: &[T], b: &[T]) -> bool {
let mut better_in_any = false;
for (ai, bi) in a.iter().zip(b.iter()) {
if ai > bi {
return false; }
if ai < bi {
better_in_any = true;
}
}
better_in_any
}
fn normalize_objectives<T: Float>(population: &mut [Individual<T>], n_obj: usize) -> Result<()> {
if population.is_empty() {
return Ok(());
}
let ideal = compute_ideal_point(population, n_obj);
let nadir = compute_nadir_point(population, n_obj);
for ind in population.iter_mut() {
for i in 0..n_obj {
let range = nadir[i] - ideal[i];
if range > T::epsilon() {
let normalized = (ind.objectives[i] - ideal[i]) / range;
ind.objectives[i] = normalized;
} else {
ind.objectives[i] = T::zero();
}
}
}
Ok(())
}
fn compute_ideal_point<T: Float>(population: &[Individual<T>], n_obj: usize) -> Vec<T> {
let mut ideal = vec![T::infinity(); n_obj];
for ind in population.iter() {
for (i, &obj) in ind.objectives.iter().enumerate() {
if obj < ideal[i] {
ideal[i] = obj;
}
}
}
ideal
}
fn compute_nadir_point<T: Float>(population: &[Individual<T>], n_obj: usize) -> Vec<T> {
let mut nadir = vec![T::neg_infinity(); n_obj];
for ind in population.iter() {
for (i, &obj) in ind.objectives.iter().enumerate() {
if obj > nadir[i] {
nadir[i] = obj;
}
}
}
nadir
}
fn associate_with_reference_points<T: Float + std::iter::Sum>(
population: &mut [Individual<T>],
reference_points: &[ReferencePoint<T>],
) -> Result<()> {
for ind in population.iter_mut() {
let (closest_idx, perp_dist) =
find_closest_reference_point(&ind.objectives, reference_points)?;
ind.reference_point_index = Some(closest_idx);
ind.perpendicular_distance = perp_dist;
}
Ok(())
}
fn find_closest_reference_point<T: Float + std::iter::Sum>(
objectives: &[T],
reference_points: &[ReferencePoint<T>],
) -> Result<(usize, T)> {
if reference_points.is_empty() {
return Err(NumRs2Error::ValueError(
"Reference points cannot be empty".to_string(),
));
}
let mut min_distance = T::infinity();
let mut closest_idx = 0;
for (idx, ref_point) in reference_points.iter().enumerate() {
let dist = perpendicular_distance(objectives, &ref_point.position)?;
if dist < min_distance {
min_distance = dist;
closest_idx = idx;
}
}
Ok((closest_idx, min_distance))
}
fn perpendicular_distance<T: Float + std::iter::Sum>(
point: &[T],
ref_direction: &[T],
) -> Result<T> {
if point.len() != ref_direction.len() {
return Err(NumRs2Error::DimensionMismatch(
"Point and reference direction must have same dimension".to_string(),
));
}
if point.is_empty() {
return Err(NumRs2Error::ValueError(
"Cannot calculate distance for empty vectors".to_string(),
));
}
let dot_product: T = point
.iter()
.zip(ref_direction.iter())
.map(|(&p, &r)| p * r)
.sum();
let projection: Vec<T> = ref_direction.iter().map(|&r| dot_product * r).collect();
let perpendicular: Vec<T> = point
.iter()
.zip(projection.iter())
.map(|(&p, &proj)| p - proj)
.collect();
let sum_squares: T = perpendicular.iter().map(|&x| x * x).sum();
Ok(sum_squares.sqrt())
}
fn scalar_projection<T: Float + std::iter::Sum>(point: &[T], ref_direction: &[T]) -> Result<T> {
if point.len() != ref_direction.len() {
return Err(NumRs2Error::DimensionMismatch(
"Dimension mismatch in scalar projection".to_string(),
));
}
let projection_coeff: T = point
.iter()
.zip(ref_direction.iter())
.map(|(&p, &r)| p * r)
.sum();
Ok(projection_coeff)
}
fn euclidean_distance<T: Float + std::iter::Sum>(a: &[T], b: &[T]) -> Result<T> {
if a.len() != b.len() {
return Err(NumRs2Error::DimensionMismatch(
"Vectors must have same length".to_string(),
));
}
let sum_squares: T = a
.iter()
.zip(b.iter())
.map(|(&ai, &bi)| {
let diff = ai - bi;
diff * diff
})
.sum();
Ok(sum_squares.sqrt())
}
fn niche_preservation_selection<T: Float>(
candidates: Vec<Individual<T>>,
reference_points: &[ReferencePoint<T>],
k: usize,
) -> Result<Vec<Individual<T>>> {
if k == 0 {
return Ok(Vec::new());
}
if candidates.is_empty() {
return Err(NumRs2Error::ValueError(
"No candidates available for selection".to_string(),
));
}
let mut ref_points_copy: Vec<ReferencePoint<T>> = reference_points.to_vec();
let mut selected_indices = Vec::new();
let mut remaining_indices: Vec<usize> = (0..candidates.len()).collect();
for _ in 0..k.min(candidates.len()) {
if remaining_indices.is_empty() {
break;
}
let min_niche_count = ref_points_copy
.iter()
.map(|rp| rp.niche_count)
.min()
.ok_or_else(|| {
NumRs2Error::ComputationError("No reference points available".to_string())
})?;
let min_niche_refs: Vec<usize> = ref_points_copy
.iter()
.enumerate()
.filter(|(_, rp)| rp.niche_count == min_niche_count)
.map(|(idx, _)| idx)
.collect();
let mut candidates_in_min_niches: Vec<(usize, T)> = Vec::new();
for &candidate_idx in &remaining_indices {
let candidate = &candidates[candidate_idx];
if let Some(ref_idx) = candidate.reference_point_index {
if min_niche_refs.contains(&ref_idx) {
candidates_in_min_niches
.push((candidate_idx, candidate.perpendicular_distance));
}
}
}
if candidates_in_min_niches.is_empty() {
let best_idx = *remaining_indices
.iter()
.min_by(|&&a, &&b| {
candidates[a]
.perpendicular_distance
.partial_cmp(&candidates[b].perpendicular_distance)
.unwrap_or(Ordering::Equal)
})
.ok_or_else(|| {
NumRs2Error::ComputationError("No candidates available".to_string())
})?;
selected_indices.push(best_idx);
remaining_indices.retain(|&idx| idx != best_idx);
if let Some(ref_idx) = candidates[best_idx].reference_point_index {
ref_points_copy[ref_idx].niche_count += 1;
}
} else {
candidates_in_min_niches
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
let (best_idx, _) = candidates_in_min_niches[0];
selected_indices.push(best_idx);
remaining_indices.retain(|&idx| idx != best_idx);
if let Some(ref_idx) = candidates[best_idx].reference_point_index {
ref_points_copy[ref_idx].niche_count += 1;
}
}
}
let selected: Vec<Individual<T>> = selected_indices
.into_iter()
.map(|idx| candidates[idx].clone())
.collect();
Ok(selected)
}
fn update_niche_counts<T: Float>(
population: &[Individual<T>],
reference_points: &mut [ReferencePoint<T>],
) {
for ref_point in reference_points.iter_mut() {
ref_point.niche_count = 0;
}
for individual in population {
if let Some(ref_idx) = individual.reference_point_index {
if ref_idx < reference_points.len() {
reference_points[ref_idx].niche_count += 1;
}
}
}
}
fn tournament_selection<'a, T: Float>(
population: &'a [Individual<T>],
rng: &mut impl Rng,
) -> Result<&'a Individual<T>> {
let n = population.len();
let i1 = (rng.random::<f64>() * n as f64) as usize % n;
let i2 = (rng.random::<f64>() * n as f64) as usize % n;
if compare_individuals(&population[i1], &population[i2]) == Ordering::Less {
Ok(&population[i1])
} else {
Ok(&population[i2])
}
}
fn compare_individuals<T: Float>(a: &Individual<T>, b: &Individual<T>) -> Ordering {
if a.rank < b.rank {
Ordering::Less
} else if a.rank > b.rank {
Ordering::Greater
} else if a.perpendicular_distance < b.perpendicular_distance {
Ordering::Less
} else if a.perpendicular_distance > b.perpendicular_distance {
Ordering::Greater
} else {
Ordering::Equal
}
}
fn sbx_crossover<T: Float>(
parent1: &[T],
parent2: &[T],
bounds: &[(T, T)],
eta: T,
rng: &mut impl Rng,
) -> Result<(Vec<T>, Vec<T>)> {
let n = parent1.len();
let mut child1 = Vec::with_capacity(n);
let mut child2 = Vec::with_capacity(n);
for i in 0..n {
let (lower, upper) = bounds[i];
let p1 = parent1[i];
let p2 = parent2[i];
let rand_val = T::from(rng.random::<f64>()).ok_or_else(|| {
NumRs2Error::ConversionError("Random value conversion failed".to_string())
})?;
if (p1 - p2).abs()
> T::from(1e-14).ok_or_else(|| {
NumRs2Error::ConversionError("Epsilon conversion failed".to_string())
})?
{
let (c1, c2) = if p1 < p2 { (p1, p2) } else { (p2, p1) };
let beta = T::one()
+ (T::from(2.0).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? * (c1 - lower))
/ (c2 - c1);
let alpha = T::from(2.0).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? - beta.powf(-(eta + T::one()));
let beta_q = if rand_val <= (T::one() / alpha) {
(rand_val * alpha).powf(T::one() / (eta + T::one()))
} else {
(T::one()
/ (T::from(2.0).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? - rand_val * alpha))
.powf(T::one() / (eta + T::one()))
};
let offspring1 = T::from(0.5).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? * ((c1 + c2) - beta_q * (c2 - c1));
let offspring2 = T::from(0.5).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? * ((c1 + c2) + beta_q * (c2 - c1));
child1.push(offspring1.max(lower).min(upper));
child2.push(offspring2.max(lower).min(upper));
} else {
child1.push(p1);
child2.push(p2);
}
}
Ok((child1, child2))
}
fn polynomial_mutation<T: Float>(
individual: &mut [T],
bounds: &[(T, T)],
eta: T,
rng: &mut impl Rng,
) -> Result<()> {
let n = individual.len();
for i in 0..n {
let (lower, upper) = bounds[i];
let x = individual[i];
let rand_val = T::from(rng.random::<f64>()).ok_or_else(|| {
NumRs2Error::ConversionError("Random value conversion failed".to_string())
})?;
let delta1 = (x - lower) / (upper - lower);
let delta2 = (upper - x) / (upper - lower);
let mut_pow = T::one() / (eta + T::one());
let delta_q = if rand_val
< T::from(0.5).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? {
let xy = T::one() - delta1;
let val = T::from(2.0).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? * rand_val
+ (T::one()
- T::from(2.0).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? * rand_val)
* xy.powf(eta + T::one());
val.powf(mut_pow) - T::one()
} else {
let xy = T::one() - delta2;
let val = T::from(2.0).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? * (T::one() - rand_val)
+ T::from(2.0).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})? * (rand_val
- T::from(0.5).ok_or_else(|| {
NumRs2Error::ConversionError("Constant conversion failed".to_string())
})?)
* xy.powf(eta + T::one());
T::one() - val.powf(mut_pow)
};
individual[i] = (x + delta_q * (upper - lower)).max(lower).min(upper);
}
Ok(())
}