sklears_multioutput/optimization/
scalarization_methods.rs

1//! Scalarization Methods for Multi-Objective Optimization
2//!
3//! This module provides various scalarization techniques for converting multi-objective
4//! optimization problems into single-objective problems. These methods enable the systematic
5//! exploration of trade-offs between conflicting objectives.
6//!
7//! ## Key Features
8//!
9//! - **Weighted Sum Method**: Simple linear combination of objectives with user-defined weights
10//! - **Epsilon-Constraint Method**: Optimize one objective while constraining others
11//! - **Achievement Scalarizing Function**: Reference point-based optimization
12//! - **Augmented Weighted Tchebycheff**: Improved Tchebycheff scalarization
13//! - **Normalized Normal Constraint**: Advanced constraint handling for Pareto front generation
14//! - **Problem Generation**: Systematic generation of scalarized subproblems
15
16// Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
17use scirs2_core::ndarray::{s, Array1, Array2, ArrayView2};
18use scirs2_core::random::thread_rng;
19use scirs2_core::random::RandNormal;
20use sklears_core::{
21    error::{Result as SklResult, SklearsError},
22    traits::{Estimator, Fit, Untrained},
23    types::Float,
24};
25
26/// Scalarization method types for multi-objective optimization
27#[derive(Debug, Clone, PartialEq)]
28pub enum ScalarizationMethod {
29    /// Weighted sum method with objective weights
30    WeightedSum(Vec<Float>),
31    /// Epsilon-constraint method with constraints and objective index
32    EpsilonConstraint {
33        objective_index: usize,
34        epsilon_values: Vec<Float>,
35    },
36    /// Achievement scalarizing function with reference point
37    AchievementScalarizingFunction {
38        reference_point: Vec<Float>,
39        augmentation_coefficient: Float,
40    },
41    /// Augmented weighted Tchebycheff method
42    AugmentedWeightedTchebycheff {
43        reference_point: Vec<Float>,
44        weights: Vec<Float>,
45        augmentation_coefficient: Float,
46    },
47    /// Normalized normal constraint method
48    NormalizedNormalConstraint {
49        anchor_points: Array2<Float>,
50        utopia_point: Vec<Float>,
51    },
52}
53
54/// Configuration for scalarization optimizer
55#[derive(Debug, Clone)]
56pub struct ScalarizationConfig {
57    /// Scalarization method to use
58    pub method: ScalarizationMethod,
59    /// Maximum number of iterations
60    pub max_iter: usize,
61    /// Convergence tolerance
62    pub tol: Float,
63    /// Learning rate for optimization
64    pub learning_rate: Float,
65    /// Random state for reproducibility
66    pub random_state: Option<u64>,
67}
68
69impl Default for ScalarizationConfig {
70    fn default() -> Self {
71        Self {
72            method: ScalarizationMethod::WeightedSum(vec![0.5, 0.5]),
73            max_iter: 1000,
74            tol: 1e-6,
75            learning_rate: 0.01,
76            random_state: None,
77        }
78    }
79}
80
81/// Scalarization optimizer for converting multi-objective problems to single-objective
82#[derive(Debug, Clone)]
83pub struct ScalarizationOptimizer<S = Untrained> {
84    state: S,
85    config: ScalarizationConfig,
86}
87
88/// Trained state for scalarization optimizer
89#[derive(Debug, Clone)]
90pub struct ScalarizationOptimizerTrained {
91    /// Model parameters
92    pub parameters: Array1<Float>,
93    /// Scalarized objective value
94    pub scalarized_value: Float,
95    /// Original objective values
96    pub objective_values: Vec<Float>,
97    /// Convergence history
98    pub convergence_history: Vec<Float>,
99    /// Number of features
100    pub n_features: usize,
101    /// Number of objectives
102    pub n_objectives: usize,
103    /// Configuration used
104    pub config: ScalarizationConfig,
105}
106
107impl ScalarizationOptimizer<Untrained> {
108    /// Create a new scalarization optimizer
109    pub fn new(method: ScalarizationMethod) -> Self {
110        Self {
111            state: Untrained,
112            config: ScalarizationConfig {
113                method,
114                ..Default::default()
115            },
116        }
117    }
118
119    /// Set the configuration
120    pub fn config(mut self, config: ScalarizationConfig) -> Self {
121        self.config = config;
122        self
123    }
124
125    /// Set maximum iterations
126    pub fn max_iter(mut self, max_iter: usize) -> Self {
127        self.config.max_iter = max_iter;
128        self
129    }
130
131    /// Set convergence tolerance
132    pub fn tol(mut self, tol: Float) -> Self {
133        self.config.tol = tol;
134        self
135    }
136
137    /// Set learning rate
138    pub fn learning_rate(mut self, learning_rate: Float) -> Self {
139        self.config.learning_rate = learning_rate;
140        self
141    }
142
143    /// Set random state
144    pub fn random_state(mut self, random_state: Option<u64>) -> Self {
145        self.config.random_state = random_state;
146        self
147    }
148
149    /// Compute scalarized objective value
150    pub fn scalarize_objectives(&self, objectives: &[Float]) -> SklResult<Float> {
151        match &self.config.method {
152            ScalarizationMethod::WeightedSum(weights) => {
153                if weights.len() != objectives.len() {
154                    return Err(SklearsError::InvalidInput(
155                        "Weight vector length must match number of objectives".to_string(),
156                    ));
157                }
158                Ok(objectives
159                    .iter()
160                    .zip(weights.iter())
161                    .map(|(obj, w)| obj * w)
162                    .sum())
163            }
164
165            ScalarizationMethod::EpsilonConstraint {
166                objective_index,
167                epsilon_values,
168            } => {
169                if *objective_index >= objectives.len() {
170                    return Err(SklearsError::InvalidInput(
171                        "Objective index out of bounds".to_string(),
172                    ));
173                }
174                if epsilon_values.len() != objectives.len() - 1 {
175                    return Err(SklearsError::InvalidInput(
176                        "Epsilon values length must be objectives - 1".to_string(),
177                    ));
178                }
179
180                // Check epsilon constraints
181                let mut eps_idx = 0;
182                for (i, &obj_val) in objectives.iter().enumerate() {
183                    if i != *objective_index {
184                        if obj_val > epsilon_values[eps_idx] {
185                            return Ok(Float::INFINITY); // Constraint violated
186                        }
187                        eps_idx += 1;
188                    }
189                }
190
191                Ok(objectives[*objective_index])
192            }
193
194            ScalarizationMethod::AchievementScalarizingFunction {
195                reference_point,
196                augmentation_coefficient,
197            } => {
198                if reference_point.len() != objectives.len() {
199                    return Err(SklearsError::InvalidInput(
200                        "Reference point length must match number of objectives".to_string(),
201                    ));
202                }
203
204                let max_normalized_diff = objectives
205                    .iter()
206                    .zip(reference_point.iter())
207                    .map(|(obj, ref_pt)| (obj - ref_pt).max(0.0))
208                    .fold(0.0, Float::max);
209
210                let augmentation_term = augmentation_coefficient
211                    * objectives
212                        .iter()
213                        .zip(reference_point.iter())
214                        .map(|(obj, ref_pt)| obj - ref_pt)
215                        .sum::<Float>();
216
217                Ok(max_normalized_diff + augmentation_term)
218            }
219
220            ScalarizationMethod::AugmentedWeightedTchebycheff {
221                reference_point,
222                weights,
223                augmentation_coefficient,
224            } => {
225                if reference_point.len() != objectives.len() || weights.len() != objectives.len() {
226                    return Err(SklearsError::InvalidInput(
227                        "Reference point and weights length must match number of objectives"
228                            .to_string(),
229                    ));
230                }
231
232                let max_weighted_diff = objectives
233                    .iter()
234                    .zip(reference_point.iter())
235                    .zip(weights.iter())
236                    .map(|((obj, ref_pt), w)| w * (obj - ref_pt).abs())
237                    .fold(0.0, Float::max);
238
239                let augmentation_term = augmentation_coefficient
240                    * objectives
241                        .iter()
242                        .zip(reference_point.iter())
243                        .map(|(obj, ref_pt)| obj - ref_pt)
244                        .sum::<Float>();
245
246                Ok(max_weighted_diff + augmentation_term)
247            }
248
249            ScalarizationMethod::NormalizedNormalConstraint {
250                anchor_points,
251                utopia_point,
252            } => {
253                if utopia_point.len() != objectives.len() {
254                    return Err(SklearsError::InvalidInput(
255                        "Utopia point length must match number of objectives".to_string(),
256                    ));
257                }
258
259                // Normalize objectives
260                let normalized_objectives: Vec<Float> = objectives
261                    .iter()
262                    .zip(utopia_point.iter())
263                    .enumerate()
264                    .map(|(i, (obj, utopia))| {
265                        let anchor = anchor_points[[i, i]];
266                        if (anchor - utopia).abs() > 1e-10 {
267                            (obj - utopia) / (anchor - utopia)
268                        } else {
269                            0.0
270                        }
271                    })
272                    .collect();
273
274                // Compute Euclidean distance from origin
275                Ok(normalized_objectives
276                    .iter()
277                    .map(|x| x * x)
278                    .sum::<Float>()
279                    .sqrt())
280            }
281        }
282    }
283
284    /// Generate multiple scalarized problems for comprehensive optimization
285    pub fn generate_scalarized_problems(
286        &self,
287        n_problems: usize,
288        n_objectives: usize,
289    ) -> SklResult<Vec<ScalarizationMethod>> {
290        let mut problems = Vec::new();
291
292        match &self.config.method {
293            ScalarizationMethod::WeightedSum(_) => {
294                // Generate uniformly distributed weight vectors
295                for i in 0..n_problems {
296                    let mut weights = vec![0.0; n_objectives];
297                    let step = 1.0 / (n_problems - 1) as Float;
298                    weights[0] = i as Float * step;
299                    weights[1] = 1.0 - weights[0];
300
301                    // Extend to higher dimensions using Dirichlet-like distribution
302                    if n_objectives > 2 {
303                        let remaining = weights[1];
304                        weights[1] = remaining * (i as Float / n_problems as Float);
305                        for j in 2..n_objectives {
306                            weights[j] = remaining / (n_objectives - 1) as Float;
307                        }
308                    }
309
310                    problems.push(ScalarizationMethod::WeightedSum(weights));
311                }
312            }
313
314            ScalarizationMethod::EpsilonConstraint {
315                objective_index, ..
316            } => {
317                // Generate different epsilon values
318                for i in 0..n_problems {
319                    let step = 1.0 / n_problems as Float;
320                    let epsilon_values = (0..n_objectives - 1)
321                        .map(|_| (i as Float + 1.0) * step)
322                        .collect();
323
324                    problems.push(ScalarizationMethod::EpsilonConstraint {
325                        objective_index: *objective_index,
326                        epsilon_values,
327                    });
328                }
329            }
330
331            _ => {
332                // For other methods, generate variations of the current method
333                for _ in 0..n_problems {
334                    problems.push(self.config.method.clone());
335                }
336            }
337        }
338
339        Ok(problems)
340    }
341}
342
343impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, Float>> for ScalarizationOptimizer<Untrained> {
344    type Fitted = ScalarizationOptimizer<ScalarizationOptimizerTrained>;
345
346    fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView2<'_, Float>) -> SklResult<Self::Fitted> {
347        let (n_samples, n_features) = X.dim();
348        let (y_samples, n_objectives) = y.dim();
349
350        if n_samples != y_samples {
351            return Err(SklearsError::InvalidInput(
352                "X and y must have the same number of samples".to_string(),
353            ));
354        }
355
356        let mut rng = thread_rng();
357
358        // Initialize parameters
359        let normal_dist = RandNormal::new(0.0, 0.1).unwrap();
360        let mut parameters = Array1::<Float>::zeros(n_features * n_objectives);
361        for i in 0..(n_features * n_objectives) {
362            parameters[i] = rng.sample(normal_dist);
363        }
364
365        let mut convergence_history = Vec::new();
366        let mut prev_scalarized_value = Float::INFINITY;
367
368        for iteration in 0..self.config.max_iter {
369            // Compute current objectives (simplified for demonstration)
370            let objectives: Vec<Float> = (0..n_objectives)
371                .map(|i| {
372                    let param_slice = parameters.slice(s![i * n_features..(i + 1) * n_features]);
373                    // Simplified objective computation - in practice this would be problem-specific
374                    param_slice.iter().map(|x| x * x).sum::<Float>() / n_features as Float
375                })
376                .collect();
377
378            // Scalarize objectives
379            let scalarized_value = self.scalarize_objectives(&objectives)?;
380            convergence_history.push(scalarized_value);
381
382            // Check convergence
383            if (prev_scalarized_value - scalarized_value).abs() < self.config.tol {
384                break;
385            }
386            prev_scalarized_value = scalarized_value;
387
388            // Simplified gradient descent update
389            for i in 0..parameters.len() {
390                let gradient = 2.0 * parameters[i] / n_features as Float; // Simplified gradient
391                parameters[i] -= self.config.learning_rate * gradient;
392            }
393        }
394
395        // Final objective computation
396        let final_objectives: Vec<Float> = (0..n_objectives)
397            .map(|i| {
398                let param_slice = parameters.slice(s![i * n_features..(i + 1) * n_features]);
399                param_slice.iter().map(|x| x * x).sum::<Float>() / n_features as Float
400            })
401            .collect();
402
403        let final_scalarized_value = self.scalarize_objectives(&final_objectives)?;
404
405        Ok(ScalarizationOptimizer {
406            state: ScalarizationOptimizerTrained {
407                parameters,
408                scalarized_value: final_scalarized_value,
409                objective_values: final_objectives,
410                convergence_history,
411                n_features,
412                n_objectives,
413                config: self.config.clone(),
414            },
415            config: self.config,
416        })
417    }
418}
419
420impl ScalarizationOptimizer<ScalarizationOptimizerTrained> {
421    /// Get the optimized parameters
422    pub fn parameters(&self) -> &Array1<Float> {
423        &self.state.parameters
424    }
425
426    /// Get the scalarized objective value
427    pub fn scalarized_value(&self) -> Float {
428        self.state.scalarized_value
429    }
430
431    /// Get the original objective values
432    pub fn objective_values(&self) -> &[Float] {
433        &self.state.objective_values
434    }
435
436    /// Get the convergence history
437    pub fn convergence_history(&self) -> &[Float] {
438        &self.state.convergence_history
439    }
440
441    /// Get the scalarization method used
442    pub fn method(&self) -> &ScalarizationMethod {
443        &self.state.config.method
444    }
445
446    /// Check if the solution is feasible (for constraint-based methods)
447    pub fn is_feasible(&self) -> bool {
448        match &self.state.config.method {
449            ScalarizationMethod::EpsilonConstraint {
450                epsilon_values,
451                objective_index,
452            } => {
453                let mut eps_idx = 0;
454                for (i, &obj_val) in self.state.objective_values.iter().enumerate() {
455                    if i != *objective_index {
456                        if obj_val > epsilon_values[eps_idx] {
457                            return false;
458                        }
459                        eps_idx += 1;
460                    }
461                }
462                true
463            }
464            _ => true, // Other methods don't have hard constraints
465        }
466    }
467}
468
469impl Estimator for ScalarizationOptimizer<Untrained> {
470    type Config = ScalarizationConfig;
471    type Error = SklearsError;
472    type Float = Float;
473
474    fn config(&self) -> &Self::Config {
475        &self.config
476    }
477}
478
479impl Estimator for ScalarizationOptimizer<ScalarizationOptimizerTrained> {
480    type Config = ScalarizationConfig;
481    type Error = SklearsError;
482    type Float = Float;
483
484    fn config(&self) -> &Self::Config {
485        &self.state.config
486    }
487}