1use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
7use scirs2_core::random::essentials::{Normal as RandNormal, Uniform};
8use scirs2_core::random::prelude::*;
9use scirs2_core::random::rngs::StdRng;
10#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13use sklears_core::prelude::*;
14
15fn noise_error(msg: &str) -> SklearsError {
16 SklearsError::InvalidInput(msg.to_string())
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum NoiseType {
21 Gaussian,
23 Uniform,
25 SaltAndPepper,
27 Dropout,
29 Multiplicative,
31 Adversarial,
33 OutlierInjection,
35 LabelNoise,
37 FeatureSwap,
39 MixedNoise,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub enum AdversarialMethod {
45 FGSM,
47 PGD,
49 RandomNoise,
51 BoundaryAttack,
53}
54
55#[derive(Debug, Clone)]
56pub struct NoiseConfig {
57 pub noise_type: NoiseType,
58 pub intensity: f64,
59 pub probability: f64,
60 pub random_state: Option<u64>,
61 pub adaptive: bool,
62 pub preserve_statistics: bool,
63 pub adversarial_method: Option<AdversarialMethod>,
64 pub outlier_factor: f64,
65 pub label_flip_rate: f64,
66 pub feature_swap_rate: f64,
67}
68
69impl Default for NoiseConfig {
70 fn default() -> Self {
71 Self {
72 noise_type: NoiseType::Gaussian,
73 intensity: 0.1,
74 probability: 1.0,
75 random_state: None,
76 adaptive: false,
77 preserve_statistics: false,
78 adversarial_method: None,
79 outlier_factor: 3.0,
80 label_flip_rate: 0.1,
81 feature_swap_rate: 0.1,
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
87pub struct RobustnessTestResult {
88 pub original_performance: f64,
89 pub noisy_performance: f64,
90 pub performance_degradation: f64,
91 pub noise_sensitivity: f64,
92 pub robustness_score: f64,
93 pub noise_statistics: NoiseStatistics,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct NoiseStatistics {
98 pub noise_type: String,
99 pub intensity: f64,
100 pub affected_samples: usize,
101 pub affected_features: usize,
102 pub signal_to_noise_ratio: f64,
103 pub perturbation_magnitude: f64,
104}
105
106pub struct NoiseInjector {
107 config: NoiseConfig,
108 rng: StdRng,
109}
110
111impl NoiseInjector {
112 pub fn new(config: NoiseConfig) -> Self {
113 let rng = if let Some(seed) = config.random_state {
114 StdRng::seed_from_u64(seed)
115 } else {
116 StdRng::from_rng(&mut scirs2_core::random::thread_rng())
117 };
118
119 Self { config, rng }
120 }
121
122 pub fn inject_feature_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
123 match self.config.noise_type {
124 NoiseType::Gaussian => self.inject_gaussian_noise(x),
125 NoiseType::Uniform => self.inject_uniform_noise(x),
126 NoiseType::SaltAndPepper => self.inject_salt_pepper_noise(x),
127 NoiseType::Dropout => self.inject_dropout_noise(x),
128 NoiseType::Multiplicative => self.inject_multiplicative_noise(x),
129 NoiseType::Adversarial => self.inject_adversarial_noise(x),
130 NoiseType::OutlierInjection => self.inject_outlier_noise(x),
131 NoiseType::FeatureSwap => self.inject_feature_swap_noise(x),
132 NoiseType::MixedNoise => self.inject_mixed_noise(x),
133 _ => Err(noise_error("Unsupported noise type for data type")),
134 }
135 }
136
137 pub fn inject_label_noise(&mut self, y: &ArrayView1<i32>) -> Result<Array1<i32>> {
138 if self.config.noise_type != NoiseType::LabelNoise {
139 return Err(noise_error("Unsupported noise type for data type"));
140 }
141
142 let mut noisy_y = y.to_owned();
143 let unique_labels: Vec<i32> = {
144 let mut labels: Vec<i32> = y.iter().cloned().collect();
145 labels.sort_unstable();
146 labels.dedup();
147 labels
148 };
149
150 if unique_labels.len() < 2 {
151 return Ok(noisy_y);
152 }
153
154 let flip_dist = Bernoulli::new(self.config.label_flip_rate)
155 .map_err(|_| noise_error("Invalid label flip rate"))?;
156
157 for i in 0..noisy_y.len() {
158 if self.rng.sample(flip_dist) {
159 let current_label = noisy_y[i];
160 let available_labels: Vec<i32> = unique_labels
161 .iter()
162 .filter(|&&label| label != current_label)
163 .cloned()
164 .collect();
165
166 if !available_labels.is_empty() {
167 let new_label_idx = self.rng.random_range(0..available_labels.len());
168 noisy_y[i] = available_labels[new_label_idx];
169 }
170 }
171 }
172
173 Ok(noisy_y)
174 }
175
176 fn inject_gaussian_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
177 let mut noisy_x = x.to_owned();
178 let (n_samples, n_features) = x.dim();
179
180 for i in 0..n_samples {
181 for j in 0..n_features {
182 if self.rng.random::<f64>() < self.config.probability {
183 let noise_std = if self.config.adaptive {
184 self.config.intensity * x[[i, j]].abs()
185 } else {
186 self.config.intensity
187 };
188
189 let normal = RandNormal::new(0.0, noise_std)
190 .map_err(|_| noise_error("Random number generation failed"))?;
191
192 let noise = self.rng.sample(normal);
193 noisy_x[[i, j]] += noise;
194 }
195 }
196 }
197
198 Ok(noisy_x)
199 }
200
201 fn inject_uniform_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
202 let mut noisy_x = x.to_owned();
203 let (n_samples, n_features) = x.dim();
204
205 for i in 0..n_samples {
206 for j in 0..n_features {
207 if self.rng.random::<f64>() < self.config.probability {
208 let noise_range = if self.config.adaptive {
209 self.config.intensity * x[[i, j]].abs()
210 } else {
211 self.config.intensity
212 };
213
214 let uniform =
215 Uniform::new(-noise_range, noise_range).expect("operation should succeed");
216 let noise = self.rng.sample(uniform);
217 noisy_x[[i, j]] += noise;
218 }
219 }
220 }
221
222 Ok(noisy_x)
223 }
224
225 fn inject_salt_pepper_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
226 let mut noisy_x = x.to_owned();
227 let (n_samples, n_features) = x.dim();
228
229 let min_val = x.iter().cloned().fold(f64::INFINITY, f64::min);
230 let max_val = x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
231
232 for i in 0..n_samples {
233 for j in 0..n_features {
234 if self.rng.random::<f64>() < self.config.probability {
235 if self.rng.random::<f64>() < 0.5 {
236 noisy_x[[i, j]] = min_val;
237 } else {
238 noisy_x[[i, j]] = max_val;
239 }
240 }
241 }
242 }
243
244 Ok(noisy_x)
245 }
246
247 fn inject_dropout_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
248 let mut noisy_x = x.to_owned();
249 let (n_samples, n_features) = x.dim();
250
251 for i in 0..n_samples {
252 for j in 0..n_features {
253 if self.rng.random::<f64>() < self.config.intensity {
254 noisy_x[[i, j]] = 0.0;
255 }
256 }
257 }
258
259 Ok(noisy_x)
260 }
261
262 fn inject_multiplicative_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
263 let mut noisy_x = x.to_owned();
264 let (n_samples, n_features) = x.dim();
265
266 for i in 0..n_samples {
267 for j in 0..n_features {
268 if self.rng.random::<f64>() < self.config.probability {
269 let noise_factor = if self.config.intensity > 0.0 {
270 let gamma = Gamma::new(1.0 / self.config.intensity, self.config.intensity)
271 .map_err(|_| noise_error("Random number generation failed"))?;
272 self.rng.sample(gamma)
273 } else {
274 1.0
275 };
276
277 noisy_x[[i, j]] *= noise_factor;
278 }
279 }
280 }
281
282 Ok(noisy_x)
283 }
284
285 fn inject_adversarial_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
286 match self
287 .config
288 .adversarial_method
289 .unwrap_or(AdversarialMethod::RandomNoise)
290 {
291 AdversarialMethod::FGSM => self.inject_fgsm_noise(x),
292 AdversarialMethod::PGD => self.inject_pgd_noise(x),
293 AdversarialMethod::RandomNoise => self.inject_random_adversarial_noise(x),
294 AdversarialMethod::BoundaryAttack => self.inject_boundary_attack_noise(x),
295 }
296 }
297
298 fn inject_fgsm_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
299 let mut noisy_x = x.to_owned();
300 let (n_samples, n_features) = x.dim();
301
302 for i in 0..n_samples {
303 for j in 0..n_features {
304 let gradient_sign = if self.rng.random::<f64>() < 0.5 {
305 -1.0
306 } else {
307 1.0
308 };
309 let perturbation = self.config.intensity * gradient_sign;
310 noisy_x[[i, j]] += perturbation;
311 }
312 }
313
314 Ok(noisy_x)
315 }
316
317 fn inject_pgd_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
318 let mut noisy_x = x.to_owned();
319 let (n_samples, n_features) = x.dim();
320 let step_size = self.config.intensity * 0.1;
321 let num_steps = 10;
322
323 for _ in 0..num_steps {
324 for i in 0..n_samples {
325 for j in 0..n_features {
326 let gradient_sign = if self.rng.random::<f64>() < 0.5 {
327 -1.0
328 } else {
329 1.0
330 };
331 let perturbation = step_size * gradient_sign;
332 noisy_x[[i, j]] += perturbation;
333
334 let max_perturbation = self.config.intensity;
335 noisy_x[[i, j]] = (noisy_x[[i, j]] - x[[i, j]])
336 .max(-max_perturbation)
337 .min(max_perturbation)
338 + x[[i, j]];
339 }
340 }
341 }
342
343 Ok(noisy_x)
344 }
345
346 fn inject_random_adversarial_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
347 let mut noisy_x = x.to_owned();
348 let (n_samples, n_features) = x.dim();
349
350 for i in 0..n_samples {
351 let mut perturbation_norm: f64 = 0.0;
352 let mut perturbations: Vec<f64> = vec![0.0; n_features];
353
354 for j in 0..n_features {
355 perturbations[j] = self.rng.random_range(-1.0..1.0);
356 perturbation_norm += perturbations[j].powi(2);
357 }
358
359 perturbation_norm = perturbation_norm.sqrt();
360 if perturbation_norm > 0.0 {
361 for j in 0..n_features {
362 perturbations[j] =
363 (perturbations[j] / perturbation_norm) * self.config.intensity;
364 noisy_x[[i, j]] += perturbations[j];
365 }
366 }
367 }
368
369 Ok(noisy_x)
370 }
371
372 fn inject_boundary_attack_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
373 let mut noisy_x = x.to_owned();
374 let (n_samples, n_features) = x.dim();
375
376 for i in 0..n_samples {
377 for j in 0..n_features {
378 let direction = if self.rng.random::<f64>() < 0.5 {
379 -1.0
380 } else {
381 1.0
382 };
383 let magnitude = self.rng.random::<f64>() * self.config.intensity;
384 noisy_x[[i, j]] += direction * magnitude;
385 }
386 }
387
388 Ok(noisy_x)
389 }
390
391 fn inject_outlier_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
392 let mut noisy_x = x.to_owned();
393 let (n_samples, n_features) = x.dim();
394
395 for j in 0..n_features {
396 let feature_values: Vec<f64> = (0..n_samples).map(|i| x[[i, j]]).collect();
397 let mean = feature_values.iter().sum::<f64>() / n_samples as f64;
398 let variance = feature_values
399 .iter()
400 .map(|&val| (val - mean).powi(2))
401 .sum::<f64>()
402 / n_samples as f64;
403 let std_dev = variance.sqrt();
404
405 for i in 0..n_samples {
406 if self.rng.random::<f64>() < self.config.probability {
407 let outlier_direction = if self.rng.random::<f64>() < 0.5 {
408 -1.0
409 } else {
410 1.0
411 };
412 let outlier_magnitude = self.config.outlier_factor * std_dev;
413 noisy_x[[i, j]] = mean + outlier_direction * outlier_magnitude;
414 }
415 }
416 }
417
418 Ok(noisy_x)
419 }
420
421 fn inject_feature_swap_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
422 let mut noisy_x = x.to_owned();
423 let (n_samples, n_features) = x.dim();
424
425 if n_features < 2 {
426 return Ok(noisy_x);
427 }
428
429 for i in 0..n_samples {
430 if self.rng.random::<f64>() < self.config.feature_swap_rate {
431 let feature1 = self.rng.random_range(0..n_features);
432 let feature2 = self.rng.random_range(0..n_features);
433
434 if feature1 != feature2 {
435 let temp = noisy_x[[i, feature1]];
436 noisy_x[[i, feature1]] = noisy_x[[i, feature2]];
437 noisy_x[[i, feature2]] = temp;
438 }
439 }
440 }
441
442 Ok(noisy_x)
443 }
444
445 fn inject_mixed_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
446 let mut noisy_x = x.to_owned();
447 let noise_types = [
448 NoiseType::Gaussian,
449 NoiseType::Uniform,
450 NoiseType::SaltAndPepper,
451 NoiseType::Multiplicative,
452 ];
453
454 let original_noise_type = self.config.noise_type;
455 let original_intensity = self.config.intensity;
456
457 for &noise_type in &noise_types {
458 self.config.noise_type = noise_type;
459 self.config.intensity = original_intensity / noise_types.len() as f64;
460
461 noisy_x = match noise_type {
462 NoiseType::Gaussian => self.inject_gaussian_noise(&noisy_x.view())?,
463 NoiseType::Uniform => self.inject_uniform_noise(&noisy_x.view())?,
464 NoiseType::SaltAndPepper => self.inject_salt_pepper_noise(&noisy_x.view())?,
465 NoiseType::Multiplicative => self.inject_multiplicative_noise(&noisy_x.view())?,
466 _ => noisy_x,
467 };
468 }
469
470 self.config.noise_type = original_noise_type;
471 self.config.intensity = original_intensity;
472
473 Ok(noisy_x)
474 }
475
476 pub fn compute_noise_statistics(
477 &self,
478 original: &ArrayView2<f64>,
479 noisy: &ArrayView2<f64>,
480 ) -> NoiseStatistics {
481 let (n_samples, n_features) = original.dim();
482 let mut affected_samples = 0;
483 let mut affected_features = 0;
484 let mut total_perturbation = 0.0;
485
486 for i in 0..n_samples {
487 let mut sample_affected = false;
488 for j in 0..n_features {
489 let perturbation = (noisy[[i, j]] - original[[i, j]]).abs();
490 if perturbation > 1e-10 {
491 if !sample_affected {
492 affected_samples += 1;
493 sample_affected = true;
494 }
495 total_perturbation += perturbation;
496 }
497 }
498 }
499
500 for j in 0..n_features {
501 let mut feature_affected = false;
502 for i in 0..n_samples {
503 if (noisy[[i, j]] - original[[i, j]]).abs() > 1e-10 {
504 feature_affected = true;
505 break;
506 }
507 }
508 if feature_affected {
509 affected_features += 1;
510 }
511 }
512
513 let signal_power: f64 = original.iter().map(|&x| x.powi(2)).sum();
514 let noise_power: f64 = original
515 .iter()
516 .zip(noisy.iter())
517 .map(|(&orig, &noise)| (noise - orig).powi(2))
518 .sum();
519
520 let snr = if noise_power > 0.0 {
521 10.0 * (signal_power / noise_power).log10()
522 } else {
523 f64::INFINITY
524 };
525
526 let avg_perturbation = total_perturbation / (n_samples * n_features) as f64;
527
528 NoiseStatistics {
529 noise_type: format!("{:?}", self.config.noise_type),
530 intensity: self.config.intensity,
531 affected_samples,
532 affected_features,
533 signal_to_noise_ratio: snr,
534 perturbation_magnitude: avg_perturbation,
535 }
536 }
537}
538
539pub fn robustness_test<M, F>(
540 model: &M,
541 x: &ArrayView2<f64>,
542 y: &ArrayView1<f64>,
543 noise_configs: Vec<NoiseConfig>,
544 eval_fn: F,
545) -> Result<Vec<RobustnessTestResult>>
546where
547 M: Clone,
548 F: Fn(&M, &ArrayView2<f64>, &ArrayView1<f64>) -> f64 + Copy,
549{
550 let original_performance = eval_fn(model, x, y);
551 let mut results = Vec::new();
552
553 for config in noise_configs {
554 let mut injector = NoiseInjector::new(config.clone());
555 let noisy_x = injector.inject_feature_noise(x)?;
556 let noisy_performance = eval_fn(model, &noisy_x.view(), y);
557
558 let performance_degradation = original_performance - noisy_performance;
559 let noise_sensitivity = performance_degradation / config.intensity.max(1e-10);
560 let robustness_score =
561 1.0 - (performance_degradation / original_performance.max(1e-10)).abs();
562
563 let noise_statistics = injector.compute_noise_statistics(x, &noisy_x.view());
564
565 results.push(RobustnessTestResult {
566 original_performance,
567 noisy_performance,
568 performance_degradation,
569 noise_sensitivity,
570 robustness_score: robustness_score.max(0.0),
571 noise_statistics,
572 });
573 }
574
575 Ok(results)
576}
577
578#[allow(non_snake_case)]
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use scirs2_core::ndarray::{arr1, arr2, Array2};
583
584 fn create_test_data() -> Array2<f64> {
585 arr2(&[
586 [1.0, 2.0, 3.0],
587 [4.0, 5.0, 6.0],
588 [7.0, 8.0, 9.0],
589 [10.0, 11.0, 12.0],
590 ])
591 }
592
593 #[test]
594 fn test_gaussian_noise() {
595 let x = create_test_data();
596 let config = NoiseConfig {
597 noise_type: NoiseType::Gaussian,
598 intensity: 0.1,
599 random_state: Some(42),
600 ..Default::default()
601 };
602
603 let mut injector = NoiseInjector::new(config);
604 let noisy_x = injector
605 .inject_feature_noise(&x.view())
606 .expect("operation should succeed");
607
608 assert_eq!(noisy_x.dim(), x.dim());
609 assert!(noisy_x != x);
610 }
611
612 #[test]
613 fn test_uniform_noise() {
614 let x = create_test_data();
615 let config = NoiseConfig {
616 noise_type: NoiseType::Uniform,
617 intensity: 0.2,
618 random_state: Some(42),
619 ..Default::default()
620 };
621
622 let mut injector = NoiseInjector::new(config);
623 let noisy_x = injector
624 .inject_feature_noise(&x.view())
625 .expect("operation should succeed");
626
627 assert_eq!(noisy_x.dim(), x.dim());
628 }
629
630 #[test]
631 fn test_dropout_noise() {
632 let x = create_test_data();
633 let config = NoiseConfig {
634 noise_type: NoiseType::Dropout,
635 intensity: 0.3,
636 random_state: Some(42),
637 ..Default::default()
638 };
639
640 let mut injector = NoiseInjector::new(config);
641 let noisy_x = injector
642 .inject_feature_noise(&x.view())
643 .expect("operation should succeed");
644
645 let zero_count = noisy_x.iter().filter(|&&val| val == 0.0).count();
646 assert!(zero_count > 0);
647 }
648
649 #[test]
650 fn test_label_noise() {
651 let y = arr1(&[0, 1, 2, 0, 1, 2]);
652 let config = NoiseConfig {
653 noise_type: NoiseType::LabelNoise,
654 label_flip_rate: 0.5,
655 random_state: Some(42),
656 ..Default::default()
657 };
658
659 let mut injector = NoiseInjector::new(config);
660 let noisy_y = injector
661 .inject_label_noise(&y.view())
662 .expect("operation should succeed");
663
664 assert_eq!(noisy_y.len(), y.len());
665 assert!(noisy_y != y);
666 }
667
668 #[test]
669 fn test_adversarial_noise() {
670 let x = create_test_data();
671 let config = NoiseConfig {
672 noise_type: NoiseType::Adversarial,
673 intensity: 0.1,
674 adversarial_method: Some(AdversarialMethod::FGSM),
675 random_state: Some(42),
676 ..Default::default()
677 };
678
679 let mut injector = NoiseInjector::new(config);
680 let noisy_x = injector
681 .inject_feature_noise(&x.view())
682 .expect("operation should succeed");
683
684 assert_eq!(noisy_x.dim(), x.dim());
685 }
686
687 #[test]
688 fn test_outlier_injection() {
689 let x = create_test_data();
690 let config = NoiseConfig {
691 noise_type: NoiseType::OutlierInjection,
692 probability: 0.1,
693 outlier_factor: 3.0,
694 random_state: Some(42),
695 ..Default::default()
696 };
697
698 let mut injector = NoiseInjector::new(config);
699 let noisy_x = injector
700 .inject_feature_noise(&x.view())
701 .expect("operation should succeed");
702
703 assert_eq!(noisy_x.dim(), x.dim());
704 }
705
706 #[test]
707 fn test_mixed_noise() {
708 let x = create_test_data();
709 let config = NoiseConfig {
710 noise_type: NoiseType::MixedNoise,
711 intensity: 0.1,
712 random_state: Some(42),
713 ..Default::default()
714 };
715
716 let mut injector = NoiseInjector::new(config);
717 let noisy_x = injector
718 .inject_feature_noise(&x.view())
719 .expect("operation should succeed");
720
721 assert_eq!(noisy_x.dim(), x.dim());
722 }
723
724 #[test]
725 fn test_noise_statistics() {
726 let x = create_test_data();
727 let config = NoiseConfig {
728 noise_type: NoiseType::Gaussian,
729 intensity: 0.1,
730 random_state: Some(42),
731 ..Default::default()
732 };
733
734 let mut injector = NoiseInjector::new(config);
735 let noisy_x = injector
736 .inject_feature_noise(&x.view())
737 .expect("operation should succeed");
738 let stats = injector.compute_noise_statistics(&x.view(), &noisy_x.view());
739
740 assert!(stats.affected_samples > 0);
741 assert!(stats.affected_features > 0);
742 assert!(stats.signal_to_noise_ratio.is_finite());
743 assert!(stats.perturbation_magnitude >= 0.0);
744 }
745
746 #[test]
747 fn test_adaptive_noise() {
748 let x = create_test_data();
749 let config = NoiseConfig {
750 noise_type: NoiseType::Gaussian,
751 intensity: 0.1,
752 adaptive: true,
753 random_state: Some(42),
754 ..Default::default()
755 };
756
757 let mut injector = NoiseInjector::new(config);
758 let noisy_x = injector
759 .inject_feature_noise(&x.view())
760 .expect("operation should succeed");
761
762 assert_eq!(noisy_x.dim(), x.dim());
763 }
764}