1use crate::error::OptimizeResult;
63use crate::result::OptimizeResults;
64use ndarray::{Array1, Array2, ArrayBase, Data, Ix1};
65
66pub trait RobustLoss: Clone {
68 fn loss(&self, r: f64) -> f64;
70
71 fn weight(&self, r: f64) -> f64;
74
75 fn weight_derivative(&self, r: f64) -> f64;
77}
78
79#[derive(Debug, Clone)]
81pub struct SquaredLoss;
82
83impl RobustLoss for SquaredLoss {
84 fn loss(&self, r: f64) -> f64 {
85 0.5 * r * r
86 }
87
88 fn weight(&self, r: f64) -> f64 {
89 1.0
90 }
91
92 fn weight_derivative(&self, r: f64) -> f64 {
93 0.0
94 }
95}
96
97#[derive(Debug, Clone)]
102pub struct HuberLoss {
103 delta: f64,
104}
105
106impl HuberLoss {
107 pub fn new(delta: f64) -> Self {
112 assert!(delta > 0.0, "Delta must be positive");
113 HuberLoss { delta }
114 }
115}
116
117impl RobustLoss for HuberLoss {
118 fn loss(&self, r: f64) -> f64 {
119 let abs_r = r.abs();
120 if abs_r <= self.delta {
121 0.5 * r * r
122 } else {
123 self.delta * (abs_r - 0.5 * self.delta)
124 }
125 }
126
127 fn weight(&self, r: f64) -> f64 {
128 let abs_r = r.abs();
129 if abs_r < 1e-10 || abs_r <= self.delta {
130 1.0
131 } else {
132 self.delta / abs_r
133 }
134 }
135
136 fn weight_derivative(&self, r: f64) -> f64 {
137 let abs_r = r.abs();
138 if abs_r <= self.delta || abs_r < 1e-10 {
139 0.0
140 } else {
141 -self.delta / (abs_r * abs_r)
142 }
143 }
144}
145
146#[derive(Debug, Clone)]
151pub struct BisquareLoss {
152 c: f64,
153}
154
155impl BisquareLoss {
156 pub fn new(c: f64) -> Self {
161 assert!(c > 0.0, "Tuning constant must be positive");
162 BisquareLoss { c }
163 }
164}
165
166impl RobustLoss for BisquareLoss {
167 fn loss(&self, r: f64) -> f64 {
168 let abs_r = r.abs();
169 if abs_r <= self.c {
170 let u = r / self.c;
171 (self.c * self.c / 6.0) * (1.0 - (1.0 - u * u).powi(3))
172 } else {
173 self.c * self.c / 6.0
174 }
175 }
176
177 fn weight(&self, r: f64) -> f64 {
178 let abs_r = r.abs();
179 if abs_r < 1e-10 {
180 1.0
181 } else if abs_r <= self.c {
182 let u = r / self.c;
183 (1.0 - u * u).powi(2)
184 } else {
185 0.0
186 }
187 }
188
189 fn weight_derivative(&self, r: f64) -> f64 {
190 let abs_r = r.abs();
191 if abs_r <= self.c && abs_r >= 1e-10 {
192 let u = r / self.c;
193 -4.0 * u * (1.0 - u * u) / (self.c * self.c)
194 } else {
195 0.0
196 }
197 }
198}
199
200#[derive(Debug, Clone)]
205pub struct CauchyLoss {
206 c: f64,
207}
208
209impl CauchyLoss {
210 pub fn new(c: f64) -> Self {
212 assert!(c > 0.0, "Scale parameter must be positive");
213 CauchyLoss { c }
214 }
215}
216
217impl RobustLoss for CauchyLoss {
218 fn loss(&self, r: f64) -> f64 {
219 let u = r / self.c;
220 (self.c * self.c / 2.0) * (1.0 + u * u).ln()
221 }
222
223 fn weight(&self, r: f64) -> f64 {
224 if r.abs() < 1e-10 {
225 1.0
226 } else {
227 let u = r / self.c;
228 1.0 / (1.0 + u * u)
229 }
230 }
231
232 fn weight_derivative(&self, r: f64) -> f64 {
233 if r.abs() < 1e-10 {
234 0.0
235 } else {
236 let u = r / self.c;
237 let denom = 1.0 + u * u;
238 -2.0 * u / (self.c * self.c * denom * denom)
239 }
240 }
241}
242
243#[derive(Debug, Clone)]
245pub struct RobustOptions {
246 pub max_iter: usize,
248
249 pub max_nfev: Option<usize>,
251
252 pub xtol: f64,
254
255 pub ftol: f64,
257
258 pub gtol: f64,
260
261 pub use_irls: bool,
263
264 pub weight_tol: f64,
266
267 pub irls_max_iter: usize,
269}
270
271impl Default for RobustOptions {
272 fn default() -> Self {
273 RobustOptions {
274 max_iter: 100,
275 max_nfev: None,
276 xtol: 1e-8,
277 ftol: 1e-8,
278 gtol: 1e-8,
279 use_irls: true,
280 weight_tol: 1e-4,
281 irls_max_iter: 20,
282 }
283 }
284}
285
286#[allow(dead_code)]
300pub fn robust_least_squares<F, J, L, D, S1, S2>(
301 residuals: F,
302 x0: &ArrayBase<S1, Ix1>,
303 loss: L,
304 jacobian: Option<J>,
305 data: &ArrayBase<S2, Ix1>,
306 options: Option<RobustOptions>,
307) -> OptimizeResult<OptimizeResults<f64>>
308where
309 F: Fn(&[f64], &[D]) -> Array1<f64>,
310 J: Fn(&[f64], &[D]) -> Array2<f64>,
311 L: RobustLoss,
312 D: Clone,
313 S1: Data<Elem = f64>,
314 S2: Data<Elem = D>,
315{
316 let options = options.unwrap_or_default();
317
318 if options.use_irls {
320 irls_optimizer(residuals, x0, loss, jacobian, data, &options)
321 } else {
322 gradient_based_robust_optimizer(residuals, x0, loss, jacobian, data, &options)
324 }
325}
326
327#[allow(dead_code)]
329fn irls_optimizer<F, J, L, D, S1, S2>(
330 residuals: F,
331 x0: &ArrayBase<S1, Ix1>,
332 loss: L,
333 jacobian: Option<J>,
334 data: &ArrayBase<S2, Ix1>,
335 options: &RobustOptions,
336) -> OptimizeResult<OptimizeResults<f64>>
337where
338 F: Fn(&[f64], &[D]) -> Array1<f64>,
339 J: Fn(&[f64], &[D]) -> Array2<f64>,
340 L: RobustLoss,
341 D: Clone,
342 S1: Data<Elem = f64>,
343 S2: Data<Elem = D>,
344{
345 let mut x = x0.to_owned();
346 let m = x.len();
347
348 let max_nfev = options.max_nfev.unwrap_or(options.max_iter * m * 10);
349 let mut nfev = 0;
350 let mut njev = 0;
351 let mut iter = 0;
352
353 let mut res = residuals(x.as_slice().unwrap(), data.as_slice().unwrap());
355 nfev += 1;
356 let n = res.len();
357
358 let mut weights = Array1::ones(n);
360 let mut prev_weights = weights.clone();
361
362 let compute_numerical_jacobian =
364 |x_val: &Array1<f64>, res_val: &Array1<f64>| -> (Array2<f64>, usize) {
365 let eps = 1e-8;
366 let mut jac = Array2::zeros((n, m));
367 let mut count = 0;
368
369 for j in 0..m {
370 let mut x_h = x_val.clone();
371 x_h[j] += eps;
372 let res_h = residuals(x_h.as_slice().unwrap(), data.as_slice().unwrap());
373 count += 1;
374
375 for i in 0..n {
376 jac[[i, j]] = (res_h[i] - res_val[i]) / eps;
377 }
378 }
379
380 (jac, count)
381 };
382
383 while iter < options.irls_max_iter && nfev < max_nfev {
385 for i in 0..n {
387 weights[i] = loss.weight(res[i]);
388 }
389
390 let weight_change = weights
392 .iter()
393 .zip(prev_weights.iter())
394 .map(|(&w, &pw)| (w - pw).abs())
395 .sum::<f64>()
396 / n as f64;
397
398 if weight_change < options.weight_tol && iter > 0 {
399 break;
400 }
401
402 prev_weights = weights.clone();
403
404 let (jac, jac_evals) = match &jacobian {
406 Some(jac_fn) => {
407 let j = jac_fn(x.as_slice().unwrap(), data.as_slice().unwrap());
408 njev += 1;
409 (j, 0)
410 }
411 None => {
412 let (j, count) = compute_numerical_jacobian(&x, &res);
413 nfev += count;
414 (j, count)
415 }
416 };
417
418 let mut weighted_jac = Array2::zeros((n, m));
420 let mut weighted_res = Array1::zeros(n);
421
422 for i in 0..n {
423 let w = weights[i].sqrt();
424 for j in 0..m {
425 weighted_jac[[i, j]] = jac[[i, j]] * w;
426 }
427 weighted_res[i] = res[i] * w;
428 }
429
430 let jt_wj = weighted_jac.t().dot(&weighted_jac);
432 let neg_jt_wr = -weighted_jac.t().dot(&weighted_res);
433
434 match solve(&jt_wj, &neg_jt_wr) {
436 Some(step) => {
437 let mut line_search_alpha = 1.0;
439 let best_cost = compute_robust_cost(&res, &loss);
440 let mut best_x = x.clone();
441
442 for _ in 0..10 {
444 let x_new = &x + &step * line_search_alpha;
445 let res_new = residuals(x_new.as_slice().unwrap(), data.as_slice().unwrap());
446 nfev += 1;
447
448 let new_cost = compute_robust_cost(&res_new, &loss);
449
450 if new_cost < best_cost {
451 best_x = x_new;
452 break;
453 }
454
455 line_search_alpha *= 0.5;
456 }
457
458 let step_norm = step.iter().map(|&s| s * s).sum::<f64>().sqrt();
460 let x_norm = x.iter().map(|&xi| xi * xi).sum::<f64>().sqrt();
461
462 if step_norm < options.xtol * (1.0 + x_norm) {
463 x = best_x;
464 res = residuals(x.as_slice().unwrap(), data.as_slice().unwrap());
465 nfev += 1;
466 break;
467 }
468
469 x = best_x;
471 res = residuals(x.as_slice().unwrap(), data.as_slice().unwrap());
472 nfev += 1;
473 }
474 None => {
475 break;
477 }
478 }
479
480 iter += 1;
481 }
482
483 let final_cost = compute_robust_cost(&res, &loss);
485
486 let mut result = OptimizeResults::<f64>::default();
488 result.x = x;
489 result.fun = final_cost;
490 result.nfev = nfev;
491 result.njev = njev;
492 result.nit = iter;
493 result.success = iter < options.irls_max_iter;
494
495 if result.success {
496 result.message = "Optimization terminated successfully.".to_string();
497 } else {
498 result.message = "Maximum iterations reached.".to_string();
499 }
500
501 Ok(result)
502}
503
504#[allow(dead_code)]
506fn gradient_based_robust_optimizer<F, J, L, D, S1, S2>(
507 _residuals: F,
508 x0: &ArrayBase<S1, Ix1>,
509 _loss: L,
510 _jacobian: Option<J>,
511 _data: &ArrayBase<S2, Ix1>,
512 _options: &RobustOptions,
513) -> OptimizeResult<OptimizeResults<f64>>
514where
515 F: Fn(&[f64], &[D]) -> Array1<f64>,
516 J: Fn(&[f64], &[D]) -> Array2<f64>,
517 L: RobustLoss,
518 D: Clone,
519 S1: Data<Elem = f64>,
520 S2: Data<Elem = D>,
521{
522 let mut result = OptimizeResults::<f64>::default();
526 result.x = x0.to_owned();
527 result.fun = 0.0;
528 result.success = false;
529 result.message = "Gradient-based robust optimization not yet implemented".to_string();
530
531 Ok(result)
532}
533
534#[allow(dead_code)]
536fn compute_robust_cost<L: RobustLoss>(residuals: &Array1<f64>, loss: &L) -> f64 {
537 residuals.iter().map(|&r| loss.loss(r)).sum()
538}
539
540#[allow(dead_code)]
542fn solve(a: &Array2<f64>, b: &Array1<f64>) -> Option<Array1<f64>> {
543 use scirs2_linalg::solve;
544
545 solve(&a.view(), &b.view(), None).ok()
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551 use ndarray::array;
552
553 #[test]
554 fn test_huber_loss() {
555 let loss = HuberLoss::new(1.0);
556
557 assert!((loss.loss(0.5) - 0.125).abs() < 1e-10);
559 assert!((loss.weight(0.5) - 1.0).abs() < 1e-10);
560
561 assert!((loss.loss(2.0) - 1.5).abs() < 1e-10);
563 assert!((loss.weight(2.0) - 0.5).abs() < 1e-10);
564 }
565
566 #[test]
567 fn test_bisquare_loss() {
568 let loss = BisquareLoss::new(4.685);
569
570 let small_r = 1.0;
572 assert!(loss.loss(small_r) > 0.0);
573 assert!(loss.weight(small_r) > 0.0);
574 assert!(loss.weight(small_r) < 1.0);
575
576 let large_r = 5.0;
578 assert!((loss.loss(large_r) - loss.loss(10.0)).abs() < 1e-10);
579 assert_eq!(loss.weight(large_r), 0.0);
580 }
581
582 #[test]
583 fn test_cauchy_loss() {
584 let loss = CauchyLoss::new(1.0);
585
586 assert!(loss.weight(0.0) > loss.weight(1.0));
588 assert!(loss.weight(1.0) > loss.weight(2.0));
589 assert!(loss.weight(2.0) > loss.weight(5.0));
590
591 assert_eq!(loss.loss(1.0), loss.loss(-1.0));
593 assert_eq!(loss.weight(1.0), loss.weight(-1.0));
594 }
595
596 #[test]
597 fn test_robust_least_squares_linear() {
598 fn residual(x: &[f64], data: &[f64]) -> Array1<f64> {
601 let n = data.len() / 2;
603 let t_values = &data[0..n];
604 let y_values = &data[n..];
605
606 let params = x;
607 let mut res = Array1::zeros(n);
608 for i in 0..n {
609 res[i] = y_values[i] - (params[0] + params[1] * t_values[i]);
610 }
611 res
612 }
613
614 fn jacobian(x: &[f64], data: &[f64]) -> Array2<f64> {
615 let n = data.len() / 2;
616 let t_values = &data[0..n];
617
618 let mut jac = Array2::zeros((n, 2));
619 for i in 0..n {
620 jac[[i, 0]] = -1.0;
621 jac[[i, 1]] = -t_values[i];
622 }
623 jac
624 }
625
626 let x0 = array![0.0, 0.0];
627
628 let data_array = array![0.0, 1.0, 2.0, 3.0, 4.0, 0.1, 0.9, 2.1, 2.9, 10.0];
630
631 let huber_loss = HuberLoss::new(1.0);
633 let result =
634 robust_least_squares(residual, &x0, huber_loss, Some(jacobian), &data_array, None)
635 .unwrap();
636
637 println!("Result: {:?}", result);
640 assert!(result.success);
641 assert!((result.x[1] - 1.0).abs() < 0.5); }
644
645 #[test]
646 fn test_irls_convergence() {
647 fn residual(x: &[f64], _: &[f64]) -> Array1<f64> {
649 array![x[0] - 1.0, x[1] - 2.0]
650 }
651
652 fn jacobian(x: &[f64], _: &[f64]) -> Array2<f64> {
653 array![[1.0, 0.0], [0.0, 1.0]]
654 }
655
656 let x0 = array![0.0, 0.0];
657 let data = array![];
658
659 let huber_loss = HuberLoss::new(1.0);
661 let result =
662 robust_least_squares(residual, &x0, huber_loss, Some(jacobian), &data, None).unwrap();
663
664 assert!(result.success);
665 assert!((result.x[0] - 1.0).abs() < 1e-3);
666 assert!((result.x[1] - 2.0).abs() < 1e-3);
667 }
668}