use scirs2_core::ndarray::{Array1, ArrayView1};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap;
pub type Solution = MultiObjectiveSolution;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiObjectiveSolution {
pub variables: Array1<f64>,
pub objectives: Array1<f64>,
pub constraint_violation: f64,
pub rank: usize,
pub crowding_distance: f64,
pub metadata: HashMap<String, f64>,
}
impl MultiObjectiveSolution {
pub fn new(variables: Array1<f64>, objectives: Array1<f64>) -> Self {
Self {
variables,
objectives,
constraint_violation: 0.0,
rank: 0,
crowding_distance: 0.0,
metadata: HashMap::new(),
}
}
pub fn new_with_constraints(
variables: Array1<f64>,
objectives: Array1<f64>,
constraint_violation: f64,
) -> Self {
Self {
variables,
objectives,
constraint_violation,
rank: 0,
crowding_distance: 0.0,
metadata: HashMap::new(),
}
}
pub fn dominates(&self, other: &Self) -> bool {
if self.constraint_violation < other.constraint_violation {
return true;
}
if self.constraint_violation > other.constraint_violation {
return false;
}
let mut at_least_one_better = false;
for (obj1, obj2) in self.objectives.iter().zip(other.objectives.iter()) {
if obj1 > obj2 {
return false; }
if obj1 < obj2 {
at_least_one_better = true;
}
}
at_least_one_better
}
pub fn is_dominated_by(&self, other: &Self) -> bool {
other.dominates(self)
}
pub fn is_non_dominated_with(&self, other: &Self) -> bool {
!self.dominates(other) && !other.dominates(self)
}
pub fn objective_distance(&self, other: &Self) -> f64 {
self.objectives
.iter()
.zip(other.objectives.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt()
}
pub fn variable_distance(&self, other: &Self) -> f64 {
self.variables
.iter()
.zip(other.variables.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt()
}
pub fn n_objectives(&self) -> usize {
self.objectives.len()
}
pub fn n_variables(&self) -> usize {
self.variables.len()
}
pub fn is_feasible(&self) -> bool {
self.constraint_violation <= 1e-10
}
pub fn set_metadata(&mut self, key: String, value: f64) {
self.metadata.insert(key, value);
}
pub fn get_metadata(&self, key: &str) -> Option<f64> {
self.metadata.get(key).copied()
}
pub fn with_objectives(&self, objectives: Array1<f64>) -> Self {
Self {
variables: self.variables.clone(),
objectives,
constraint_violation: self.constraint_violation,
rank: self.rank,
crowding_distance: self.crowding_distance,
metadata: self.metadata.clone(),
}
}
pub fn with_variables(&self, variables: Array1<f64>) -> Self {
Self {
variables,
objectives: self.objectives.clone(),
constraint_violation: self.constraint_violation,
rank: self.rank,
crowding_distance: self.crowding_distance,
metadata: self.metadata.clone(),
}
}
}
impl PartialEq for MultiObjectiveSolution {
fn eq(&self, other: &Self) -> bool {
self.variables == other.variables && self.objectives == other.objectives
}
}
impl PartialOrd for MultiObjectiveSolution {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self.rank.cmp(&other.rank) {
Ordering::Equal => {
other.crowding_distance.partial_cmp(&self.crowding_distance)
}
other_order => Some(other_order),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiObjectiveResult {
pub pareto_front: Vec<MultiObjectiveSolution>,
pub population: Vec<MultiObjectiveSolution>,
pub n_evaluations: usize,
pub n_generations: usize,
pub success: bool,
pub message: String,
pub hypervolume: Option<f64>,
pub metrics: OptimizationMetrics,
}
impl MultiObjectiveResult {
pub fn new(
pareto_front: Vec<MultiObjectiveSolution>,
population: Vec<MultiObjectiveSolution>,
n_evaluations: usize,
n_generations: usize,
) -> Self {
Self {
pareto_front,
population,
n_evaluations,
n_generations,
success: true,
message: "Optimization completed successfully".to_string(),
hypervolume: None,
metrics: OptimizationMetrics::default(),
}
}
pub fn failed(message: String, n_evaluations: usize, n_generations: usize) -> Self {
Self {
pareto_front: vec![],
population: vec![],
n_evaluations,
n_generations,
success: false,
message,
hypervolume: None,
metrics: OptimizationMetrics::default(),
}
}
pub fn best_for_objective(&self, objective_index: usize) -> Option<&MultiObjectiveSolution> {
self.pareto_front.iter().min_by(|a, b| {
a.objectives[objective_index]
.partial_cmp(&b.objectives[objective_index])
.unwrap_or(Ordering::Equal)
})
}
pub fn closest_to_ideal(&self, ideal_point: &Array1<f64>) -> Option<&MultiObjectiveSolution> {
self.pareto_front.iter().min_by(|a, b| {
let dist_a = a
.objectives
.iter()
.zip(ideal_point.iter())
.map(|(obj, ideal)| (obj - ideal).powi(2))
.sum::<f64>();
let dist_b = b
.objectives
.iter()
.zip(ideal_point.iter())
.map(|(obj, ideal)| (obj - ideal).powi(2))
.sum::<f64>();
dist_a.partial_cmp(&dist_b).unwrap_or(Ordering::Equal)
})
}
pub fn pareto_front_size(&self) -> usize {
self.pareto_front.len()
}
pub fn has_feasible_solutions(&self) -> bool {
self.pareto_front.iter().any(|sol| sol.is_feasible())
}
pub fn feasible_solutions(&self) -> Vec<&MultiObjectiveSolution> {
self.pareto_front
.iter()
.filter(|sol| sol.is_feasible())
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OptimizationMetrics {
pub convergence_history: Vec<f64>,
pub diversity_history: Vec<f64>,
pub average_objectives: Vec<Array1<f64>>,
pub best_objectives: Vec<Array1<f64>>,
pub population_stats: PopulationStatistics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopulationStatistics {
pub mean_objectives: Array1<f64>,
pub std_objectives: Array1<f64>,
pub min_objectives: Array1<f64>,
pub max_objectives: Array1<f64>,
pub feasibility_ratio: f64,
pub avg_constraint_violation: f64,
}
impl Default for PopulationStatistics {
fn default() -> Self {
Self {
mean_objectives: Array1::zeros(0),
std_objectives: Array1::zeros(0),
min_objectives: Array1::zeros(0),
max_objectives: Array1::zeros(0),
feasibility_ratio: 0.0,
avg_constraint_violation: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct Population {
solutions: Vec<MultiObjectiveSolution>,
}
impl Population {
pub fn new() -> Self {
Self { solutions: vec![] }
}
pub fn with_capacity(
population_size: usize,
_n_objectives: usize,
_n_variables: usize,
) -> Self {
Self {
solutions: Vec::with_capacity(population_size),
}
}
pub fn from_solutions(solutions: Vec<MultiObjectiveSolution>) -> Self {
Self { solutions }
}
pub fn add(&mut self, solution: MultiObjectiveSolution) {
self.solutions.push(solution);
}
pub fn solutions(&self) -> &[MultiObjectiveSolution] {
&self.solutions
}
pub fn solutions_mut(&mut self) -> &mut Vec<MultiObjectiveSolution> {
&mut self.solutions
}
pub fn size(&self) -> usize {
self.solutions.len()
}
pub fn is_empty(&self) -> bool {
self.solutions.is_empty()
}
pub fn clear(&mut self) {
self.solutions.clear();
}
pub fn extract_pareto_front(&self) -> Vec<MultiObjectiveSolution> {
let mut pareto_front: Vec<MultiObjectiveSolution> = Vec::new();
for candidate in &self.solutions {
let mut is_dominated = false;
for existing in &pareto_front {
if existing.dominates(candidate) {
is_dominated = true;
break;
}
}
if !is_dominated {
pareto_front.retain(|existing| !candidate.dominates(existing));
pareto_front.push(candidate.clone());
}
}
pareto_front
}
pub fn non_dominated_sort(&mut self) -> Vec<Vec<usize>> {
let n = self.solutions.len();
let mut fronts = Vec::new();
let mut domination_counts = vec![0; n];
let mut dominated_solutions: Vec<Vec<usize>> = vec![Vec::new(); n];
for i in 0..n {
for j in 0..n {
if i != j {
if self.solutions[i].dominates(&self.solutions[j]) {
dominated_solutions[i].push(j);
} else if self.solutions[j].dominates(&self.solutions[i]) {
domination_counts[i] += 1;
}
}
}
}
let mut current_front = Vec::new();
for i in 0..n {
if domination_counts[i] == 0 {
self.solutions[i].rank = 0;
current_front.push(i);
}
}
let mut front_number = 0;
while !current_front.is_empty() {
fronts.push(current_front.clone());
let mut next_front = Vec::new();
for &i in ¤t_front {
for &j in &dominated_solutions[i] {
domination_counts[j] -= 1;
if domination_counts[j] == 0 {
self.solutions[j].rank = front_number + 1;
next_front.push(j);
}
}
}
current_front = next_front;
front_number += 1;
}
fronts
}
pub fn calculate_crowding_distances(&mut self, front_indices: &[usize]) {
let front_size = front_indices.len();
if front_size <= 2 {
for &i in front_indices {
self.solutions[i].crowding_distance = f64::INFINITY;
}
return;
}
let n_objectives = self.solutions[front_indices[0]].n_objectives();
for &i in front_indices {
self.solutions[i].crowding_distance = 0.0;
}
for obj in 0..n_objectives {
let mut sorted_indices = front_indices.to_vec();
sorted_indices.sort_by(|&a, &b| {
self.solutions[a].objectives[obj]
.partial_cmp(&self.solutions[b].objectives[obj])
.unwrap_or(Ordering::Equal)
});
self.solutions[sorted_indices[0]].crowding_distance = f64::INFINITY;
self.solutions[sorted_indices[front_size - 1]].crowding_distance = f64::INFINITY;
let obj_min = self.solutions[sorted_indices[0]].objectives[obj];
let obj_max = self.solutions[sorted_indices[front_size - 1]].objectives[obj];
let obj_range = obj_max - obj_min;
if obj_range > 0.0 {
for i in 1..front_size - 1 {
if self.solutions[sorted_indices[i]].crowding_distance != f64::INFINITY {
let distance = (self.solutions[sorted_indices[i + 1]].objectives[obj]
- self.solutions[sorted_indices[i - 1]].objectives[obj])
/ obj_range;
self.solutions[sorted_indices[i]].crowding_distance += distance;
}
}
}
}
}
pub fn select_best(&mut self, target_size: usize) -> Vec<MultiObjectiveSolution> {
if self.solutions.len() <= target_size {
return self.solutions.clone();
}
let fronts = self.non_dominated_sort();
for front in &fronts {
self.calculate_crowding_distances(front);
}
let mut selected = Vec::new();
for front in &fronts {
if selected.len() + front.len() <= target_size {
for &i in front {
selected.push(self.solutions[i].clone());
}
} else {
let remaining = target_size - selected.len();
let mut front_solutions: Vec<_> =
front.iter().map(|&i| &self.solutions[i]).collect();
front_solutions.sort_by(|a, b| {
b.crowding_distance
.partial_cmp(&a.crowding_distance)
.unwrap_or(Ordering::Equal)
});
for solution in front_solutions.iter().take(remaining) {
selected.push((*solution).clone());
}
break;
}
}
selected
}
pub fn calculate_statistics(&self) -> PopulationStatistics {
if self.solutions.is_empty() {
return PopulationStatistics::default();
}
let n_objectives = self.solutions[0].n_objectives();
let n_solutions = self.solutions.len();
let mut mean_objectives = Array1::zeros(n_objectives);
let mut min_objectives = Array1::from_elem(n_objectives, f64::INFINITY);
let mut max_objectives = Array1::from_elem(n_objectives, f64::NEG_INFINITY);
let mut feasible_count = 0;
let mut total_violation = 0.0;
for solution in &self.solutions {
for (i, &obj) in solution.objectives.iter().enumerate() {
mean_objectives[i] += obj;
min_objectives[i] = min_objectives[i].min(obj);
max_objectives[i] = max_objectives[i].max(obj);
}
if solution.is_feasible() {
feasible_count += 1;
}
total_violation += solution.constraint_violation;
}
mean_objectives /= n_solutions as f64;
let mut std_objectives = Array1::zeros(n_objectives);
for solution in &self.solutions {
for (i, &obj) in solution.objectives.iter().enumerate() {
let diff = obj - mean_objectives[i];
std_objectives[i] += diff * diff;
}
}
std_objectives = std_objectives.mapv(|x: f64| (x / n_solutions as f64).sqrt());
PopulationStatistics {
mean_objectives,
std_objectives,
min_objectives,
max_objectives,
feasibility_ratio: feasible_count as f64 / n_solutions as f64,
avg_constraint_violation: total_violation / n_solutions as f64,
}
}
}
impl Default for Population {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_solution_creation() {
let variables = array![1.0, 2.0, 3.0];
let objectives = array![4.0, 5.0];
let solution = MultiObjectiveSolution::new(variables.clone(), objectives.clone());
assert_eq!(solution.variables, variables);
assert_eq!(solution.objectives, objectives);
assert_eq!(solution.constraint_violation, 0.0);
assert!(solution.is_feasible());
}
#[test]
fn test_domination() {
let sol1 = MultiObjectiveSolution::new(array![1.0], array![1.0, 2.0]);
let sol2 = MultiObjectiveSolution::new(array![2.0], array![2.0, 3.0]);
let sol3 = MultiObjectiveSolution::new(array![3.0], array![0.5, 3.5]);
assert!(sol1.dominates(&sol2)); assert!(!sol2.dominates(&sol1));
assert!(sol1.is_non_dominated_with(&sol3)); }
#[test]
fn test_constraint_domination() {
let feasible =
MultiObjectiveSolution::new_with_constraints(array![1.0], array![2.0, 3.0], 0.0);
let infeasible =
MultiObjectiveSolution::new_with_constraints(array![2.0], array![1.0, 2.0], 1.0);
assert!(feasible.dominates(&infeasible)); assert!(!infeasible.dominates(&feasible));
}
#[test]
fn test_population_pareto_front() {
let mut population = Population::new();
population.add(MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]));
population.add(MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]));
population.add(MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]));
population.add(MultiObjectiveSolution::new(array![4.0], array![2.5, 2.5]));
let pareto_front = population.extract_pareto_front();
assert_eq!(pareto_front.len(), 3); }
#[test]
fn test_non_dominated_sorting() {
let mut population = Population::new();
population.add(MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]));
population.add(MultiObjectiveSolution::new(array![2.0], array![3.0, 1.0]));
population.add(MultiObjectiveSolution::new(array![3.0], array![2.0, 2.0]));
population.add(MultiObjectiveSolution::new(array![4.0], array![4.0, 4.0]));
let fronts = population.non_dominated_sort();
assert_eq!(fronts.len(), 2); assert_eq!(fronts[0].len(), 3); assert_eq!(fronts[1].len(), 1);
for &i in &fronts[0] {
assert_eq!(population.solutions[i].rank, 0);
}
for &i in &fronts[1] {
assert_eq!(population.solutions[i].rank, 1);
}
}
#[test]
fn test_crowding_distance() {
let mut population = Population::new();
population.add(MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]));
population.add(MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]));
population.add(MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]));
let front = vec![0, 1, 2];
population.calculate_crowding_distances(&front);
assert_eq!(population.solutions[0].crowding_distance, f64::INFINITY);
assert_eq!(population.solutions[2].crowding_distance, f64::INFINITY);
assert!(population.solutions[1].crowding_distance.is_finite());
assert!(population.solutions[1].crowding_distance > 0.0);
}
#[test]
fn test_population_statistics() {
let mut population = Population::new();
population.add(MultiObjectiveSolution::new(array![1.0], array![1.0, 3.0]));
population.add(MultiObjectiveSolution::new(array![2.0], array![2.0, 2.0]));
population.add(MultiObjectiveSolution::new(array![3.0], array![3.0, 1.0]));
let stats = population.calculate_statistics();
assert_eq!(stats.mean_objectives[0], 2.0); assert_eq!(stats.mean_objectives[1], 2.0); assert_eq!(stats.min_objectives[0], 1.0);
assert_eq!(stats.max_objectives[0], 3.0);
assert_eq!(stats.feasibility_ratio, 1.0); }
}