sklears_multioutput/optimization/
scalarization_methods.rs1#![allow(non_snake_case)] use scirs2_core::ndarray::{s, Array1, Array2, ArrayView2};
19use scirs2_core::random::thread_rng;
20use scirs2_core::random::RandNormal;
21use sklears_core::{
22 error::{Result as SklResult, SklearsError},
23 traits::{Estimator, Fit, Untrained},
24 types::Float,
25};
26
27#[derive(Debug, Clone, PartialEq)]
29pub enum ScalarizationMethod {
30 WeightedSum(Vec<Float>),
32 EpsilonConstraint {
34 objective_index: usize,
35 epsilon_values: Vec<Float>,
36 },
37 AchievementScalarizingFunction {
39 reference_point: Vec<Float>,
40 augmentation_coefficient: Float,
41 },
42 AugmentedWeightedTchebycheff {
44 reference_point: Vec<Float>,
45 weights: Vec<Float>,
46 augmentation_coefficient: Float,
47 },
48 NormalizedNormalConstraint {
50 anchor_points: Array2<Float>,
51 utopia_point: Vec<Float>,
52 },
53}
54
55#[derive(Debug, Clone)]
57pub struct ScalarizationConfig {
58 pub method: ScalarizationMethod,
60 pub max_iter: usize,
62 pub tol: Float,
64 pub learning_rate: Float,
66 pub random_state: Option<u64>,
68}
69
70impl Default for ScalarizationConfig {
71 fn default() -> Self {
72 Self {
73 method: ScalarizationMethod::WeightedSum(vec![0.5, 0.5]),
74 max_iter: 1000,
75 tol: 1e-6,
76 learning_rate: 0.01,
77 random_state: None,
78 }
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct ScalarizationOptimizer<S = Untrained> {
85 state: S,
86 config: ScalarizationConfig,
87}
88
89#[derive(Debug, Clone)]
91pub struct ScalarizationOptimizerTrained {
92 pub parameters: Array1<Float>,
94 pub scalarized_value: Float,
96 pub objective_values: Vec<Float>,
98 pub convergence_history: Vec<Float>,
100 pub n_features: usize,
102 pub n_objectives: usize,
104 pub config: ScalarizationConfig,
106}
107
108impl ScalarizationOptimizer<Untrained> {
109 pub fn new(method: ScalarizationMethod) -> Self {
111 Self {
112 state: Untrained,
113 config: ScalarizationConfig {
114 method,
115 ..Default::default()
116 },
117 }
118 }
119
120 pub fn config(mut self, config: ScalarizationConfig) -> Self {
122 self.config = config;
123 self
124 }
125
126 pub fn max_iter(mut self, max_iter: usize) -> Self {
128 self.config.max_iter = max_iter;
129 self
130 }
131
132 pub fn tol(mut self, tol: Float) -> Self {
134 self.config.tol = tol;
135 self
136 }
137
138 pub fn learning_rate(mut self, learning_rate: Float) -> Self {
140 self.config.learning_rate = learning_rate;
141 self
142 }
143
144 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
146 self.config.random_state = random_state;
147 self
148 }
149
150 pub fn scalarize_objectives(&self, objectives: &[Float]) -> SklResult<Float> {
152 match &self.config.method {
153 ScalarizationMethod::WeightedSum(weights) => {
154 if weights.len() != objectives.len() {
155 return Err(SklearsError::InvalidInput(
156 "Weight vector length must match number of objectives".to_string(),
157 ));
158 }
159 Ok(objectives
160 .iter()
161 .zip(weights.iter())
162 .map(|(obj, w)| obj * w)
163 .sum())
164 }
165
166 ScalarizationMethod::EpsilonConstraint {
167 objective_index,
168 epsilon_values,
169 } => {
170 if *objective_index >= objectives.len() {
171 return Err(SklearsError::InvalidInput(
172 "Objective index out of bounds".to_string(),
173 ));
174 }
175 if epsilon_values.len() != objectives.len() - 1 {
176 return Err(SklearsError::InvalidInput(
177 "Epsilon values length must be objectives - 1".to_string(),
178 ));
179 }
180
181 let mut eps_idx = 0;
183 for (i, &obj_val) in objectives.iter().enumerate() {
184 if i != *objective_index {
185 if obj_val > epsilon_values[eps_idx] {
186 return Ok(Float::INFINITY); }
188 eps_idx += 1;
189 }
190 }
191
192 Ok(objectives[*objective_index])
193 }
194
195 ScalarizationMethod::AchievementScalarizingFunction {
196 reference_point,
197 augmentation_coefficient,
198 } => {
199 if reference_point.len() != objectives.len() {
200 return Err(SklearsError::InvalidInput(
201 "Reference point length must match number of objectives".to_string(),
202 ));
203 }
204
205 let max_normalized_diff = objectives
206 .iter()
207 .zip(reference_point.iter())
208 .map(|(obj, ref_pt)| (obj - ref_pt).max(0.0))
209 .fold(0.0, Float::max);
210
211 let augmentation_term = augmentation_coefficient
212 * objectives
213 .iter()
214 .zip(reference_point.iter())
215 .map(|(obj, ref_pt)| obj - ref_pt)
216 .sum::<Float>();
217
218 Ok(max_normalized_diff + augmentation_term)
219 }
220
221 ScalarizationMethod::AugmentedWeightedTchebycheff {
222 reference_point,
223 weights,
224 augmentation_coefficient,
225 } => {
226 if reference_point.len() != objectives.len() || weights.len() != objectives.len() {
227 return Err(SklearsError::InvalidInput(
228 "Reference point and weights length must match number of objectives"
229 .to_string(),
230 ));
231 }
232
233 let max_weighted_diff = objectives
234 .iter()
235 .zip(reference_point.iter())
236 .zip(weights.iter())
237 .map(|((obj, ref_pt), w)| w * (obj - ref_pt).abs())
238 .fold(0.0, Float::max);
239
240 let augmentation_term = augmentation_coefficient
241 * objectives
242 .iter()
243 .zip(reference_point.iter())
244 .map(|(obj, ref_pt)| obj - ref_pt)
245 .sum::<Float>();
246
247 Ok(max_weighted_diff + augmentation_term)
248 }
249
250 ScalarizationMethod::NormalizedNormalConstraint {
251 anchor_points,
252 utopia_point,
253 } => {
254 if utopia_point.len() != objectives.len() {
255 return Err(SklearsError::InvalidInput(
256 "Utopia point length must match number of objectives".to_string(),
257 ));
258 }
259
260 let normalized_objectives: Vec<Float> = objectives
262 .iter()
263 .zip(utopia_point.iter())
264 .enumerate()
265 .map(|(i, (obj, utopia))| {
266 let anchor = anchor_points[[i, i]];
267 if (anchor - utopia).abs() > 1e-10 {
268 (obj - utopia) / (anchor - utopia)
269 } else {
270 0.0
271 }
272 })
273 .collect();
274
275 Ok(normalized_objectives
277 .iter()
278 .map(|x| x * x)
279 .sum::<Float>()
280 .sqrt())
281 }
282 }
283 }
284
285 pub fn generate_scalarized_problems(
287 &self,
288 n_problems: usize,
289 n_objectives: usize,
290 ) -> SklResult<Vec<ScalarizationMethod>> {
291 let mut problems = Vec::new();
292
293 match &self.config.method {
294 ScalarizationMethod::WeightedSum(_) => {
295 for i in 0..n_problems {
297 let mut weights = vec![0.0; n_objectives];
298 let step = 1.0 / (n_problems - 1) as Float;
299 weights[0] = i as Float * step;
300 weights[1] = 1.0 - weights[0];
301
302 if n_objectives > 2 {
304 let remaining = weights[1];
305 weights[1] = remaining * (i as Float / n_problems as Float);
306 for weight in weights[2..].iter_mut() {
307 *weight = remaining / (n_objectives - 1) as Float;
308 }
309 }
310
311 problems.push(ScalarizationMethod::WeightedSum(weights));
312 }
313 }
314
315 ScalarizationMethod::EpsilonConstraint {
316 objective_index, ..
317 } => {
318 for i in 0..n_problems {
320 let step = 1.0 / n_problems as Float;
321 let epsilon_values = (0..n_objectives - 1)
322 .map(|_| (i as Float + 1.0) * step)
323 .collect();
324
325 problems.push(ScalarizationMethod::EpsilonConstraint {
326 objective_index: *objective_index,
327 epsilon_values,
328 });
329 }
330 }
331
332 _ => {
333 for _ in 0..n_problems {
335 problems.push(self.config.method.clone());
336 }
337 }
338 }
339
340 Ok(problems)
341 }
342}
343
344impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, Float>> for ScalarizationOptimizer<Untrained> {
345 type Fitted = ScalarizationOptimizer<ScalarizationOptimizerTrained>;
346
347 fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView2<'_, Float>) -> SklResult<Self::Fitted> {
348 let (n_samples, n_features) = X.dim();
349 let (y_samples, n_objectives) = y.dim();
350
351 if n_samples != y_samples {
352 return Err(SklearsError::InvalidInput(
353 "X and y must have the same number of samples".to_string(),
354 ));
355 }
356
357 let mut rng = thread_rng();
358
359 let normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
361 let mut parameters = Array1::<Float>::zeros(n_features * n_objectives);
362 for i in 0..(n_features * n_objectives) {
363 parameters[i] = rng.sample(normal_dist);
364 }
365
366 let mut convergence_history = Vec::new();
367 let mut prev_scalarized_value = Float::INFINITY;
368
369 for _iteration in 0..self.config.max_iter {
370 let objectives: Vec<Float> = (0..n_objectives)
372 .map(|i| {
373 let param_slice = parameters.slice(s![i * n_features..(i + 1) * n_features]);
374 param_slice.iter().map(|x| x * x).sum::<Float>() / n_features as Float
376 })
377 .collect();
378
379 let scalarized_value = self.scalarize_objectives(&objectives)?;
381 convergence_history.push(scalarized_value);
382
383 if (prev_scalarized_value - scalarized_value).abs() < self.config.tol {
385 break;
386 }
387 prev_scalarized_value = scalarized_value;
388
389 for i in 0..parameters.len() {
391 let gradient = 2.0 * parameters[i] / n_features as Float; parameters[i] -= self.config.learning_rate * gradient;
393 }
394 }
395
396 let final_objectives: Vec<Float> = (0..n_objectives)
398 .map(|i| {
399 let param_slice = parameters.slice(s![i * n_features..(i + 1) * n_features]);
400 param_slice.iter().map(|x| x * x).sum::<Float>() / n_features as Float
401 })
402 .collect();
403
404 let final_scalarized_value = self.scalarize_objectives(&final_objectives)?;
405
406 Ok(ScalarizationOptimizer {
407 state: ScalarizationOptimizerTrained {
408 parameters,
409 scalarized_value: final_scalarized_value,
410 objective_values: final_objectives,
411 convergence_history,
412 n_features,
413 n_objectives,
414 config: self.config.clone(),
415 },
416 config: self.config,
417 })
418 }
419}
420
421impl ScalarizationOptimizer<ScalarizationOptimizerTrained> {
422 pub fn parameters(&self) -> &Array1<Float> {
424 &self.state.parameters
425 }
426
427 pub fn scalarized_value(&self) -> Float {
429 self.state.scalarized_value
430 }
431
432 pub fn objective_values(&self) -> &[Float] {
434 &self.state.objective_values
435 }
436
437 pub fn convergence_history(&self) -> &[Float] {
439 &self.state.convergence_history
440 }
441
442 pub fn method(&self) -> &ScalarizationMethod {
444 &self.state.config.method
445 }
446
447 pub fn is_feasible(&self) -> bool {
449 match &self.state.config.method {
450 ScalarizationMethod::EpsilonConstraint {
451 epsilon_values,
452 objective_index,
453 } => {
454 let mut eps_idx = 0;
455 for (i, &obj_val) in self.state.objective_values.iter().enumerate() {
456 if i != *objective_index {
457 if obj_val > epsilon_values[eps_idx] {
458 return false;
459 }
460 eps_idx += 1;
461 }
462 }
463 true
464 }
465 _ => true, }
467 }
468}
469
470impl Estimator for ScalarizationOptimizer<Untrained> {
471 type Config = ScalarizationConfig;
472 type Error = SklearsError;
473 type Float = Float;
474
475 fn config(&self) -> &Self::Config {
476 &self.config
477 }
478}
479
480impl Estimator for ScalarizationOptimizer<ScalarizationOptimizerTrained> {
481 type Config = ScalarizationConfig;
482 type Error = SklearsError;
483 type Float = Float;
484
485 fn config(&self) -> &Self::Config {
486 &self.state.config
487 }
488}