sklears_multioutput/optimization/
scalarization_methods.rs1use 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#[derive(Debug, Clone, PartialEq)]
28pub enum ScalarizationMethod {
29 WeightedSum(Vec<Float>),
31 EpsilonConstraint {
33 objective_index: usize,
34 epsilon_values: Vec<Float>,
35 },
36 AchievementScalarizingFunction {
38 reference_point: Vec<Float>,
39 augmentation_coefficient: Float,
40 },
41 AugmentedWeightedTchebycheff {
43 reference_point: Vec<Float>,
44 weights: Vec<Float>,
45 augmentation_coefficient: Float,
46 },
47 NormalizedNormalConstraint {
49 anchor_points: Array2<Float>,
50 utopia_point: Vec<Float>,
51 },
52}
53
54#[derive(Debug, Clone)]
56pub struct ScalarizationConfig {
57 pub method: ScalarizationMethod,
59 pub max_iter: usize,
61 pub tol: Float,
63 pub learning_rate: Float,
65 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#[derive(Debug, Clone)]
83pub struct ScalarizationOptimizer<S = Untrained> {
84 state: S,
85 config: ScalarizationConfig,
86}
87
88#[derive(Debug, Clone)]
90pub struct ScalarizationOptimizerTrained {
91 pub parameters: Array1<Float>,
93 pub scalarized_value: Float,
95 pub objective_values: Vec<Float>,
97 pub convergence_history: Vec<Float>,
99 pub n_features: usize,
101 pub n_objectives: usize,
103 pub config: ScalarizationConfig,
105}
106
107impl ScalarizationOptimizer<Untrained> {
108 pub fn new(method: ScalarizationMethod) -> Self {
110 Self {
111 state: Untrained,
112 config: ScalarizationConfig {
113 method,
114 ..Default::default()
115 },
116 }
117 }
118
119 pub fn config(mut self, config: ScalarizationConfig) -> Self {
121 self.config = config;
122 self
123 }
124
125 pub fn max_iter(mut self, max_iter: usize) -> Self {
127 self.config.max_iter = max_iter;
128 self
129 }
130
131 pub fn tol(mut self, tol: Float) -> Self {
133 self.config.tol = tol;
134 self
135 }
136
137 pub fn learning_rate(mut self, learning_rate: Float) -> Self {
139 self.config.learning_rate = learning_rate;
140 self
141 }
142
143 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
145 self.config.random_state = random_state;
146 self
147 }
148
149 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 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); }
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 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 Ok(normalized_objectives
276 .iter()
277 .map(|x| x * x)
278 .sum::<Float>()
279 .sqrt())
280 }
281 }
282 }
283
284 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 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 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 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 _ 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 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 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 param_slice.iter().map(|x| x * x).sum::<Float>() / n_features as Float
375 })
376 .collect();
377
378 let scalarized_value = self.scalarize_objectives(&objectives)?;
380 convergence_history.push(scalarized_value);
381
382 if (prev_scalarized_value - scalarized_value).abs() < self.config.tol {
384 break;
385 }
386 prev_scalarized_value = scalarized_value;
387
388 for i in 0..parameters.len() {
390 let gradient = 2.0 * parameters[i] / n_features as Float; parameters[i] -= self.config.learning_rate * gradient;
392 }
393 }
394
395 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 pub fn parameters(&self) -> &Array1<Float> {
423 &self.state.parameters
424 }
425
426 pub fn scalarized_value(&self) -> Float {
428 self.state.scalarized_value
429 }
430
431 pub fn objective_values(&self) -> &[Float] {
433 &self.state.objective_values
434 }
435
436 pub fn convergence_history(&self) -> &[Float] {
438 &self.state.convergence_history
439 }
440
441 pub fn method(&self) -> &ScalarizationMethod {
443 &self.state.config.method
444 }
445
446 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, }
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}