use super::functions::*;
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::rand_prelude::SliceRandom;
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::RngExt;
use scirs2_core::random::SeedableRng;
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Trained, Untrained},
};
use std::marker::PhantomData;
pub type MultiObjectiveResult = SklResult<(Vec<Individual>, Vec<Vec<usize>>, Array2<f64>)>;
pub trait CrossValidator {
fn split(&self, n_samples: usize) -> Vec<(Vec<usize>, Vec<usize>)>;
}
#[derive(Debug, Clone)]
pub struct GeneticSelector<E, State = Untrained> {
pub(crate) estimator: E,
pub(crate) population_size: usize,
pub(crate) generations: usize,
pub(crate) crossover_rate: f64,
pub(crate) mutation_rate: f64,
pub(crate) tournament_size: usize,
pub(crate) min_features: usize,
pub(crate) max_features: Option<usize>,
pub(crate) cv_folds: usize,
pub(crate) random_state: Option<u64>,
pub(crate) state: PhantomData<State>,
pub(crate) best_features_: Option<Vec<usize>>,
pub(crate) best_score_: Option<f64>,
pub(crate) n_features_: Option<usize>,
}
impl<E: Clone> GeneticSelector<E, Untrained> {
pub fn new(estimator: E) -> Self {
Self {
estimator,
population_size: 50,
generations: 100,
crossover_rate: 0.8,
mutation_rate: 0.1,
tournament_size: 3,
min_features: 1,
max_features: None,
cv_folds: 5,
random_state: None,
state: PhantomData,
best_features_: None,
best_score_: None,
n_features_: None,
}
}
pub fn population_size(mut self, size: usize) -> Result<Self, SklearsError> {
if size < 2 {
return Err(SklearsError::InvalidInput(
"population_size must be at least 2".to_string(),
));
}
self.population_size = size;
Ok(self)
}
pub fn generations(mut self, generations: usize) -> Self {
self.generations = generations;
self
}
pub fn crossover_rate(mut self, rate: f64) -> Result<Self, SklearsError> {
if !(0.0..=1.0).contains(&rate) {
return Err(SklearsError::InvalidInput(
"crossover_rate must be between 0 and 1".to_string(),
));
}
self.crossover_rate = rate;
Ok(self)
}
pub fn mutation_rate(mut self, rate: f64) -> Result<Self, SklearsError> {
if !(0.0..=1.0).contains(&rate) {
return Err(SklearsError::InvalidInput(
"mutation_rate must be between 0 and 1".to_string(),
));
}
self.mutation_rate = rate;
Ok(self)
}
pub fn tournament_size(mut self, size: usize) -> Result<Self, SklearsError> {
if size < 1 {
return Err(SklearsError::InvalidInput(
"tournament_size must be at least 1".to_string(),
));
}
self.tournament_size = size;
Ok(self)
}
pub fn min_features(mut self, min: usize) -> Result<Self, SklearsError> {
if min < 1 {
return Err(SklearsError::InvalidInput(
"min_features must be at least 1".to_string(),
));
}
self.min_features = min;
Ok(self)
}
pub fn max_features(mut self, max: Option<usize>) -> Self {
self.max_features = max;
self
}
pub fn cv_folds(mut self, folds: usize) -> Result<Self, SklearsError> {
if folds < 2 {
return Err(SklearsError::InvalidInput(
"cv_folds must be at least 2".to_string(),
));
}
self.cv_folds = folds;
Ok(self)
}
pub fn random_state(mut self, seed: u64) -> Self {
self.random_state = Some(seed);
self
}
}
impl<E> GeneticSelector<E, Trained> {
pub fn best_features(&self) -> SklResult<&[usize]> {
self.best_features_.as_deref().ok_or_else(|| {
SklearsError::FitError("GeneticSelector not fitted: best_features_ missing".to_string())
})
}
pub fn best_score(&self) -> SklResult<f64> {
self.best_score_.ok_or_else(|| {
SklearsError::FitError("GeneticSelector not fitted: best_score_ missing".to_string())
})
}
}
#[derive(Debug, Clone)]
pub struct FeatureImportanceObjective {
pub(crate) importance_scores: Array1<f64>,
}
impl FeatureImportanceObjective {
pub fn new(importance_scores: Array1<f64>) -> Self {
Self { importance_scores }
}
}
#[derive(Debug, Clone)]
pub struct CostSensitiveObjective {
pub(crate) feature_costs: Array1<f64>,
pub(crate) cost_weight: f64,
}
impl CostSensitiveObjective {
pub fn new(feature_costs: Array1<f64>, cost_weight: f64) -> Self {
Self {
feature_costs,
cost_weight: cost_weight.clamp(0.0, 1.0),
}
}
}
#[derive(Debug, Clone)]
pub enum FairnessMetric {
DemographicParity,
EqualOpportunity,
EqualizedOdds,
StatisticalParityDifference,
}
#[derive(Debug, Clone)]
pub struct FeatureDiversityObjective;
#[derive(Debug, Clone)]
pub struct MultiObjectiveFeatureSelector<State = Untrained> {
pub(crate) objectives: Vec<Box<dyn ObjectiveFunction>>,
pub(crate) optimization_method: MultiObjectiveMethod,
pub(crate) population_size: usize,
pub(crate) n_generations: usize,
pub(crate) crossover_prob: f64,
pub(crate) mutation_prob: f64,
pub(crate) random_state: Option<u64>,
pub(crate) state: PhantomData<State>,
pub(crate) pareto_front_: Option<Vec<Individual>>,
pub(crate) best_solutions_: Option<Vec<Vec<usize>>>,
pub(crate) objective_values_: Option<Array2<f64>>,
pub(crate) n_features_: Option<usize>,
}
impl MultiObjectiveFeatureSelector<Untrained> {
pub fn new() -> Self {
Self {
objectives: Vec::new(),
optimization_method: MultiObjectiveMethod::NSGAII,
population_size: 100,
n_generations: 50,
crossover_prob: 0.8,
mutation_prob: 0.1,
random_state: None,
state: PhantomData,
pareto_front_: None,
best_solutions_: None,
objective_values_: None,
n_features_: None,
}
}
pub fn add_objective(mut self, objective: Box<dyn ObjectiveFunction>) -> Self {
self.objectives.push(objective);
self
}
pub fn optimization_method(mut self, method: MultiObjectiveMethod) -> Self {
self.optimization_method = method;
self
}
pub fn population_size(mut self, size: usize) -> Self {
self.population_size = size;
self
}
pub fn n_generations(mut self, generations: usize) -> Self {
self.n_generations = generations;
self
}
pub fn crossover_prob(mut self, prob: f64) -> Self {
self.crossover_prob = prob;
self
}
pub fn mutation_prob(mut self, prob: f64) -> Self {
self.mutation_prob = prob;
self
}
pub fn random_state(mut self, seed: u64) -> Self {
self.random_state = Some(seed);
self
}
pub fn add_standard_objectives(mut self, importance_scores: Option<Array1<f64>>) -> Self {
self.objectives.push(Box::new(FeatureCountObjective));
self.objectives
.push(Box::new(PredictivePerformanceObjective));
self.objectives.push(Box::new(FeatureDiversityObjective));
if let Some(scores) = importance_scores {
self.objectives
.push(Box::new(FeatureImportanceObjective::new(scores)));
}
self
}
}
impl MultiObjectiveFeatureSelector<Untrained> {
pub(crate) fn optimize(
&self,
x: &Array2<f64>,
y_classif: Option<&Array1<i32>>,
y_regression: Option<&Array1<f64>>,
n_features: usize,
) -> MultiObjectiveResult {
match &self.optimization_method {
MultiObjectiveMethod::NSGAII => self.nsga_ii(x, y_classif, y_regression, n_features),
MultiObjectiveMethod::WeightedSum(weights) => {
self.weighted_sum(x, y_classif, y_regression, n_features, weights)
}
_ => self.nsga_ii(x, y_classif, y_regression, n_features),
}
}
fn nsga_ii(
&self,
x: &Array2<f64>,
y_classif: Option<&Array1<i32>>,
y_regression: Option<&Array1<f64>>,
n_features: usize,
) -> MultiObjectiveResult {
let mut rng = if let Some(seed) = self.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::seed_from_u64(42)
};
let mut population = self.initialize_population(n_features, &mut rng);
for individual in &mut population {
individual.objectives =
self.evaluate_objectives(x, y_classif, y_regression, &individual.features)?;
}
for _generation in 0..self.n_generations {
let mut offspring = self.create_offspring(&population, n_features, &mut rng)?;
for individual in &mut offspring {
individual.objectives =
self.evaluate_objectives(x, y_classif, y_regression, &individual.features)?;
}
let mut combined = population;
combined.extend(offspring);
population = self.select_next_generation(combined, self.population_size);
}
let pareto_front = self.extract_pareto_front(&population);
let best_solutions: Vec<Vec<usize>> = pareto_front
.iter()
.map(|ind| {
ind.features
.iter()
.enumerate()
.filter_map(|(i, &selected)| if selected { Some(i) } else { None })
.collect()
})
.collect();
let n_objectives = self.objectives.len();
let mut objective_values = Array2::<f64>::zeros((pareto_front.len(), n_objectives));
for (i, individual) in pareto_front.iter().enumerate() {
for (j, &value) in individual.objectives.iter().enumerate() {
objective_values[[i, j]] = value;
}
}
Ok((pareto_front, best_solutions, objective_values))
}
fn weighted_sum(
&self,
x: &Array2<f64>,
y_classif: Option<&Array1<i32>>,
y_regression: Option<&Array1<f64>>,
n_features: usize,
weights: &[f64],
) -> MultiObjectiveResult {
if weights.len() != self.objectives.len() {
return Err(SklearsError::InvalidInput(
"Number of weights must match number of objectives".to_string(),
));
}
let mut rng = if let Some(seed) = self.random_state {
StdRng::seed_from_u64(seed)
} else {
StdRng::seed_from_u64(42)
};
let mut population = self.initialize_population(n_features, &mut rng);
let mut best_individual = None;
let mut best_score = f64::NEG_INFINITY;
for individual in &mut population {
individual.objectives =
self.evaluate_objectives(x, y_classif, y_regression, &individual.features)?;
let weighted_score: f64 = individual
.objectives
.iter()
.zip(weights.iter())
.map(|(&obj, &weight)| obj * weight)
.sum();
if weighted_score > best_score {
best_score = weighted_score;
best_individual = Some(individual.clone());
}
}
let best = best_individual.expect("operation should succeed");
let best_features: Vec<usize> = best
.features
.iter()
.enumerate()
.filter_map(|(i, &selected)| if selected { Some(i) } else { None })
.collect();
let mut objective_values = Array2::<f64>::zeros((1, self.objectives.len()));
for (j, &value) in best.objectives.iter().enumerate() {
objective_values[[0, j]] = value;
}
Ok((vec![best], vec![best_features], objective_values))
}
fn initialize_population(&self, n_features: usize, rng: &mut StdRng) -> Vec<Individual> {
let mut population = Vec::with_capacity(self.population_size);
for _ in 0..self.population_size {
let mut features = vec![false; n_features];
let n_selected = rng.random_range((n_features / 10).max(1)..=(n_features / 2).max(1));
let mut indices: Vec<usize> = (0..n_features).collect();
indices.shuffle(rng);
for &idx in indices.iter().take(n_selected) {
features[idx] = true;
}
population.push(Individual {
features,
objectives: Vec::new(),
rank: None,
crowding_distance: None,
});
}
population
}
fn evaluate_objectives(
&self,
x: &Array2<f64>,
y_classif: Option<&Array1<i32>>,
y_regression: Option<&Array1<f64>>,
feature_mask: &[bool],
) -> SklResult<Vec<f64>> {
let mut objectives = Vec::with_capacity(self.objectives.len());
for objective in &self.objectives {
let mut value = objective.evaluate(x, y_classif, y_regression, feature_mask)?;
if objective.is_minimization() {
value = -value;
}
objectives.push(value);
}
Ok(objectives)
}
fn create_offspring(
&self,
population: &[Individual],
_n_features: usize,
rng: &mut StdRng,
) -> SklResult<Vec<Individual>> {
let mut offspring = Vec::with_capacity(self.population_size);
for _ in 0..self.population_size {
let parent1 = self.tournament_selection(population, rng);
let parent2 = self.tournament_selection(population, rng);
let mut child = if rng.random::<f64>() < self.crossover_prob {
self.uniform_crossover(&parent1.features, &parent2.features, rng)
} else {
parent1.features.clone()
};
if rng.random::<f64>() < self.mutation_prob {
self.bit_flip_mutation(&mut child, rng);
}
offspring.push(Individual {
features: child,
objectives: Vec::new(),
rank: None,
crowding_distance: None,
});
}
Ok(offspring)
}
fn tournament_selection<'a>(
&self,
population: &'a [Individual],
rng: &mut StdRng,
) -> &'a Individual {
let tournament_size = 3;
let mut best = &population[rng.random_range(0..population.len())];
for _ in 1..tournament_size {
let candidate = &population[rng.random_range(0..population.len())];
if self.dominates(&candidate.objectives, &best.objectives) {
best = candidate;
}
}
best
}
fn uniform_crossover(&self, parent1: &[bool], parent2: &[bool], rng: &mut StdRng) -> Vec<bool> {
parent1
.iter()
.zip(parent2.iter())
.map(|(&p1, &p2)| if rng.random::<f64>() < 0.5 { p1 } else { p2 })
.collect()
}
fn bit_flip_mutation(&self, individual: &mut [bool], rng: &mut StdRng) {
for gene in individual.iter_mut() {
if rng.random::<f64>() < 0.1 {
*gene = !*gene;
}
}
}
fn dominates(&self, objectives1: &[f64], objectives2: &[f64]) -> bool {
let at_least_one_better = objectives1
.iter()
.zip(objectives2.iter())
.any(|(&a, &b)| a > b);
let all_as_good = objectives1
.iter()
.zip(objectives2.iter())
.all(|(&a, &b)| a >= b);
at_least_one_better && all_as_good
}
fn select_next_generation(
&self,
mut population: Vec<Individual>,
target_size: usize,
) -> Vec<Individual> {
let fronts = self.non_dominated_sort(&mut population);
let mut next_generation = Vec::new();
for front in fronts {
if next_generation.len() + front.len() <= target_size {
next_generation.extend(front);
} else {
let remaining = target_size - next_generation.len();
let mut front = front;
front.sort_by(|a, b| {
b.crowding_distance
.unwrap_or(0.0)
.partial_cmp(&a.crowding_distance.unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
});
next_generation.extend(front.into_iter().take(remaining));
break;
}
}
next_generation
}
fn non_dominated_sort(&self, population: &mut [Individual]) -> Vec<Vec<Individual>> {
let mut fronts = Vec::new();
let mut current_front: Vec<Individual> = Vec::new();
let mut remaining: Vec<Individual> = population.to_vec();
while !remaining.is_empty() {
current_front.clear();
let mut next_remaining = Vec::new();
for individual in remaining {
let mut is_dominated = false;
for other in ¤t_front {
if self.dominates(&other.objectives, &individual.objectives) {
is_dominated = true;
break;
}
}
if !is_dominated {
current_front
.retain(|other| !self.dominates(&individual.objectives, &other.objectives));
current_front.push(individual);
} else {
next_remaining.push(individual);
}
}
fronts.push(current_front.clone());
remaining = next_remaining;
}
fronts
}
fn extract_pareto_front(&self, population: &[Individual]) -> Vec<Individual> {
let mut pareto_front = Vec::new();
for individual in population {
let mut is_dominated = false;
for other in population {
if self.dominates(&other.objectives, &individual.objectives) {
is_dominated = true;
break;
}
}
if !is_dominated {
pareto_front.push(individual.clone());
}
}
pareto_front
}
}
impl MultiObjectiveFeatureSelector<Trained> {
pub fn pareto_front(&self) -> &[Individual] {
self.pareto_front_.as_deref().unwrap_or(&[])
}
pub fn pareto_optimal_features(&self) -> &[Vec<usize>] {
self.best_solutions_.as_deref().unwrap_or(&[])
}
pub fn objective_values(&self) -> &Array2<f64> {
self.objective_values_
.as_ref()
.expect("operation should succeed")
}
pub fn select_solution(&self, index: usize) -> Option<&[usize]> {
self.best_solutions_
.as_ref()
.expect("operation should succeed")
.get(index)
.map(|v| v.as_slice())
}
pub fn select_by_preference(&self, weights: &[f64]) -> SklResult<&[usize]> {
if weights.len() != self.objectives.len() {
return Err(SklearsError::InvalidInput(
"Number of weights must match number of objectives".to_string(),
));
}
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let mut best_score = f64::NEG_INFINITY;
let mut best_index = 0;
for i in 0..objective_values.nrows() {
let score: f64 = (0..objective_values.ncols())
.map(|j| objective_values[[i, j]] * weights[j])
.sum();
if score > best_score {
best_score = score;
best_index = i;
}
}
Ok(self
.best_solutions_
.as_ref()
.expect("operation should succeed")[best_index]
.as_slice())
}
pub fn balanced_solution(&self) -> SklResult<&[usize]> {
let n_objectives = self.objectives.len();
let weights = vec![1.0 / n_objectives as f64; n_objectives];
self.select_by_preference(&weights)
}
pub fn select_cost_optimal(&self, performance_threshold: f64) -> SklResult<&[usize]> {
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let solutions = self
.best_solutions_
.as_ref()
.expect("operation should succeed");
let mut cost_idx = None;
let mut perf_idx = None;
for (i, obj) in self.objectives.iter().enumerate() {
if obj.name() == "cost_sensitive" {
cost_idx = Some(i);
} else if obj.name() == "predictive_performance" {
perf_idx = Some(i);
}
}
let mut best_cost = f64::INFINITY;
let mut best_solution_idx = 0;
for i in 0..objective_values.nrows() {
let performance = if let Some(perf_idx) = perf_idx {
objective_values[[i, perf_idx]]
} else {
(0..objective_values.ncols())
.map(|j| objective_values[[i, j]])
.sum::<f64>()
/ objective_values.ncols() as f64
};
if performance >= performance_threshold {
let cost = if let Some(cost_idx) = cost_idx {
1.0 - objective_values[[i, cost_idx]]
} else {
solutions[i].len() as f64
};
if cost < best_cost {
best_cost = cost;
best_solution_idx = i;
}
}
}
if best_cost == f64::INFINITY {
return Err(SklearsError::InvalidInput(
"No solution meets the performance threshold".to_string(),
));
}
Ok(&solutions[best_solution_idx])
}
pub fn select_cost_efficient(&self) -> SklResult<&[usize]> {
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let solutions = self
.best_solutions_
.as_ref()
.expect("operation should succeed");
let mut cost_idx = None;
let mut perf_idx = None;
for (i, obj) in self.objectives.iter().enumerate() {
if obj.name() == "cost_sensitive" {
cost_idx = Some(i);
} else if obj.name() == "predictive_performance" {
perf_idx = Some(i);
}
}
let mut best_ratio = f64::NEG_INFINITY;
let mut best_solution_idx = 0;
for i in 0..objective_values.nrows() {
let performance = if let Some(perf_idx) = perf_idx {
objective_values[[i, perf_idx]]
} else {
(0..objective_values.ncols())
.map(|j| objective_values[[i, j]])
.sum::<f64>()
/ objective_values.ncols() as f64
};
let cost = if let Some(cost_idx) = cost_idx {
1.0 - objective_values[[i, cost_idx]]
} else {
solutions[i].len() as f64
};
let ratio = if cost > 0.0 {
performance / cost
} else {
f64::INFINITY
};
if ratio > best_ratio {
best_ratio = ratio;
best_solution_idx = i;
}
}
Ok(&solutions[best_solution_idx])
}
pub fn cost_breakdown(&self) -> SklResult<Vec<f64>> {
let solutions = self
.best_solutions_
.as_ref()
.expect("operation should succeed");
let mut costs = Vec::new();
let mut cost_objective = None;
for obj in &self.objectives {
if obj.name() == "cost_sensitive" {
cost_objective = Some(obj);
break;
}
}
if let Some(_cost_obj) = cost_objective {
for solution in solutions {
costs.push(solution.len() as f64);
}
} else {
for solution in solutions {
costs.push(solution.len() as f64);
}
}
Ok(costs)
}
pub fn select_fairness_optimal(&self) -> SklResult<&[usize]> {
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let solutions = self
.best_solutions_
.as_ref()
.expect("operation should succeed");
let mut fairness_idx = None;
for (i, obj) in self.objectives.iter().enumerate() {
if obj.name() == "fairness_aware" {
fairness_idx = Some(i);
break;
}
}
let mut best_fairness = f64::NEG_INFINITY;
let mut best_solution_idx = 0;
for i in 0..objective_values.nrows() {
let fairness = if let Some(fairness_idx) = fairness_idx {
objective_values[[i, fairness_idx]]
} else {
(0..objective_values.ncols())
.map(|j| objective_values[[i, j]])
.sum::<f64>()
/ objective_values.ncols() as f64
};
if fairness > best_fairness {
best_fairness = fairness;
best_solution_idx = i;
}
}
Ok(&solutions[best_solution_idx])
}
pub fn select_fair_balanced(&self, fairness_threshold: f64) -> SklResult<&[usize]> {
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let solutions = self
.best_solutions_
.as_ref()
.expect("operation should succeed");
let mut fairness_idx = None;
let mut perf_idx = None;
for (i, obj) in self.objectives.iter().enumerate() {
if obj.name() == "fairness_aware" {
fairness_idx = Some(i);
} else if obj.name() == "predictive_performance" {
perf_idx = Some(i);
}
}
let mut best_score = f64::NEG_INFINITY;
let mut best_solution_idx = 0;
for i in 0..objective_values.nrows() {
let fairness = if let Some(fairness_idx) = fairness_idx {
objective_values[[i, fairness_idx]]
} else {
(0..objective_values.ncols())
.map(|j| objective_values[[i, j]])
.sum::<f64>()
/ objective_values.ncols() as f64
};
if fairness >= fairness_threshold {
let performance = if let Some(perf_idx) = perf_idx {
objective_values[[i, perf_idx]]
} else {
(0..objective_values.ncols())
.map(|j| objective_values[[i, j]])
.sum::<f64>()
/ objective_values.ncols() as f64
};
let balanced_score = 0.5 * performance + 0.5 * fairness;
if balanced_score > best_score {
best_score = balanced_score;
best_solution_idx = i;
}
}
}
if best_score == f64::NEG_INFINITY {
return Err(SklearsError::InvalidInput(
"No solution meets the fairness threshold".to_string(),
));
}
Ok(&solutions[best_solution_idx])
}
pub fn fairness_metrics(&self) -> SklResult<Vec<f64>> {
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let mut fairness_values = Vec::new();
let mut fairness_idx = None;
for (i, obj) in self.objectives.iter().enumerate() {
if obj.name() == "fairness_aware" {
fairness_idx = Some(i);
break;
}
}
if let Some(fairness_idx) = fairness_idx {
for i in 0..objective_values.nrows() {
fairness_values.push(objective_values[[i, fairness_idx]]);
}
} else {
for i in 0..objective_values.nrows() {
let avg = (0..objective_values.ncols())
.map(|j| objective_values[[i, j]])
.sum::<f64>()
/ objective_values.ncols() as f64;
fairness_values.push(avg);
}
}
Ok(fairness_values)
}
pub fn hypervolume(&self, reference_point: &[f64]) -> SklResult<f64> {
if reference_point.len() != self.objectives.len() {
return Err(SklearsError::InvalidInput(
"Reference point must have same dimensions as objectives".to_string(),
));
}
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let mut hypervolume = 0.0;
if self.objectives.len() == 2 {
let mut points: Vec<(f64, f64)> = Vec::new();
for i in 0..objective_values.nrows() {
let x = objective_values[[i, 0]];
let y = objective_values[[i, 1]];
points.push((x, y));
}
points.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
let mut prev_y = reference_point[1];
for (x, y) in points {
if x > reference_point[0] && y > reference_point[1] {
hypervolume += (x - reference_point[0]) * (y - prev_y);
prev_y = y;
}
}
} else {
for i in 0..objective_values.nrows() {
let mut volume = 1.0;
for j in 0..objective_values.ncols() {
let diff = objective_values[[i, j]] - reference_point[j];
if diff > 0.0 {
volume *= diff;
} else {
volume = 0.0;
break;
}
}
hypervolume += volume;
}
}
Ok(hypervolume)
}
pub fn pareto_diversity(&self) -> SklResult<f64> {
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let n_solutions = objective_values.nrows();
if n_solutions < 2 {
return Ok(0.0);
}
let mut total_distance = 0.0;
let mut count = 0;
for i in 0..n_solutions {
for j in (i + 1)..n_solutions {
let mut distance = 0.0;
for k in 0..objective_values.ncols() {
let diff = objective_values[[i, k]] - objective_values[[j, k]];
distance += diff * diff;
}
total_distance += distance.sqrt();
count += 1;
}
}
Ok(total_distance / count as f64)
}
pub fn knee_point_solution(&self) -> SklResult<&[usize]> {
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let solutions = self
.best_solutions_
.as_ref()
.expect("operation should succeed");
if self.objectives.len() != 2 {
return Err(SklearsError::InvalidInput(
"Knee point detection only supported for 2-objective problems".to_string(),
));
}
let mut max_distance = f64::NEG_INFINITY;
let mut knee_idx = 0;
let mut min_obj1 = f64::INFINITY;
let mut max_obj1 = f64::NEG_INFINITY;
let mut min_obj2 = f64::INFINITY;
let mut max_obj2 = f64::NEG_INFINITY;
for i in 0..objective_values.nrows() {
let obj1 = objective_values[[i, 0]];
let obj2 = objective_values[[i, 1]];
min_obj1 = min_obj1.min(obj1);
max_obj1 = max_obj1.max(obj1);
min_obj2 = min_obj2.min(obj2);
max_obj2 = max_obj2.max(obj2);
}
for i in 0..objective_values.nrows() {
let obj1 = objective_values[[i, 0]];
let obj2 = objective_values[[i, 1]];
let norm_obj1 = (obj1 - min_obj1) / (max_obj1 - min_obj1);
let norm_obj2 = (obj2 - min_obj2) / (max_obj2 - min_obj2);
let distance = (norm_obj1 + norm_obj2 - 1.0).abs() / 2.0_f64.sqrt();
if distance > max_distance {
max_distance = distance;
knee_idx = i;
}
}
Ok(&solutions[knee_idx])
}
pub fn domination_analysis(&self) -> SklResult<Vec<usize>> {
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let n_solutions = objective_values.nrows();
let mut domination_counts = vec![0; n_solutions];
for i in 0..n_solutions {
for j in 0..n_solutions {
if i != j {
let mut dominates = true;
let mut strictly_better = false;
for k in 0..objective_values.ncols() {
let val_i = objective_values[[i, k]];
let val_j = objective_values[[j, k]];
if val_i < val_j {
dominates = false;
break;
} else if val_i > val_j {
strictly_better = true;
}
}
if dominates && strictly_better {
domination_counts[i] += 1;
}
}
}
}
Ok(domination_counts)
}
pub fn select_closest_to_ideal(&self, ideal_point: &[f64]) -> SklResult<&[usize]> {
if ideal_point.len() != self.objectives.len() {
return Err(SklearsError::InvalidInput(
"Ideal point must have same dimensions as objectives".to_string(),
));
}
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let solutions = self
.best_solutions_
.as_ref()
.expect("operation should succeed");
let mut min_distance = f64::INFINITY;
let mut best_solution_idx = 0;
for i in 0..objective_values.nrows() {
let mut distance = 0.0;
for j in 0..objective_values.ncols() {
let diff = objective_values[[i, j]] - ideal_point[j];
distance += diff * diff;
}
distance = distance.sqrt();
if distance < min_distance {
min_distance = distance;
best_solution_idx = i;
}
}
Ok(&solutions[best_solution_idx])
}
pub fn select_reference_point(&self, reference: &[f64]) -> SklResult<&[usize]> {
self.select_closest_to_ideal(reference)
}
pub fn filter_by_objectives(
&self,
min_values: &[f64],
max_values: &[f64],
) -> SklResult<Vec<usize>> {
if min_values.len() != self.objectives.len() || max_values.len() != self.objectives.len() {
return Err(SklearsError::InvalidInput(
"Min and max values must have same dimensions as objectives".to_string(),
));
}
let objective_values = self
.objective_values_
.as_ref()
.expect("operation should succeed");
let mut filtered_indices = Vec::new();
for i in 0..objective_values.nrows() {
let mut within_range = true;
for j in 0..objective_values.ncols() {
let value = objective_values[[i, j]];
if value < min_values[j] || value > max_values[j] {
within_range = false;
break;
}
}
if within_range {
filtered_indices.push(i);
}
}
Ok(filtered_indices)
}
}
#[derive(Debug, Clone)]
pub enum MultiObjectiveMethod {
NSGAII,
MOEAD,
PAES,
WeightedSum(Vec<f64>),
Lexicographic,
}
#[derive(Debug, Clone)]
pub struct PredictivePerformanceObjective;
#[derive(Debug, Clone)]
pub struct KFold {
pub(crate) n_splits: usize,
pub(crate) shuffle: bool,
}
impl KFold {
pub fn new(n_splits: usize) -> Self {
Self {
n_splits,
shuffle: false,
}
}
pub fn shuffle(mut self, shuffle: bool) -> Self {
self.shuffle = shuffle;
self
}
}
#[derive(Debug, Clone)]
pub struct Individual {
pub features: Vec<bool>,
pub objectives: Vec<f64>,
pub rank: Option<usize>,
pub crowding_distance: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct FairnessAwareObjective {
pub(crate) protected_groups: Array1<i32>,
pub(crate) fairness_weight: f64,
pub(crate) fairness_metric: FairnessMetric,
}
impl FairnessAwareObjective {
pub fn new(
protected_groups: Array1<i32>,
fairness_weight: f64,
fairness_metric: FairnessMetric,
) -> Self {
Self {
protected_groups,
fairness_weight: fairness_weight.clamp(0.0, 1.0),
fairness_metric,
}
}
}
#[derive(Debug, Clone)]
pub struct FeatureCountObjective;