pub mod configuration;
pub mod das_dennis;
use crate::configuration::GaConfiguration;
use crate::error::GaError;
use crate::multi_objective::non_dominated_sort::{
assign_ranks, non_dominated_sort_with_directions,
};
use crate::multi_objective::pareto::{ParetoFront, ParetoIndividual};
use crate::nsga2::configuration::ObjectiveDirection;
use crate::nsga3::configuration::Nsga3Configuration;
use crate::observer::Nsga3Observer;
use crate::operations::mutation;
use crate::traits::{InitializationFn, LinearChromosome, MutationOperator, VectorFitness};
use rand::Rng;
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
use rayon::prelude::*;
use std::sync::Arc;
use std::time::Instant;
pub struct Nsga3Ga<U>
where
U: LinearChromosome + VectorFitness,
{
pub nsga3_config: Nsga3Configuration,
pub ga_config: GaConfiguration,
pub alleles: Vec<U::Gene>,
pub initialization_fn: Option<Arc<InitializationFn<U::Gene>>>,
pub observer: Option<Arc<dyn Nsga3Observer<U> + Send + Sync>>,
}
impl<U> Nsga3Ga<U>
where
U: LinearChromosome + VectorFitness,
{
pub fn new(nsga3_config: Nsga3Configuration, ga_config: GaConfiguration) -> Self {
Nsga3Ga {
nsga3_config,
ga_config,
alleles: Vec::new(),
initialization_fn: None,
observer: None,
}
}
pub fn with_observer(mut self, obs: Arc<dyn Nsga3Observer<U> + Send + Sync>) -> Self {
self.observer = Some(obs);
self
}
#[inline]
fn notify<F: FnOnce(&dyn Nsga3Observer<U>)>(&self, f: F) {
if let Some(ref obs) = self.observer {
f(obs.as_ref());
}
}
pub fn with_alleles(mut self, alleles: Vec<U::Gene>) -> Self {
self.alleles = alleles;
self
}
pub fn with_initialization_fn<F>(mut self, f: F) -> Self
where
F: Fn(usize, Option<&[U::Gene]>) -> Vec<U::Gene> + Send + Sync + 'static,
{
self.initialization_fn = Some(Arc::new(f));
self
}
pub fn build(self) -> Result<Self, GaError> {
self.validate()?;
Ok(self)
}
pub fn validate(&self) -> Result<(), GaError> {
if self.nsga3_config.num_objectives == 0 {
return Err(GaError::InvalidNsga3Configuration(
"num_objectives must be > 0".to_string(),
));
}
if self.nsga3_config.population_size < 2 {
return Err(GaError::InvalidNsga3Configuration(
"population_size must be >= 2".to_string(),
));
}
if self.initialization_fn.is_none() {
return Err(GaError::InvalidNsga3Configuration(
"initialization_fn is required".to_string(),
));
}
if !self.nsga3_config.objective_directions.is_empty()
&& self.nsga3_config.objective_directions.len() != self.nsga3_config.num_objectives
{
return Err(GaError::InvalidNsga3Configuration(format!(
"objective_directions length ({}) must match num_objectives ({})",
self.nsga3_config.objective_directions.len(),
self.nsga3_config.num_objectives
)));
}
if let Some(p) = self.nsga3_config.reference_points_auto_p() {
if p == 0 {
return Err(GaError::InvalidNsga3Configuration(
"Das-Dennis subdivision count p must be >= 1".to_string(),
));
}
}
let ref_points = self.nsga3_config.effective_reference_points();
match ref_points {
None => {
return Err(GaError::InvalidNsga3Configuration(
"reference points must be configured via with_reference_points_auto(p) or with_reference_points(points)".to_string(),
));
}
Some(points) => {
if points.is_empty() {
return Err(GaError::InvalidNsga3Configuration(
"reference points list must not be empty".to_string(),
));
}
for (i, pt) in points.iter().enumerate() {
if pt.len() != self.nsga3_config.num_objectives {
return Err(GaError::InvalidNsga3Configuration(format!(
"reference point {} has dimension {}, expected {}",
i,
pt.len(),
self.nsga3_config.num_objectives
)));
}
}
}
}
Ok(())
}
fn validate_and_get_ref_points(&self) -> Result<Vec<Vec<f64>>, GaError> {
if self.nsga3_config.num_objectives == 0 {
return Err(GaError::InvalidNsga3Configuration(
"num_objectives must be > 0".to_string(),
));
}
if self.nsga3_config.population_size < 2 {
return Err(GaError::InvalidNsga3Configuration(
"population_size must be >= 2".to_string(),
));
}
if self.initialization_fn.is_none() {
return Err(GaError::InvalidNsga3Configuration(
"initialization_fn is required".to_string(),
));
}
if !self.nsga3_config.objective_directions.is_empty()
&& self.nsga3_config.objective_directions.len() != self.nsga3_config.num_objectives
{
return Err(GaError::InvalidNsga3Configuration(format!(
"objective_directions length ({}) must match num_objectives ({})",
self.nsga3_config.objective_directions.len(),
self.nsga3_config.num_objectives
)));
}
if let Some(p) = self.nsga3_config.reference_points_auto_p() {
if p == 0 {
return Err(GaError::InvalidNsga3Configuration(
"Das-Dennis subdivision count p must be >= 1".to_string(),
));
}
}
let points = self
.nsga3_config
.effective_reference_points()
.ok_or_else(|| {
GaError::InvalidNsga3Configuration(
"reference points must be configured via with_reference_points_auto(p) or with_reference_points(points)".to_string(),
)
})?;
if points.is_empty() {
return Err(GaError::InvalidNsga3Configuration(
"reference points list must not be empty".to_string(),
));
}
for (i, pt) in points.iter().enumerate() {
if pt.len() != self.nsga3_config.num_objectives {
return Err(GaError::InvalidNsga3Configuration(format!(
"reference point {} has dimension {}, expected {}",
i,
pt.len(),
self.nsga3_config.num_objectives
)));
}
}
Ok(points)
}
}
impl<U> Nsga3Ga<U>
where
U: LinearChromosome
+ VectorFitness
+ mutation::ValueMutable
+ crate::traits::RealValuedMutation,
{
pub fn run(&mut self) -> Result<ParetoFront<U>, GaError> {
let reference_points = self.validate_and_get_ref_points()?;
crate::rng::set_seed(self.ga_config.rng_seed);
let pop_size = self.nsga3_config.population_size;
let max_gens = self.nsga3_config.max_generations;
let directions = self.nsga3_config.effective_directions();
let mut population = self.initialize_population()?;
if let Some(first) = population.first() {
let got = first.chromosome.fitness_values().len();
if got != self.nsga3_config.num_objectives {
return Err(GaError::InvalidNsga3Configuration(format!(
"Expected {} objectives from fitness_values(), got {}",
self.nsga3_config.num_objectives, got
)));
}
}
for gen in 0..max_gens {
let t_sort: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
let parent_objs: Vec<&[f64]> =
population.iter().map(|i| i.objectives.as_slice()).collect();
let parent_fronts = non_dominated_sort_with_directions(&parent_objs, &directions);
let mut parent_ranks = vec![0usize; population.len()];
assign_ranks(&mut parent_ranks, &parent_fronts);
for (i, &r) in parent_ranks.iter().enumerate() {
population[i].rank = r;
}
if let Some(start) = t_sort {
self.notify(|obs| {
obs.on_non_dominated_sort_complete(gen, start.elapsed().as_secs_f64() * 1000.0)
});
}
let offspring = self.create_offspring(&population)?;
let mut combined = population;
combined.extend(offspring);
let combined_objs: Vec<&[f64]> =
combined.iter().map(|i| i.objectives.as_slice()).collect();
let combined_fronts = non_dominated_sort_with_directions(&combined_objs, &directions);
let mut combined_ranks = vec![0usize; combined.len()];
assign_ranks(&mut combined_ranks, &combined_fronts);
for (i, &r) in combined_ranks.iter().enumerate() {
combined[i].rank = r;
}
population = nsga3_environmental_selection(
combined,
combined_fronts,
pop_size,
&reference_points,
&directions,
);
let front_count = population
.iter()
.map(|i| i.rank)
.max()
.map(|m| m + 1)
.unwrap_or(0);
self.notify(|obs| obs.on_pareto_front_assigned(gen, front_count, population.len()));
}
let front_individuals: Vec<ParetoIndividual<U>> =
population.into_iter().filter(|ind| ind.rank == 0).collect();
Ok(ParetoFront::new(front_individuals))
}
fn initialize_population(&self) -> Result<Vec<ParetoIndividual<U>>, GaError> {
let init_fn = self.initialization_fn.as_ref().ok_or_else(|| {
GaError::InitializationError("No initialization function set".to_string())
})?;
let pop_size = self.nsga3_config.population_size;
let genes_per_chrom = match self.ga_config.limit_configuration.chromosome_length {
crate::chromosomes::ChromosomeLength::Fixed(n) => n,
crate::chromosomes::ChromosomeLength::Variable { .. } => {
return Err(GaError::InvalidNsga3Configuration(
"ChromosomeLength::Variable is not yet supported (Phase 52). Use ChromosomeLength::Fixed.".into(),
));
}
};
let alleles = if self.alleles.is_empty() {
None
} else {
Some(self.alleles.as_slice())
};
let chromosomes: Vec<U> = crate::traits::initialize_chromosomes(
pop_size,
genes_per_chrom,
alleles,
init_fn,
None,
0,
);
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
let population: Vec<ParetoIndividual<U>> = chromosomes
.into_par_iter()
.map(|mut chrom| {
chrom.calculate_fitness();
let objectives = chrom.fitness_values().to_vec();
ParetoIndividual::new(chrom, objectives)
})
.collect();
#[cfg(any(target_arch = "wasm32", not(feature = "parallel")))]
let population: Vec<ParetoIndividual<U>> = chromosomes
.into_iter()
.map(|mut chrom| {
chrom.calculate_fitness();
let objectives = chrom.fitness_values().to_vec();
ParetoIndividual::new(chrom, objectives)
})
.collect();
Ok(population)
}
fn create_offspring(
&self,
population: &[ParetoIndividual<U>],
) -> Result<Vec<ParetoIndividual<U>>, GaError> {
use crate::operations::crossover;
let pop_size = self.nsga3_config.population_size;
let crossover_config = self.ga_config.crossover_configuration;
let mutation_config = self.ga_config.mutation_configuration;
let crossover_prob = crossover_config.probability_max.unwrap_or(1.0);
let mut_prob = mutation_config.probability_max.unwrap_or(0.1);
let mut rng = crate::rng::make_rng();
let mut raw_offspring: Vec<U> = Vec::with_capacity(pop_size);
while raw_offspring.len() < pop_size {
let parent_a = self.binary_tournament(population, &mut rng);
let parent_b = self.binary_tournament(population, &mut rng);
let p: f64 = rng.random();
let mut children = if p <= crossover_prob {
crossover::factory(
&population[parent_a].chromosome,
&population[parent_b].chromosome,
crossover_config,
)?
} else {
vec![
population[parent_a].chromosome.clone(),
population[parent_b].chromosome.clone(),
]
};
for child in children.iter_mut() {
let mp: f64 = rng.random();
if mp <= mut_prob {
if matches!(
mutation_config.method,
crate::operations::Mutation::Differential(..)
) {
return Err(GaError::MutationError(
"Differential mutation is not supported in NSGA-III; \
use Cauchy, LevyFlight, Polynomial, or a standard mutation method instead."
.to_string(),
));
}
mutation_config
.method
.mutate(child, &mutation_config.method)?;
}
}
for child in children {
raw_offspring.push(child);
if raw_offspring.len() >= pop_size {
break;
}
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
let offspring: Vec<ParetoIndividual<U>> = raw_offspring
.into_par_iter()
.map(|mut chrom| {
chrom.calculate_fitness();
let objectives = chrom.fitness_values().to_vec();
ParetoIndividual::new(chrom, objectives)
})
.collect();
#[cfg(any(target_arch = "wasm32", not(feature = "parallel")))]
let offspring: Vec<ParetoIndividual<U>> = raw_offspring
.into_iter()
.map(|mut chrom| {
chrom.calculate_fitness();
let objectives = chrom.fitness_values().to_vec();
ParetoIndividual::new(chrom, objectives)
})
.collect();
Ok(offspring)
}
fn binary_tournament(&self, population: &[ParetoIndividual<U>], rng: &mut impl Rng) -> usize {
let n = population.len();
let i = rng.random_range(0..n);
let j = rng.random_range(0..n);
if population[i].rank < population[j].rank {
i
} else if population[j].rank < population[i].rank {
j
} else if rng.random::<bool>() {
i
} else {
j
}
}
}
fn nsga3_environmental_selection<U: LinearChromosome>(
combined: Vec<ParetoIndividual<U>>,
fronts: Vec<Vec<usize>>,
pop_size: usize,
reference_points: &[Vec<f64>],
directions: &[ObjectiveDirection],
) -> Vec<ParetoIndividual<U>> {
let mut next_indices: Vec<usize> = Vec::with_capacity(pop_size);
let mut splitting_front: Vec<usize> = Vec::new();
for front in &fronts {
if next_indices.len() + front.len() <= pop_size {
next_indices.extend_from_slice(front);
if next_indices.len() == pop_size {
splitting_front.clear();
break;
}
} else {
splitting_front = front.clone();
break;
}
}
if splitting_front.is_empty() {
return next_indices
.into_iter()
.map(|idx| combined[idx].clone())
.collect();
}
let mut st_indices: Vec<usize> = next_indices.clone();
st_indices.extend_from_slice(&splitting_front);
let st_normalized = normalize_st(&combined, &st_indices, directions);
let association: Vec<(usize, f64)> =
associate_to_reference_points(&st_normalized, reference_points);
let mut niche_count = vec![0usize; reference_points.len()];
for &(ref_idx, _) in &association[..next_indices.len()] {
niche_count[ref_idx] += 1;
}
let split_offset = next_indices.len();
let mut remaining: Vec<Vec<(usize, f64)>> = vec![Vec::new(); reference_points.len()];
for (i_in_split, _) in splitting_front.iter().enumerate() {
let k = split_offset + i_in_split;
let (ref_idx, perp) = association[k];
remaining[ref_idx].push((i_in_split, perp));
}
let mut rng = crate::rng::make_rng();
while next_indices.len() < pop_size {
let mut min_niche = usize::MAX;
for (j, slot) in remaining.iter().enumerate() {
if !slot.is_empty() && niche_count[j] < min_niche {
min_niche = niche_count[j];
}
}
let tied: Vec<usize> = remaining
.iter()
.enumerate()
.filter_map(|(j, slot)| {
if !slot.is_empty() && niche_count[j] == min_niche {
Some(j)
} else {
None
}
})
.collect();
if tied.is_empty() {
break;
}
let j_star = tied[rng.random_range(0..tied.len())];
let chosen_position_in_slot = if niche_count[j_star] == 0 {
let mut best_pos = 0;
let mut best_dist = remaining[j_star][0].1;
for (pos, &(_idx, dist)) in remaining[j_star].iter().enumerate().skip(1) {
if dist < best_dist {
best_dist = dist;
best_pos = pos;
}
}
best_pos
} else {
rng.random_range(0..remaining[j_star].len())
};
let (cand_idx_in_split, _) = remaining[j_star].swap_remove(chosen_position_in_slot);
let global_idx = splitting_front[cand_idx_in_split];
next_indices.push(global_idx);
niche_count[j_star] += 1;
}
next_indices
.into_iter()
.map(|idx| combined[idx].clone())
.collect()
}
fn normalize_st<U: LinearChromosome>(
combined: &[ParetoIndividual<U>],
st_indices: &[usize],
directions: &[ObjectiveDirection],
) -> Vec<Vec<f64>> {
let m = directions.len();
if st_indices.is_empty() {
return Vec::new();
}
let raw: Vec<Vec<f64>> = st_indices
.iter()
.map(|&i| {
(0..m)
.map(|d| match directions[d] {
ObjectiveDirection::Minimize => combined[i].objectives[d],
ObjectiveDirection::Maximize => -combined[i].objectives[d],
})
.collect()
})
.collect();
let mut ideal = vec![f64::INFINITY; m];
for v in &raw {
for d in 0..m {
if v[d] < ideal[d] {
ideal[d] = v[d];
}
}
}
let translated: Vec<Vec<f64>> = raw
.iter()
.map(|v| (0..m).map(|d| v[d] - ideal[d]).collect())
.collect();
let mut intercepts = vec![0.0f64; m];
let mut degenerate = false;
for axis in 0..m {
let mut best_idx: usize = 0;
let mut best_asf = f64::INFINITY;
for (i, v) in translated.iter().enumerate() {
let mut asf = f64::NEG_INFINITY;
for (k, &vk) in v.iter().enumerate() {
let w = if k == axis { 1.0 } else { 1.0e-6 };
let val = vk / w;
if val > asf {
asf = val;
}
}
if asf < best_asf {
best_asf = asf;
best_idx = i;
}
}
intercepts[axis] = translated[best_idx][axis];
if intercepts[axis].abs() < f64::EPSILON {
degenerate = true;
}
}
if degenerate {
let mut nadir = vec![f64::NEG_INFINITY; m];
for v in &translated {
for d in 0..m {
if v[d] > nadir[d] {
nadir[d] = v[d];
}
}
}
for d in 0..m {
intercepts[d] = nadir[d].max(f64::EPSILON);
}
}
for intercept in intercepts.iter_mut() {
if *intercept < f64::EPSILON {
*intercept = f64::EPSILON;
}
}
translated
.iter()
.map(|v| (0..m).map(|d| v[d] / intercepts[d]).collect())
.collect()
}
fn associate_to_reference_points(
normalized: &[Vec<f64>],
reference_points: &[Vec<f64>],
) -> Vec<(usize, f64)> {
let r_dot_r: Vec<f64> = reference_points
.iter()
.map(|r| r.iter().map(|x| x * x).sum::<f64>().max(f64::EPSILON))
.collect();
normalized
.iter()
.map(|f| {
let mut best_idx = 0usize;
let mut best_dist = f64::INFINITY;
for (j, r) in reference_points.iter().enumerate() {
let fr: f64 = f.iter().zip(r.iter()).map(|(a, b)| a * b).sum();
let scale = fr / r_dot_r[j];
let dist_sq: f64 = f
.iter()
.zip(r.iter())
.map(|(a, b)| {
let diff = a - scale * b;
diff * diff
})
.sum();
let dist = dist_sq.sqrt();
if dist < best_dist {
best_dist = dist;
best_idx = j;
}
}
(best_idx, best_dist)
})
.collect()
}