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
286pub fn robust_least_squares<F, J, L, D, S1, S2>(
300 residuals: F,
301 x0: &ArrayBase<S1, Ix1>,
302 loss: L,
303 jacobian: Option<J>,
304 data: &ArrayBase<S2, Ix1>,
305 options: Option<RobustOptions>,
306) -> OptimizeResult<OptimizeResults<f64>>
307where
308 F: Fn(&[f64], &[D]) -> Array1<f64>,
309 J: Fn(&[f64], &[D]) -> Array2<f64>,
310 L: RobustLoss,
311 D: Clone,
312 S1: Data<Elem = f64>,
313 S2: Data<Elem = D>,
314{
315 let options = options.unwrap_or_default();
316
317 if options.use_irls {
319 irls_optimizer(residuals, x0, loss, jacobian, data, &options)
320 } else {
321 gradient_based_robust_optimizer(residuals, x0, loss, jacobian, data, &options)
323 }
324}
325
326fn irls_optimizer<F, J, L, D, S1, S2>(
328 residuals: F,
329 x0: &ArrayBase<S1, Ix1>,
330 loss: L,
331 jacobian: Option<J>,
332 data: &ArrayBase<S2, Ix1>,
333 options: &RobustOptions,
334) -> OptimizeResult<OptimizeResults<f64>>
335where
336 F: Fn(&[f64], &[D]) -> Array1<f64>,
337 J: Fn(&[f64], &[D]) -> Array2<f64>,
338 L: RobustLoss,
339 D: Clone,
340 S1: Data<Elem = f64>,
341 S2: Data<Elem = D>,
342{
343 let mut x = x0.to_owned();
344 let m = x.len();
345
346 let max_nfev = options.max_nfev.unwrap_or(options.max_iter * m * 10);
347 let mut nfev = 0;
348 let mut njev = 0;
349 let mut iter = 0;
350
351 let mut res = residuals(x.as_slice().unwrap(), data.as_slice().unwrap());
353 nfev += 1;
354 let n = res.len();
355
356 let mut weights = Array1::ones(n);
358 let mut prev_weights = weights.clone();
359
360 let compute_numerical_jacobian =
362 |x_val: &Array1<f64>, res_val: &Array1<f64>| -> (Array2<f64>, usize) {
363 let eps = 1e-8;
364 let mut jac = Array2::zeros((n, m));
365 let mut count = 0;
366
367 for j in 0..m {
368 let mut x_h = x_val.clone();
369 x_h[j] += eps;
370 let res_h = residuals(x_h.as_slice().unwrap(), data.as_slice().unwrap());
371 count += 1;
372
373 for i in 0..n {
374 jac[[i, j]] = (res_h[i] - res_val[i]) / eps;
375 }
376 }
377
378 (jac, count)
379 };
380
381 while iter < options.irls_max_iter && nfev < max_nfev {
383 for i in 0..n {
385 weights[i] = loss.weight(res[i]);
386 }
387
388 let weight_change = weights
390 .iter()
391 .zip(prev_weights.iter())
392 .map(|(&w, &pw)| (w - pw).abs())
393 .sum::<f64>()
394 / n as f64;
395
396 if weight_change < options.weight_tol && iter > 0 {
397 break;
398 }
399
400 prev_weights = weights.clone();
401
402 let (jac, _jac_evals) = match &jacobian {
404 Some(jac_fn) => {
405 let j = jac_fn(x.as_slice().unwrap(), data.as_slice().unwrap());
406 njev += 1;
407 (j, 0)
408 }
409 None => {
410 let (j, count) = compute_numerical_jacobian(&x, &res);
411 nfev += count;
412 (j, count)
413 }
414 };
415
416 let mut weighted_jac = Array2::zeros((n, m));
418 let mut weighted_res = Array1::zeros(n);
419
420 for i in 0..n {
421 let w = weights[i].sqrt();
422 for j in 0..m {
423 weighted_jac[[i, j]] = jac[[i, j]] * w;
424 }
425 weighted_res[i] = res[i] * w;
426 }
427
428 let jt_wj = weighted_jac.t().dot(&weighted_jac);
430 let neg_jt_wr = -weighted_jac.t().dot(&weighted_res);
431
432 match solve_linear_system(&jt_wj, &neg_jt_wr) {
434 Some(step) => {
435 let mut line_search_alpha = 1.0;
437 let best_cost = compute_robust_cost(&res, &loss);
438 let mut best_x = x.clone();
439
440 for _ in 0..10 {
442 let x_new = &x + &step * line_search_alpha;
443 let res_new = residuals(x_new.as_slice().unwrap(), data.as_slice().unwrap());
444 nfev += 1;
445
446 let new_cost = compute_robust_cost(&res_new, &loss);
447
448 if new_cost < best_cost {
449 best_x = x_new;
450 break;
451 }
452
453 line_search_alpha *= 0.5;
454 }
455
456 let step_norm = step.iter().map(|&s| s * s).sum::<f64>().sqrt();
458 let x_norm = x.iter().map(|&xi| xi * xi).sum::<f64>().sqrt();
459
460 if step_norm < options.xtol * (1.0 + x_norm) {
461 x = best_x;
462 res = residuals(x.as_slice().unwrap(), data.as_slice().unwrap());
463 nfev += 1;
464 break;
465 }
466
467 x = best_x;
469 res = residuals(x.as_slice().unwrap(), data.as_slice().unwrap());
470 nfev += 1;
471 }
472 None => {
473 break;
475 }
476 }
477
478 iter += 1;
479 }
480
481 let final_cost = compute_robust_cost(&res, &loss);
483
484 let mut result = OptimizeResults::default();
486 result.x = x;
487 result.fun = final_cost;
488 result.nfev = nfev;
489 result.njev = njev;
490 result.nit = iter;
491 result.success = iter < options.irls_max_iter;
492
493 if result.success {
494 result.message = "Optimization terminated successfully.".to_string();
495 } else {
496 result.message = "Maximum iterations reached.".to_string();
497 }
498
499 Ok(result)
500}
501
502fn gradient_based_robust_optimizer<F, J, L, D, S1, S2>(
504 _residuals: F,
505 x0: &ArrayBase<S1, Ix1>,
506 _loss: L,
507 _jacobian: Option<J>,
508 _data: &ArrayBase<S2, Ix1>,
509 _options: &RobustOptions,
510) -> OptimizeResult<OptimizeResults<f64>>
511where
512 F: Fn(&[f64], &[D]) -> Array1<f64>,
513 J: Fn(&[f64], &[D]) -> Array2<f64>,
514 L: RobustLoss,
515 D: Clone,
516 S1: Data<Elem = f64>,
517 S2: Data<Elem = D>,
518{
519 let mut result = OptimizeResults::default();
523 result.x = x0.to_owned();
524 result.fun = 0.0;
525 result.success = false;
526 result.message = "Gradient-based robust optimization not yet implemented".to_string();
527
528 Ok(result)
529}
530
531fn compute_robust_cost<L: RobustLoss>(residuals: &Array1<f64>, loss: &L) -> f64 {
533 residuals.iter().map(|&r| loss.loss(r)).sum()
534}
535
536fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> Option<Array1<f64>> {
538 let n = a.shape()[0];
539 if n != a.shape()[1] || n != b.len() {
540 return None;
541 }
542
543 let mut aug = Array2::zeros((n, n + 1));
545 for i in 0..n {
546 for j in 0..n {
547 aug[[i, j]] = a[[i, j]];
548 }
549 aug[[i, n]] = b[i];
550 }
551
552 for i in 0..n {
554 let mut max_row = i;
556 let mut max_val = aug[[i, i]].abs();
557
558 for j in i + 1..n {
559 if aug[[j, i]].abs() > max_val {
560 max_row = j;
561 max_val = aug[[j, i]].abs();
562 }
563 }
564
565 if max_val < 1e-10 {
567 return None;
568 }
569
570 if max_row != i {
572 for j in 0..=n {
573 let temp = aug[[i, j]];
574 aug[[i, j]] = aug[[max_row, j]];
575 aug[[max_row, j]] = temp;
576 }
577 }
578
579 for j in i + 1..n {
581 let c = aug[[j, i]] / aug[[i, i]];
582 aug[[j, i]] = 0.0;
583
584 for k in i + 1..=n {
585 aug[[j, k]] -= c * aug[[i, k]];
586 }
587 }
588 }
589
590 let mut x = Array1::zeros(n);
592 for i in (0..n).rev() {
593 let mut sum = aug[[i, n]];
594 for j in i + 1..n {
595 sum -= aug[[i, j]] * x[j];
596 }
597
598 if aug[[i, i]].abs() < 1e-10 {
599 return None;
600 }
601
602 x[i] = sum / aug[[i, i]];
603 }
604
605 Some(x)
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use ndarray::array;
612
613 #[test]
614 fn test_huber_loss() {
615 let loss = HuberLoss::new(1.0);
616
617 assert!((loss.loss(0.5) - 0.125).abs() < 1e-10);
619 assert!((loss.weight(0.5) - 1.0).abs() < 1e-10);
620
621 assert!((loss.loss(2.0) - 1.5).abs() < 1e-10);
623 assert!((loss.weight(2.0) - 0.5).abs() < 1e-10);
624 }
625
626 #[test]
627 fn test_bisquare_loss() {
628 let loss = BisquareLoss::new(4.685);
629
630 let small_r = 1.0;
632 assert!(loss.loss(small_r) > 0.0);
633 assert!(loss.weight(small_r) > 0.0);
634 assert!(loss.weight(small_r) < 1.0);
635
636 let large_r = 5.0;
638 assert!((loss.loss(large_r) - loss.loss(10.0)).abs() < 1e-10);
639 assert_eq!(loss.weight(large_r), 0.0);
640 }
641
642 #[test]
643 fn test_cauchy_loss() {
644 let loss = CauchyLoss::new(1.0);
645
646 assert!(loss.weight(0.0) > loss.weight(1.0));
648 assert!(loss.weight(1.0) > loss.weight(2.0));
649 assert!(loss.weight(2.0) > loss.weight(5.0));
650
651 assert_eq!(loss.loss(1.0), loss.loss(-1.0));
653 assert_eq!(loss.weight(1.0), loss.weight(-1.0));
654 }
655
656 #[test]
657 fn test_robust_least_squares_linear() {
658 fn residual(_x: &[f64], data: &[f64]) -> Array1<f64> {
661 let n = data.len() / 2;
663 let t_values = &data[0..n];
664 let y_values = &data[n..];
665
666 let params = _x;
667 let mut res = Array1::zeros(n);
668 for i in 0..n {
669 res[i] = y_values[i] - (params[0] + params[1] * t_values[i]);
670 }
671 res
672 }
673
674 fn jacobian(_x: &[f64], data: &[f64]) -> Array2<f64> {
675 let n = data.len() / 2;
676 let t_values = &data[0..n];
677
678 let mut jac = Array2::zeros((n, 2));
679 for i in 0..n {
680 jac[[i, 0]] = -1.0;
681 jac[[i, 1]] = -t_values[i];
682 }
683 jac
684 }
685
686 let x0 = array![0.0, 0.0];
687
688 let data_array = array![0.0, 1.0, 2.0, 3.0, 4.0, 0.1, 0.9, 2.1, 2.9, 10.0];
690
691 let huber_loss = HuberLoss::new(1.0);
693 let result =
694 robust_least_squares(residual, &x0, huber_loss, Some(jacobian), &data_array, None)
695 .unwrap();
696
697 println!("Result: {:?}", result);
700 assert!(result.success);
701 assert!((result.x[1] - 1.0).abs() < 0.5); }
704
705 #[test]
706 fn test_irls_convergence() {
707 fn residual(x: &[f64], _: &[f64]) -> Array1<f64> {
709 array![x[0] - 1.0, x[1] - 2.0]
710 }
711
712 fn jacobian(_x: &[f64], _: &[f64]) -> Array2<f64> {
713 array![[1.0, 0.0], [0.0, 1.0]]
714 }
715
716 let x0 = array![0.0, 0.0];
717 let data = array![];
718
719 let huber_loss = HuberLoss::new(1.0);
721 let result =
722 robust_least_squares(residual, &x0, huber_loss, Some(jacobian), &data, None).unwrap();
723
724 assert!(result.success);
725 assert!((result.x[0] - 1.0).abs() < 1e-3);
726 assert!((result.x[1] - 2.0).abs() < 1e-3);
727 }
728}