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.gen_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.gen::<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.gen::<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 = Uniform::new(-noise_range, noise_range).unwrap();
215 let noise = self.rng.sample(uniform);
216 noisy_x[[i, j]] += noise;
217 }
218 }
219 }
220
221 Ok(noisy_x)
222 }
223
224 fn inject_salt_pepper_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
225 let mut noisy_x = x.to_owned();
226 let (n_samples, n_features) = x.dim();
227
228 let min_val = x.iter().cloned().fold(f64::INFINITY, f64::min);
229 let max_val = x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
230
231 for i in 0..n_samples {
232 for j in 0..n_features {
233 if self.rng.gen::<f64>() < self.config.probability {
234 if self.rng.gen::<f64>() < 0.5 {
235 noisy_x[[i, j]] = min_val;
236 } else {
237 noisy_x[[i, j]] = max_val;
238 }
239 }
240 }
241 }
242
243 Ok(noisy_x)
244 }
245
246 fn inject_dropout_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
247 let mut noisy_x = x.to_owned();
248 let (n_samples, n_features) = x.dim();
249
250 for i in 0..n_samples {
251 for j in 0..n_features {
252 if self.rng.gen::<f64>() < self.config.intensity {
253 noisy_x[[i, j]] = 0.0;
254 }
255 }
256 }
257
258 Ok(noisy_x)
259 }
260
261 fn inject_multiplicative_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
262 let mut noisy_x = x.to_owned();
263 let (n_samples, n_features) = x.dim();
264
265 for i in 0..n_samples {
266 for j in 0..n_features {
267 if self.rng.gen::<f64>() < self.config.probability {
268 let noise_factor = if self.config.intensity > 0.0 {
269 let gamma = Gamma::new(1.0 / self.config.intensity, self.config.intensity)
270 .map_err(|_| noise_error("Random number generation failed"))?;
271 self.rng.sample(gamma)
272 } else {
273 1.0
274 };
275
276 noisy_x[[i, j]] *= noise_factor;
277 }
278 }
279 }
280
281 Ok(noisy_x)
282 }
283
284 fn inject_adversarial_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
285 match self
286 .config
287 .adversarial_method
288 .unwrap_or(AdversarialMethod::RandomNoise)
289 {
290 AdversarialMethod::FGSM => self.inject_fgsm_noise(x),
291 AdversarialMethod::PGD => self.inject_pgd_noise(x),
292 AdversarialMethod::RandomNoise => self.inject_random_adversarial_noise(x),
293 AdversarialMethod::BoundaryAttack => self.inject_boundary_attack_noise(x),
294 }
295 }
296
297 fn inject_fgsm_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
298 let mut noisy_x = x.to_owned();
299 let (n_samples, n_features) = x.dim();
300
301 for i in 0..n_samples {
302 for j in 0..n_features {
303 let gradient_sign = if self.rng.gen::<f64>() < 0.5 {
304 -1.0
305 } else {
306 1.0
307 };
308 let perturbation = self.config.intensity * gradient_sign;
309 noisy_x[[i, j]] += perturbation;
310 }
311 }
312
313 Ok(noisy_x)
314 }
315
316 fn inject_pgd_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
317 let mut noisy_x = x.to_owned();
318 let (n_samples, n_features) = x.dim();
319 let step_size = self.config.intensity * 0.1;
320 let num_steps = 10;
321
322 for _ in 0..num_steps {
323 for i in 0..n_samples {
324 for j in 0..n_features {
325 let gradient_sign = if self.rng.gen::<f64>() < 0.5 {
326 -1.0
327 } else {
328 1.0
329 };
330 let perturbation = step_size * gradient_sign;
331 noisy_x[[i, j]] += perturbation;
332
333 let max_perturbation = self.config.intensity;
334 noisy_x[[i, j]] = (noisy_x[[i, j]] - x[[i, j]])
335 .max(-max_perturbation)
336 .min(max_perturbation)
337 + x[[i, j]];
338 }
339 }
340 }
341
342 Ok(noisy_x)
343 }
344
345 fn inject_random_adversarial_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
346 let mut noisy_x = x.to_owned();
347 let (n_samples, n_features) = x.dim();
348
349 for i in 0..n_samples {
350 let mut perturbation_norm: f64 = 0.0;
351 let mut perturbations: Vec<f64> = vec![0.0; n_features];
352
353 for j in 0..n_features {
354 perturbations[j] = self.rng.gen_range(-1.0..1.0);
355 perturbation_norm += perturbations[j].powi(2);
356 }
357
358 perturbation_norm = perturbation_norm.sqrt();
359 if perturbation_norm > 0.0 {
360 for j in 0..n_features {
361 perturbations[j] =
362 (perturbations[j] / perturbation_norm) * self.config.intensity;
363 noisy_x[[i, j]] += perturbations[j];
364 }
365 }
366 }
367
368 Ok(noisy_x)
369 }
370
371 fn inject_boundary_attack_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
372 let mut noisy_x = x.to_owned();
373 let (n_samples, n_features) = x.dim();
374
375 for i in 0..n_samples {
376 for j in 0..n_features {
377 let direction = if self.rng.gen::<f64>() < 0.5 {
378 -1.0
379 } else {
380 1.0
381 };
382 let magnitude = self.rng.gen::<f64>() * self.config.intensity;
383 noisy_x[[i, j]] += direction * magnitude;
384 }
385 }
386
387 Ok(noisy_x)
388 }
389
390 fn inject_outlier_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
391 let mut noisy_x = x.to_owned();
392 let (n_samples, n_features) = x.dim();
393
394 for j in 0..n_features {
395 let feature_values: Vec<f64> = (0..n_samples).map(|i| x[[i, j]]).collect();
396 let mean = feature_values.iter().sum::<f64>() / n_samples as f64;
397 let variance = feature_values
398 .iter()
399 .map(|&val| (val - mean).powi(2))
400 .sum::<f64>()
401 / n_samples as f64;
402 let std_dev = variance.sqrt();
403
404 for i in 0..n_samples {
405 if self.rng.gen::<f64>() < self.config.probability {
406 let outlier_direction = if self.rng.gen::<f64>() < 0.5 {
407 -1.0
408 } else {
409 1.0
410 };
411 let outlier_magnitude = self.config.outlier_factor * std_dev;
412 noisy_x[[i, j]] = mean + outlier_direction * outlier_magnitude;
413 }
414 }
415 }
416
417 Ok(noisy_x)
418 }
419
420 fn inject_feature_swap_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
421 let mut noisy_x = x.to_owned();
422 let (n_samples, n_features) = x.dim();
423
424 if n_features < 2 {
425 return Ok(noisy_x);
426 }
427
428 for i in 0..n_samples {
429 if self.rng.gen::<f64>() < self.config.feature_swap_rate {
430 let feature1 = self.rng.gen_range(0..n_features);
431 let feature2 = self.rng.gen_range(0..n_features);
432
433 if feature1 != feature2 {
434 let temp = noisy_x[[i, feature1]];
435 noisy_x[[i, feature1]] = noisy_x[[i, feature2]];
436 noisy_x[[i, feature2]] = temp;
437 }
438 }
439 }
440
441 Ok(noisy_x)
442 }
443
444 fn inject_mixed_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
445 let mut noisy_x = x.to_owned();
446 let noise_types = [
447 NoiseType::Gaussian,
448 NoiseType::Uniform,
449 NoiseType::SaltAndPepper,
450 NoiseType::Multiplicative,
451 ];
452
453 let original_noise_type = self.config.noise_type;
454 let original_intensity = self.config.intensity;
455
456 for &noise_type in &noise_types {
457 self.config.noise_type = noise_type;
458 self.config.intensity = original_intensity / noise_types.len() as f64;
459
460 noisy_x = match noise_type {
461 NoiseType::Gaussian => self.inject_gaussian_noise(&noisy_x.view())?,
462 NoiseType::Uniform => self.inject_uniform_noise(&noisy_x.view())?,
463 NoiseType::SaltAndPepper => self.inject_salt_pepper_noise(&noisy_x.view())?,
464 NoiseType::Multiplicative => self.inject_multiplicative_noise(&noisy_x.view())?,
465 _ => noisy_x,
466 };
467 }
468
469 self.config.noise_type = original_noise_type;
470 self.config.intensity = original_intensity;
471
472 Ok(noisy_x)
473 }
474
475 pub fn compute_noise_statistics(
476 &self,
477 original: &ArrayView2<f64>,
478 noisy: &ArrayView2<f64>,
479 ) -> NoiseStatistics {
480 let (n_samples, n_features) = original.dim();
481 let mut affected_samples = 0;
482 let mut affected_features = 0;
483 let mut total_perturbation = 0.0;
484
485 for i in 0..n_samples {
486 let mut sample_affected = false;
487 for j in 0..n_features {
488 let perturbation = (noisy[[i, j]] - original[[i, j]]).abs();
489 if perturbation > 1e-10 {
490 if !sample_affected {
491 affected_samples += 1;
492 sample_affected = true;
493 }
494 total_perturbation += perturbation;
495 }
496 }
497 }
498
499 for j in 0..n_features {
500 let mut feature_affected = false;
501 for i in 0..n_samples {
502 if (noisy[[i, j]] - original[[i, j]]).abs() > 1e-10 {
503 feature_affected = true;
504 break;
505 }
506 }
507 if feature_affected {
508 affected_features += 1;
509 }
510 }
511
512 let signal_power: f64 = original.iter().map(|&x| x.powi(2)).sum();
513 let noise_power: f64 = original
514 .iter()
515 .zip(noisy.iter())
516 .map(|(&orig, &noise)| (noise - orig).powi(2))
517 .sum();
518
519 let snr = if noise_power > 0.0 {
520 10.0 * (signal_power / noise_power).log10()
521 } else {
522 f64::INFINITY
523 };
524
525 let avg_perturbation = total_perturbation / (n_samples * n_features) as f64;
526
527 NoiseStatistics {
528 noise_type: format!("{:?}", self.config.noise_type),
529 intensity: self.config.intensity,
530 affected_samples,
531 affected_features,
532 signal_to_noise_ratio: snr,
533 perturbation_magnitude: avg_perturbation,
534 }
535 }
536}
537
538pub fn robustness_test<M, F>(
539 model: &M,
540 x: &ArrayView2<f64>,
541 y: &ArrayView1<f64>,
542 noise_configs: Vec<NoiseConfig>,
543 eval_fn: F,
544) -> Result<Vec<RobustnessTestResult>>
545where
546 M: Clone,
547 F: Fn(&M, &ArrayView2<f64>, &ArrayView1<f64>) -> f64 + Copy,
548{
549 let original_performance = eval_fn(model, x, y);
550 let mut results = Vec::new();
551
552 for config in noise_configs {
553 let mut injector = NoiseInjector::new(config.clone());
554 let noisy_x = injector.inject_feature_noise(x)?;
555 let noisy_performance = eval_fn(model, &noisy_x.view(), y);
556
557 let performance_degradation = original_performance - noisy_performance;
558 let noise_sensitivity = performance_degradation / config.intensity.max(1e-10);
559 let robustness_score =
560 1.0 - (performance_degradation / original_performance.max(1e-10)).abs();
561
562 let noise_statistics = injector.compute_noise_statistics(x, &noisy_x.view());
563
564 results.push(RobustnessTestResult {
565 original_performance,
566 noisy_performance,
567 performance_degradation,
568 noise_sensitivity,
569 robustness_score: robustness_score.max(0.0),
570 noise_statistics,
571 });
572 }
573
574 Ok(results)
575}
576
577#[allow(non_snake_case)]
578#[cfg(test)]
579mod tests {
580 use super::*;
581 use scirs2_core::ndarray::{arr1, arr2, Array2};
582
583 fn create_test_data() -> Array2<f64> {
584 arr2(&[
585 [1.0, 2.0, 3.0],
586 [4.0, 5.0, 6.0],
587 [7.0, 8.0, 9.0],
588 [10.0, 11.0, 12.0],
589 ])
590 }
591
592 #[test]
593 fn test_gaussian_noise() {
594 let x = create_test_data();
595 let config = NoiseConfig {
596 noise_type: NoiseType::Gaussian,
597 intensity: 0.1,
598 random_state: Some(42),
599 ..Default::default()
600 };
601
602 let mut injector = NoiseInjector::new(config);
603 let noisy_x = injector.inject_feature_noise(&x.view()).unwrap();
604
605 assert_eq!(noisy_x.dim(), x.dim());
606 assert!(noisy_x != x);
607 }
608
609 #[test]
610 fn test_uniform_noise() {
611 let x = create_test_data();
612 let config = NoiseConfig {
613 noise_type: NoiseType::Uniform,
614 intensity: 0.2,
615 random_state: Some(42),
616 ..Default::default()
617 };
618
619 let mut injector = NoiseInjector::new(config);
620 let noisy_x = injector.inject_feature_noise(&x.view()).unwrap();
621
622 assert_eq!(noisy_x.dim(), x.dim());
623 }
624
625 #[test]
626 fn test_dropout_noise() {
627 let x = create_test_data();
628 let config = NoiseConfig {
629 noise_type: NoiseType::Dropout,
630 intensity: 0.3,
631 random_state: Some(42),
632 ..Default::default()
633 };
634
635 let mut injector = NoiseInjector::new(config);
636 let noisy_x = injector.inject_feature_noise(&x.view()).unwrap();
637
638 let zero_count = noisy_x.iter().filter(|&&val| val == 0.0).count();
639 assert!(zero_count > 0);
640 }
641
642 #[test]
643 fn test_label_noise() {
644 let y = arr1(&[0, 1, 2, 0, 1, 2]);
645 let config = NoiseConfig {
646 noise_type: NoiseType::LabelNoise,
647 label_flip_rate: 0.5,
648 random_state: Some(42),
649 ..Default::default()
650 };
651
652 let mut injector = NoiseInjector::new(config);
653 let noisy_y = injector.inject_label_noise(&y.view()).unwrap();
654
655 assert_eq!(noisy_y.len(), y.len());
656 assert!(noisy_y != y);
657 }
658
659 #[test]
660 fn test_adversarial_noise() {
661 let x = create_test_data();
662 let config = NoiseConfig {
663 noise_type: NoiseType::Adversarial,
664 intensity: 0.1,
665 adversarial_method: Some(AdversarialMethod::FGSM),
666 random_state: Some(42),
667 ..Default::default()
668 };
669
670 let mut injector = NoiseInjector::new(config);
671 let noisy_x = injector.inject_feature_noise(&x.view()).unwrap();
672
673 assert_eq!(noisy_x.dim(), x.dim());
674 }
675
676 #[test]
677 fn test_outlier_injection() {
678 let x = create_test_data();
679 let config = NoiseConfig {
680 noise_type: NoiseType::OutlierInjection,
681 probability: 0.1,
682 outlier_factor: 3.0,
683 random_state: Some(42),
684 ..Default::default()
685 };
686
687 let mut injector = NoiseInjector::new(config);
688 let noisy_x = injector.inject_feature_noise(&x.view()).unwrap();
689
690 assert_eq!(noisy_x.dim(), x.dim());
691 }
692
693 #[test]
694 fn test_mixed_noise() {
695 let x = create_test_data();
696 let config = NoiseConfig {
697 noise_type: NoiseType::MixedNoise,
698 intensity: 0.1,
699 random_state: Some(42),
700 ..Default::default()
701 };
702
703 let mut injector = NoiseInjector::new(config);
704 let noisy_x = injector.inject_feature_noise(&x.view()).unwrap();
705
706 assert_eq!(noisy_x.dim(), x.dim());
707 }
708
709 #[test]
710 fn test_noise_statistics() {
711 let x = create_test_data();
712 let config = NoiseConfig {
713 noise_type: NoiseType::Gaussian,
714 intensity: 0.1,
715 random_state: Some(42),
716 ..Default::default()
717 };
718
719 let mut injector = NoiseInjector::new(config);
720 let noisy_x = injector.inject_feature_noise(&x.view()).unwrap();
721 let stats = injector.compute_noise_statistics(&x.view(), &noisy_x.view());
722
723 assert!(stats.affected_samples > 0);
724 assert!(stats.affected_features > 0);
725 assert!(stats.signal_to_noise_ratio.is_finite());
726 assert!(stats.perturbation_magnitude >= 0.0);
727 }
728
729 #[test]
730 fn test_adaptive_noise() {
731 let x = create_test_data();
732 let config = NoiseConfig {
733 noise_type: NoiseType::Gaussian,
734 intensity: 0.1,
735 adaptive: true,
736 random_state: Some(42),
737 ..Default::default()
738 };
739
740 let mut injector = NoiseInjector::new(config);
741 let noisy_x = injector.inject_feature_noise(&x.view()).unwrap();
742
743 assert_eq!(noisy_x.dim(), x.dim());
744 }
745}