use crate::error::{OptimizeError, OptimizeResult};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{Rng, SeedableRng};
pub fn dominates(a: &[f64], b: &[f64]) -> bool {
if a.len() != b.len() || a.is_empty() {
return false;
}
let mut strictly_better = false;
for (&ai, &bi) in a.iter().zip(b.iter()) {
if ai > bi {
return false;
}
if ai < bi {
strictly_better = true;
}
}
strictly_better
}
pub fn non_dominated_sort(solutions: &[Vec<f64>]) -> Vec<Vec<usize>> {
let n = solutions.len();
let mut dom_count = vec![0usize; n];
let mut dom_set: Vec<Vec<usize>> = vec![Vec::new(); n];
for i in 0..n {
for j in (i + 1)..n {
if dominates(&solutions[i], &solutions[j]) {
dom_set[i].push(j);
dom_count[j] += 1;
} else if dominates(&solutions[j], &solutions[i]) {
dom_set[j].push(i);
dom_count[i] += 1;
}
}
}
let mut fronts: Vec<Vec<usize>> = Vec::new();
let mut current_front: Vec<usize> = (0..n).filter(|&i| dom_count[i] == 0).collect();
while !current_front.is_empty() {
let mut next_front = Vec::new();
for &p in ¤t_front {
for &q in &dom_set[p] {
dom_count[q] -= 1;
if dom_count[q] == 0 {
next_front.push(q);
}
}
}
fronts.push(current_front);
current_front = next_front;
}
fronts
}
pub fn hypervolume(pareto_front: &[Vec<f64>], reference_point: &[f64]) -> f64 {
if pareto_front.is_empty() {
return 0.0;
}
match reference_point.len() {
0 => 0.0,
1 => hv_1d(pareto_front, reference_point),
2 => hv_2d(pareto_front, reference_point),
_ => hv_mc(pareto_front, reference_point, 100_000),
}
}
fn hv_1d(front: &[Vec<f64>], ref_pt: &[f64]) -> f64 {
let min_f = front
.iter()
.filter_map(|f| f.first().copied())
.fold(f64::INFINITY, f64::min);
(ref_pt[0] - min_f).max(0.0)
}
fn hv_2d(front: &[Vec<f64>], ref_pt: &[f64]) -> f64 {
let mut pts: Vec<(f64, f64)> = front
.iter()
.filter(|f| f.len() >= 2 && f[0] < ref_pt[0] && f[1] < ref_pt[1])
.map(|f| (f[0], f[1]))
.collect();
if pts.is_empty() {
return 0.0;
}
pts.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let mut hv = 0.0f64;
let mut min_y = ref_pt[1];
let mut prev_x = ref_pt[0];
for &(x, y) in &pts {
if y < min_y {
hv += (prev_x - x) * (min_y - y);
min_y = y;
prev_x = x;
}
}
hv
}
fn hv_mc(front: &[Vec<f64>], ref_pt: &[f64], n_samples: usize) -> f64 {
let n_obj = ref_pt.len();
let ideal: Vec<f64> = (0..n_obj)
.map(|j| front.iter().filter_map(|f| f.get(j).copied()).fold(f64::INFINITY, f64::min))
.collect();
let box_vol: f64 = (0..n_obj).map(|j| (ref_pt[j] - ideal[j]).max(0.0)).product();
if box_vol <= 0.0 {
return 0.0;
}
let mut rng = StdRng::seed_from_u64(42);
let count = (0..n_samples)
.filter(|_| {
let sample: Vec<f64> = (0..n_obj)
.map(|j| ideal[j] + rng.random::<f64>() * (ref_pt[j] - ideal[j]))
.collect();
front.iter().any(|f| {
f.len() >= n_obj && (0..n_obj).all(|j| f[j] <= sample[j])
})
})
.count();
box_vol * count as f64 / n_samples as f64
}
pub fn crowding_distance(front: &[Vec<f64>]) -> Vec<f64> {
let n = front.len();
if n == 0 {
return Vec::new();
}
if n <= 2 {
return vec![f64::INFINITY; n];
}
let n_obj = front[0].len();
let mut dist = vec![0.0f64; n];
for obj in 0..n_obj {
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
front[a][obj]
.partial_cmp(&front[b][obj])
.unwrap_or(std::cmp::Ordering::Equal)
});
let f_min = front[order[0]][obj];
let f_max = front[order[n - 1]][obj];
let range = f_max - f_min;
dist[order[0]] = f64::INFINITY;
dist[order[n - 1]] = f64::INFINITY;
if range < 1e-14 {
continue;
}
for k in 1..(n - 1) {
dist[order[k]] +=
(front[order[k + 1]][obj] - front[order[k - 1]][obj]) / range;
}
}
dist
}
#[derive(Debug, Clone)]
pub struct NsgaIII {
pub n_objectives: usize,
pub population_size: usize,
pub n_generations: usize,
pub crossover_prob: f64,
pub mutation_prob: f64,
pub seed: u64,
pub n_divisions: usize,
}
impl NsgaIII {
pub fn new(n_objectives: usize) -> Self {
Self {
n_objectives,
population_size: 100,
n_generations: 100,
crossover_prob: 0.9,
mutation_prob: 0.01,
seed: 42,
n_divisions: 4,
}
}
pub fn optimize<F>(
&self,
objectives: F,
bounds: &[(f64, f64)],
) -> OptimizeResult<NsgaResult>
where
F: Fn(&[f64]) -> Vec<f64>,
{
if bounds.is_empty() {
return Err(OptimizeError::InvalidInput(
"bounds must not be empty".to_string(),
));
}
let (pareto_dec, pareto_obj, n_eval) = run_nsga3_internal(
&objectives,
bounds,
self.n_objectives,
self.population_size,
self.n_generations,
self.crossover_prob,
self.mutation_prob,
self.seed,
self.n_divisions,
);
Ok(NsgaResult {
pareto_front: pareto_dec,
objective_values: pareto_obj,
n_evaluations: n_eval,
})
}
}
#[derive(Debug, Clone)]
pub struct NsgaResult {
pub pareto_front: Vec<Vec<f64>>,
pub objective_values: Vec<Vec<f64>>,
pub n_evaluations: usize,
}
pub fn weighted_sum_optimize<F>(
objectives: F,
weights: &[f64],
bounds: &[(f64, f64)],
n_starts: usize,
seed: u64,
) -> (Vec<f64>, Vec<f64>)
where
F: Fn(&[f64]) -> Vec<f64>,
{
let n_vars = bounds.len();
let n_starts = n_starts.max(1);
let scalar = |x: &[f64]| -> f64 {
objectives(x)
.iter()
.zip(weights.iter())
.map(|(&fi, &wi)| wi * fi)
.sum::<f64>()
};
let mut rng = StdRng::seed_from_u64(seed);
let mut best_x = vec![0.0f64; n_vars];
let mut best_val = f64::INFINITY;
for _ in 0..n_starts {
let mut x: Vec<f64> = bounds
.iter()
.map(|&(lb, ub)| lb + rng.random::<f64>() * (ub - lb))
.collect();
let avg_range = bounds.iter().map(|&(lb, ub)| (ub - lb).abs()).sum::<f64>()
/ n_vars.max(1) as f64;
let mut step = 0.1 * avg_range;
let mut fx = scalar(&x);
loop {
let mut improved = false;
for j in 0..n_vars {
let (lb, ub) = bounds[j];
for &dir in &[1.0f64, -1.0] {
let xj_new = (x[j] + dir * step).clamp(lb, ub);
let mut x_try = x.clone();
x_try[j] = xj_new;
let f_try = scalar(&x_try);
if f_try < fx - 1e-12 {
x[j] = xj_new;
fx = f_try;
improved = true;
}
}
}
if !improved {
step *= 0.5;
if step < 1e-8 {
break;
}
}
}
if fx < best_val {
best_val = fx;
best_x = x;
}
}
let best_f = objectives(&best_x);
(best_x, best_f)
}
pub fn epsilon_constraint_optimize<F>(
objectives: F,
primary_idx: usize,
epsilon_bounds: &[(f64, f64)],
bounds: &[(f64, f64)],
n_starts: usize,
seed: u64,
) -> (Vec<f64>, Vec<f64>)
where
F: Fn(&[f64]) -> Vec<f64>,
{
let n_vars = bounds.len();
let n_starts = n_starts.max(1);
let penalty_rho = 1e4;
let penalized = |x: &[f64]| -> f64 {
let f = objectives(x);
let primary = f.get(primary_idx).copied().unwrap_or(f64::INFINITY);
let mut viol = 0.0f64;
let mut eps_idx = 0usize;
for (i, &fi) in f.iter().enumerate() {
if i == primary_idx {
continue;
}
if let Some(&(lb, ub)) = epsilon_bounds.get(eps_idx) {
viol += (lb - fi).max(0.0) + (fi - ub).max(0.0);
}
eps_idx += 1;
}
primary + penalty_rho * viol
};
let mut rng = StdRng::seed_from_u64(seed);
let mut best_x = vec![0.0f64; n_vars];
let mut best_val = f64::INFINITY;
for _ in 0..n_starts {
let mut x: Vec<f64> = bounds
.iter()
.map(|&(lb, ub)| lb + rng.random::<f64>() * (ub - lb))
.collect();
let avg_range = bounds.iter().map(|&(lb, ub)| (ub - lb).abs()).sum::<f64>()
/ n_vars.max(1) as f64;
let mut step = 0.1 * avg_range;
let mut fx = penalized(&x);
loop {
let mut improved = false;
for j in 0..n_vars {
let (lb, ub) = bounds[j];
for &dir in &[1.0f64, -1.0] {
let xj_new = (x[j] + dir * step).clamp(lb, ub);
let mut x_try = x.clone();
x_try[j] = xj_new;
let f_try = penalized(&x_try);
if f_try < fx - 1e-12 {
x[j] = xj_new;
fx = f_try;
improved = true;
}
}
}
if !improved {
step *= 0.5;
if step < 1e-8 {
break;
}
}
}
if fx < best_val {
best_val = fx;
best_x = x;
}
}
let best_f = objectives(&best_x);
(best_x, best_f)
}
#[allow(clippy::too_many_arguments)]
fn run_nsga3_internal<F>(
objectives: &F,
bounds: &[(f64, f64)],
n_obj: usize,
pop_size: usize,
n_gen: usize,
cx_prob: f64,
mut_prob: f64,
seed: u64,
n_div: usize,
) -> (Vec<Vec<f64>>, Vec<Vec<f64>>, usize)
where
F: Fn(&[f64]) -> Vec<f64>,
{
let n_vars = bounds.len();
let mut rng = StdRng::seed_from_u64(seed);
let mut n_eval = 0usize;
let ref_pts = gen_reference_points(n_obj, n_div);
let mut pop: Vec<Vec<f64>> = (0..pop_size)
.map(|_| bounds.iter().map(|&(lb, ub)| lb + rng.random::<f64>() * (ub - lb)).collect())
.collect();
let mut obj_vals: Vec<Vec<f64>> = pop.iter().map(|x| { n_eval += 1; objectives(x) }).collect();
for _gen in 0..n_gen {
let mut offspring: Vec<Vec<f64>> = Vec::with_capacity(pop_size);
while offspring.len() < pop_size {
let p1 = rng.random_range(0..pop_size);
let p2 = rng.random_range(0..pop_size);
let (c1, c2) = if rng.random::<f64>() < cx_prob {
sbx_crossover(&pop[p1], &pop[p2], bounds, &mut rng, 20.0)
} else {
(pop[p1].clone(), pop[p2].clone())
};
let c1 = poly_mutation(c1, bounds, &mut rng, mut_prob, 20.0);
let c2 = poly_mutation(c2, bounds, &mut rng, mut_prob, 20.0);
offspring.push(c1);
if offspring.len() < pop_size {
offspring.push(c2);
}
}
let offspring_obj: Vec<Vec<f64>> =
offspring.iter().map(|x| { n_eval += 1; objectives(x) }).collect();
let combined: Vec<Vec<f64>> = pop.iter().chain(offspring.iter()).cloned().collect();
let combined_obj: Vec<Vec<f64>> =
obj_vals.iter().chain(offspring_obj.iter()).cloned().collect();
let fronts = non_dominated_sort(&combined_obj);
let mut new_pop: Vec<Vec<f64>> = Vec::with_capacity(pop_size);
let mut new_obj: Vec<Vec<f64>> = Vec::with_capacity(pop_size);
let mut critical_indices: Vec<usize> = Vec::new();
for front in &fronts {
if new_pop.len() + front.len() <= pop_size {
for &idx in front {
new_pop.push(combined[idx].clone());
new_obj.push(combined_obj[idx].clone());
}
} else {
critical_indices = front.clone();
break;
}
}
let remaining = pop_size.saturating_sub(new_pop.len());
if remaining > 0 && !critical_indices.is_empty() {
let selected = niche_select(
&critical_indices,
&combined_obj,
&new_obj,
&ref_pts,
remaining,
n_obj,
&mut rng,
);
for idx in selected {
new_pop.push(combined[idx].clone());
new_obj.push(combined_obj[idx].clone());
}
}
pop = new_pop;
obj_vals = new_obj;
}
let fronts = non_dominated_sort(&obj_vals);
let pareto_idx = fronts.into_iter().next().unwrap_or_default();
let dec: Vec<Vec<f64>> = pareto_idx.iter().map(|&i| pop[i].clone()).collect();
let obj: Vec<Vec<f64>> = pareto_idx.iter().map(|&i| obj_vals[i].clone()).collect();
(dec, obj, n_eval)
}
fn gen_reference_points(n_obj: usize, n_div: usize) -> Vec<Vec<f64>> {
let mut points = Vec::new();
let mut cur = vec![0usize; n_obj];
gen_ref_rec(n_obj, n_div, 0, n_div, &mut cur, &mut points);
points
}
fn gen_ref_rec(
n_obj: usize,
n_div: usize,
idx: usize,
rem: usize,
cur: &mut Vec<usize>,
points: &mut Vec<Vec<f64>>,
) {
if idx == n_obj - 1 {
cur[idx] = rem;
points.push(cur.iter().map(|&v| v as f64 / n_div.max(1) as f64).collect());
return;
}
for v in 0..=rem {
cur[idx] = v;
gen_ref_rec(n_obj, n_div, idx + 1, rem - v, cur, points);
}
}
#[allow(clippy::too_many_arguments)]
fn niche_select(
critical: &[usize],
all_obj: &[Vec<f64>],
selected_obj: &[Vec<f64>],
ref_pts: &[Vec<f64>],
n_select: usize,
n_obj: usize,
rng: &mut StdRng,
) -> Vec<usize> {
let n_ref = ref_pts.len();
if n_ref == 0 {
let mut idx = critical.to_vec();
idx.truncate(n_select);
return idx;
}
let all_selected_obj: Vec<&Vec<f64>> = selected_obj
.iter()
.chain(critical.iter().map(|&i| &all_obj[i]))
.collect();
let ideal: Vec<f64> = (0..n_obj)
.map(|j| all_selected_obj.iter().filter_map(|f| f.get(j).copied()).fold(f64::INFINITY, f64::min))
.collect();
let nadir: Vec<f64> = (0..n_obj)
.map(|j| all_selected_obj.iter().filter_map(|f| f.get(j).copied()).fold(f64::NEG_INFINITY, f64::max))
.collect();
let norm = |f: &[f64]| -> Vec<f64> {
(0..n_obj)
.map(|j| {
let range = (nadir[j] - ideal[j]).max(1e-10);
(f.get(j).copied().unwrap_or(0.0) - ideal[j]) / range
})
.collect()
};
let line_dist = |f_n: &[f64], rp: &[f64]| -> f64 {
let rp_norm: f64 = rp.iter().map(|&r| r * r).sum::<f64>().sqrt().max(1e-12);
let dot: f64 = f_n.iter().zip(rp).map(|(&fi, &ri)| fi * ri).sum();
let proj = dot / rp_norm;
let proj_pt: Vec<f64> = rp.iter().map(|&ri| proj * ri / rp_norm).collect();
f_n.iter().zip(&proj_pt).map(|(&fi, &pi)| (fi - pi).powi(2)).sum::<f64>().sqrt()
};
let mut niche_count = vec![0usize; n_ref];
for f in selected_obj {
let fn_ = norm(f);
let best = (0..n_ref)
.min_by(|&r1, &r2| line_dist(&fn_, &ref_pts[r1]).partial_cmp(&line_dist(&fn_, &ref_pts[r2])).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0);
niche_count[best] += 1;
}
let info: Vec<(usize, usize, f64)> = critical
.iter()
.map(|&idx| {
let fn_ = norm(&all_obj[idx]);
let (r, d) = (0..n_ref)
.map(|r| (r, line_dist(&fn_, &ref_pts[r])))
.min_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or((0, f64::INFINITY));
(idx, r, d)
})
.collect();
let mut remaining = info;
let mut selected = Vec::with_capacity(n_select);
while selected.len() < n_select && !remaining.is_empty() {
let min_nc = remaining.iter().map(|&(_, r, _)| niche_count[r]).min().unwrap_or(0);
let cands: Vec<usize> = remaining
.iter()
.enumerate()
.filter(|(_, &(_, r, _))| niche_count[r] == min_nc)
.map(|(i, _)| i)
.collect();
let chosen = if min_nc == 0 {
cands[rng.random_range(0..cands.len())]
} else {
*cands.iter()
.min_by(|&&a, &&b| remaining[a].2.partial_cmp(&remaining[b].2).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(&cands[0])
};
let (sol, ref_idx, _) = remaining.remove(chosen);
selected.push(sol);
niche_count[ref_idx] += 1;
}
selected
}
fn sbx_crossover(
p1: &[f64],
p2: &[f64],
bounds: &[(f64, f64)],
rng: &mut StdRng,
eta: f64,
) -> (Vec<f64>, Vec<f64>) {
let mut c1 = p1.to_vec();
let mut c2 = p2.to_vec();
for i in 0..p1.len() {
let (lb, ub) = bounds[i];
if rng.random::<f64>() > 0.5 {
continue;
}
let u: f64 = rng.random();
let beta: f64 = if u <= 0.5 {
(2.0_f64 * u).powf(1.0_f64 / (eta + 1.0))
} else {
(1.0_f64 / (2.0_f64 * (1.0 - u))).powf(1.0_f64 / (eta + 1.0))
};
c1[i] = (0.5 * ((1.0 + beta) * p1[i] + (1.0 - beta) * p2[i])).clamp(lb, ub);
c2[i] = (0.5 * ((1.0 - beta) * p1[i] + (1.0 + beta) * p2[i])).clamp(lb, ub);
}
(c1, c2)
}
fn poly_mutation(
mut x: Vec<f64>,
bounds: &[(f64, f64)],
rng: &mut StdRng,
prob: f64,
eta: f64,
) -> Vec<f64> {
for i in 0..x.len() {
if rng.random::<f64>() >= prob {
continue;
}
let (lb, ub) = bounds[i];
let range = (ub - lb).max(1e-12);
let d1 = (x[i] - lb) / range;
let d2 = (ub - x[i]) / range;
let u: f64 = rng.random();
let dq = if u <= 0.5 {
let v = 2.0 * u + (1.0 - 2.0 * u) * (1.0 - d1).powf(eta + 1.0);
v.powf(1.0 / (eta + 1.0)) - 1.0
} else {
let v = 2.0 * (1.0 - u) + 2.0 * (u - 0.5) * (1.0 - d2).powf(eta + 1.0);
1.0 - v.powf(1.0 / (eta + 1.0))
};
x[i] = (x[i] + dq * range).clamp(lb, ub);
}
x
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_dominates_basic() {
assert!(dominates(&[1.0, 2.0], &[2.0, 3.0]));
assert!(!dominates(&[1.0, 2.0], &[1.0, 2.0]));
assert!(!dominates(&[2.0, 1.0], &[1.0, 2.0]));
}
#[test]
fn test_non_dominated_sort_basic() {
let sols = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0], vec![4.0, 4.0]];
let fronts = non_dominated_sort(&sols);
assert_eq!(fronts[0].len(), 3);
assert!(fronts[1].contains(&3));
}
#[test]
fn test_hypervolume_2d() {
let front = vec![vec![1.0, 2.0], vec![2.0, 1.0]];
let ref_pt = vec![3.0, 3.0];
let hv = hypervolume(&front, &ref_pt);
assert_abs_diff_eq!(hv, 3.0, epsilon = 1e-10);
}
#[test]
fn test_hypervolume_empty() {
assert_eq!(hypervolume(&[], &[3.0, 3.0]), 0.0);
}
#[test]
fn test_crowding_distance_boundary() {
let front = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
let d = crowding_distance(&front);
assert_eq!(d[0], f64::INFINITY);
assert_eq!(d[2], f64::INFINITY);
assert!(d[1].is_finite());
}
#[test]
fn test_nsga3_basic() {
let mut nsga = NsgaIII::new(2);
nsga.population_size = 10;
nsga.n_generations = 3;
nsga.n_divisions = 2;
let bounds: Vec<(f64, f64)> = vec![(0.0, 1.0); 2];
let result = nsga.optimize(|x| vec![x[0], 1.0 - x[0].sqrt()], &bounds).expect("failed to create result");
assert!(!result.pareto_front.is_empty());
assert!(result.n_evaluations > 0);
}
#[test]
fn test_nsga3_empty_bounds_err() {
let nsga = NsgaIII::new(2);
assert!(nsga.optimize(|x| vec![x[0]], &[]).is_err());
}
#[test]
fn test_weighted_sum_single() {
let (x, f) = weighted_sum_optimize(
|x| vec![x[0].powi(2)],
&[1.0],
&[(0.0, 2.0)],
5,
0,
);
assert!(x[0] < 0.2, "x near 0, got {}", x[0]);
assert!(f[0] < 0.05, "f near 0, got {}", f[0]);
}
#[test]
fn test_epsilon_constraint_basic() {
let (x, f) = epsilon_constraint_optimize(
|x| vec![x[0], x[1]],
0,
&[(0.3, 0.7)],
&[(0.0, 1.0), (0.0, 1.0)],
5,
42,
);
assert!(x[0] < 0.2, "x[0] near 0, got {}", x[0]);
assert!(f[1] >= 0.2 && f[1] <= 0.8, "f[1] in bounds, got {}", f[1]);
}
#[test]
fn test_hypervolume_3d_positive() {
let front = vec![
vec![0.1, 0.5, 0.9],
vec![0.5, 0.1, 0.9],
vec![0.9, 0.9, 0.1],
];
let ref_pt = vec![1.0, 1.0, 1.0];
let hv = hypervolume(&front, &ref_pt);
assert!(hv > 0.0, "3D HV should be positive, got {}", hv);
}
}