#![allow(non_snake_case)]
use scirs2_core::ndarray::{s, Array1, Array2, ArrayView2};
use scirs2_core::random::thread_rng;
use scirs2_core::random::RandNormal;
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Estimator, Fit, Untrained},
types::Float,
};
#[derive(Debug, Clone, PartialEq)]
pub enum ScalarizationMethod {
WeightedSum(Vec<Float>),
EpsilonConstraint {
objective_index: usize,
epsilon_values: Vec<Float>,
},
AchievementScalarizingFunction {
reference_point: Vec<Float>,
augmentation_coefficient: Float,
},
AugmentedWeightedTchebycheff {
reference_point: Vec<Float>,
weights: Vec<Float>,
augmentation_coefficient: Float,
},
NormalizedNormalConstraint {
anchor_points: Array2<Float>,
utopia_point: Vec<Float>,
},
}
#[derive(Debug, Clone)]
pub struct ScalarizationConfig {
pub method: ScalarizationMethod,
pub max_iter: usize,
pub tol: Float,
pub learning_rate: Float,
pub random_state: Option<u64>,
}
impl Default for ScalarizationConfig {
fn default() -> Self {
Self {
method: ScalarizationMethod::WeightedSum(vec![0.5, 0.5]),
max_iter: 1000,
tol: 1e-6,
learning_rate: 0.01,
random_state: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ScalarizationOptimizer<S = Untrained> {
state: S,
config: ScalarizationConfig,
}
#[derive(Debug, Clone)]
pub struct ScalarizationOptimizerTrained {
pub parameters: Array1<Float>,
pub scalarized_value: Float,
pub objective_values: Vec<Float>,
pub convergence_history: Vec<Float>,
pub n_features: usize,
pub n_objectives: usize,
pub config: ScalarizationConfig,
}
impl ScalarizationOptimizer<Untrained> {
pub fn new(method: ScalarizationMethod) -> Self {
Self {
state: Untrained,
config: ScalarizationConfig {
method,
..Default::default()
},
}
}
pub fn config(mut self, config: ScalarizationConfig) -> Self {
self.config = config;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.config.max_iter = max_iter;
self
}
pub fn tol(mut self, tol: Float) -> Self {
self.config.tol = tol;
self
}
pub fn learning_rate(mut self, learning_rate: Float) -> Self {
self.config.learning_rate = learning_rate;
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.config.random_state = random_state;
self
}
pub fn scalarize_objectives(&self, objectives: &[Float]) -> SklResult<Float> {
match &self.config.method {
ScalarizationMethod::WeightedSum(weights) => {
if weights.len() != objectives.len() {
return Err(SklearsError::InvalidInput(
"Weight vector length must match number of objectives".to_string(),
));
}
Ok(objectives
.iter()
.zip(weights.iter())
.map(|(obj, w)| obj * w)
.sum())
}
ScalarizationMethod::EpsilonConstraint {
objective_index,
epsilon_values,
} => {
if *objective_index >= objectives.len() {
return Err(SklearsError::InvalidInput(
"Objective index out of bounds".to_string(),
));
}
if epsilon_values.len() != objectives.len() - 1 {
return Err(SklearsError::InvalidInput(
"Epsilon values length must be objectives - 1".to_string(),
));
}
let mut eps_idx = 0;
for (i, &obj_val) in objectives.iter().enumerate() {
if i != *objective_index {
if obj_val > epsilon_values[eps_idx] {
return Ok(Float::INFINITY); }
eps_idx += 1;
}
}
Ok(objectives[*objective_index])
}
ScalarizationMethod::AchievementScalarizingFunction {
reference_point,
augmentation_coefficient,
} => {
if reference_point.len() != objectives.len() {
return Err(SklearsError::InvalidInput(
"Reference point length must match number of objectives".to_string(),
));
}
let max_normalized_diff = objectives
.iter()
.zip(reference_point.iter())
.map(|(obj, ref_pt)| (obj - ref_pt).max(0.0))
.fold(0.0, Float::max);
let augmentation_term = augmentation_coefficient
* objectives
.iter()
.zip(reference_point.iter())
.map(|(obj, ref_pt)| obj - ref_pt)
.sum::<Float>();
Ok(max_normalized_diff + augmentation_term)
}
ScalarizationMethod::AugmentedWeightedTchebycheff {
reference_point,
weights,
augmentation_coefficient,
} => {
if reference_point.len() != objectives.len() || weights.len() != objectives.len() {
return Err(SklearsError::InvalidInput(
"Reference point and weights length must match number of objectives"
.to_string(),
));
}
let max_weighted_diff = objectives
.iter()
.zip(reference_point.iter())
.zip(weights.iter())
.map(|((obj, ref_pt), w)| w * (obj - ref_pt).abs())
.fold(0.0, Float::max);
let augmentation_term = augmentation_coefficient
* objectives
.iter()
.zip(reference_point.iter())
.map(|(obj, ref_pt)| obj - ref_pt)
.sum::<Float>();
Ok(max_weighted_diff + augmentation_term)
}
ScalarizationMethod::NormalizedNormalConstraint {
anchor_points,
utopia_point,
} => {
if utopia_point.len() != objectives.len() {
return Err(SklearsError::InvalidInput(
"Utopia point length must match number of objectives".to_string(),
));
}
let normalized_objectives: Vec<Float> = objectives
.iter()
.zip(utopia_point.iter())
.enumerate()
.map(|(i, (obj, utopia))| {
let anchor = anchor_points[[i, i]];
if (anchor - utopia).abs() > 1e-10 {
(obj - utopia) / (anchor - utopia)
} else {
0.0
}
})
.collect();
Ok(normalized_objectives
.iter()
.map(|x| x * x)
.sum::<Float>()
.sqrt())
}
}
}
pub fn generate_scalarized_problems(
&self,
n_problems: usize,
n_objectives: usize,
) -> SklResult<Vec<ScalarizationMethod>> {
let mut problems = Vec::new();
match &self.config.method {
ScalarizationMethod::WeightedSum(_) => {
for i in 0..n_problems {
let mut weights = vec![0.0; n_objectives];
let step = 1.0 / (n_problems - 1) as Float;
weights[0] = i as Float * step;
weights[1] = 1.0 - weights[0];
if n_objectives > 2 {
let remaining = weights[1];
weights[1] = remaining * (i as Float / n_problems as Float);
for weight in weights[2..].iter_mut() {
*weight = remaining / (n_objectives - 1) as Float;
}
}
problems.push(ScalarizationMethod::WeightedSum(weights));
}
}
ScalarizationMethod::EpsilonConstraint {
objective_index, ..
} => {
for i in 0..n_problems {
let step = 1.0 / n_problems as Float;
let epsilon_values = (0..n_objectives - 1)
.map(|_| (i as Float + 1.0) * step)
.collect();
problems.push(ScalarizationMethod::EpsilonConstraint {
objective_index: *objective_index,
epsilon_values,
});
}
}
_ => {
for _ in 0..n_problems {
problems.push(self.config.method.clone());
}
}
}
Ok(problems)
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, Float>> for ScalarizationOptimizer<Untrained> {
type Fitted = ScalarizationOptimizer<ScalarizationOptimizerTrained>;
fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView2<'_, Float>) -> SklResult<Self::Fitted> {
let (n_samples, n_features) = X.dim();
let (y_samples, n_objectives) = y.dim();
if n_samples != y_samples {
return Err(SklearsError::InvalidInput(
"X and y must have the same number of samples".to_string(),
));
}
let mut rng = thread_rng();
let normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
let mut parameters = Array1::<Float>::zeros(n_features * n_objectives);
for i in 0..(n_features * n_objectives) {
parameters[i] = rng.sample(normal_dist);
}
let mut convergence_history = Vec::new();
let mut prev_scalarized_value = Float::INFINITY;
for _iteration in 0..self.config.max_iter {
let objectives: Vec<Float> = (0..n_objectives)
.map(|i| {
let param_slice = parameters.slice(s![i * n_features..(i + 1) * n_features]);
param_slice.iter().map(|x| x * x).sum::<Float>() / n_features as Float
})
.collect();
let scalarized_value = self.scalarize_objectives(&objectives)?;
convergence_history.push(scalarized_value);
if (prev_scalarized_value - scalarized_value).abs() < self.config.tol {
break;
}
prev_scalarized_value = scalarized_value;
for i in 0..parameters.len() {
let gradient = 2.0 * parameters[i] / n_features as Float; parameters[i] -= self.config.learning_rate * gradient;
}
}
let final_objectives: Vec<Float> = (0..n_objectives)
.map(|i| {
let param_slice = parameters.slice(s![i * n_features..(i + 1) * n_features]);
param_slice.iter().map(|x| x * x).sum::<Float>() / n_features as Float
})
.collect();
let final_scalarized_value = self.scalarize_objectives(&final_objectives)?;
Ok(ScalarizationOptimizer {
state: ScalarizationOptimizerTrained {
parameters,
scalarized_value: final_scalarized_value,
objective_values: final_objectives,
convergence_history,
n_features,
n_objectives,
config: self.config.clone(),
},
config: self.config,
})
}
}
impl ScalarizationOptimizer<ScalarizationOptimizerTrained> {
pub fn parameters(&self) -> &Array1<Float> {
&self.state.parameters
}
pub fn scalarized_value(&self) -> Float {
self.state.scalarized_value
}
pub fn objective_values(&self) -> &[Float] {
&self.state.objective_values
}
pub fn convergence_history(&self) -> &[Float] {
&self.state.convergence_history
}
pub fn method(&self) -> &ScalarizationMethod {
&self.state.config.method
}
pub fn is_feasible(&self) -> bool {
match &self.state.config.method {
ScalarizationMethod::EpsilonConstraint {
epsilon_values,
objective_index,
} => {
let mut eps_idx = 0;
for (i, &obj_val) in self.state.objective_values.iter().enumerate() {
if i != *objective_index {
if obj_val > epsilon_values[eps_idx] {
return false;
}
eps_idx += 1;
}
}
true
}
_ => true, }
}
}
impl Estimator for ScalarizationOptimizer<Untrained> {
type Config = ScalarizationConfig;
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&self.config
}
}
impl Estimator for ScalarizationOptimizer<ScalarizationOptimizerTrained> {
type Config = ScalarizationConfig;
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&self.state.config
}
}