1use super::adversarial_analysis::*;
8use super::edge_case_discovery::*;
9use super::perturbation_testing::*;
10use super::reporting::SimulationReport;
11use super::types::*;
12use super::what_if_analysis::*;
13use anyhow::Result;
14use chrono::Utc;
15use std::collections::HashMap;
16
17#[derive(Debug)]
19pub struct SimulationAnalyzer {
20 config: SimulationConfig,
21 what_if_results: Vec<WhatIfAnalysisResult>,
22 perturbation_results: Vec<PerturbationTestResult>,
23 adversarial_results: Vec<AdversarialProbingResult>,
24 edge_case_results: Vec<EdgeCaseDiscoveryResult>,
25}
26
27impl SimulationAnalyzer {
28 pub fn new(config: SimulationConfig) -> Self {
30 Self {
31 config,
32 what_if_results: Vec::new(),
33 perturbation_results: Vec::new(),
34 adversarial_results: Vec::new(),
35 edge_case_results: Vec::new(),
36 }
37 }
38
39 pub async fn analyze_what_if(
41 &mut self,
42 base_input: &HashMap<String, f64>,
43 model_fn: Box<dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync>,
44 ) -> Result<WhatIfAnalysisResult> {
45 if !self.config.enable_what_if_analysis {
46 return Err(anyhow::anyhow!("What-if analysis is disabled"));
47 }
48
49 let base_prediction = model_fn(base_input);
50 let base_scenario = Scenario {
51 id: "base".to_string(),
52 description: "Original input scenario".to_string(),
53 features: base_input.clone(),
54 prediction: base_prediction,
55 confidence: None,
60 changed_features: vec![],
61 distance_from_base: 0.0,
62 plausibility: 1.0,
63 };
64
65 let scenarios = self.generate_what_if_scenarios(base_input, &model_fn).await?;
67
68 let impact_analysis = self.analyze_scenario_impacts(&base_scenario, &scenarios);
70
71 let sensitivity_analysis = self.analyze_feature_sensitivity_from_scenarios(&scenarios);
73
74 let counterfactual_insights =
76 self.generate_counterfactual_insights(&base_scenario, &scenarios);
77
78 let decision_boundary_exploration = self.explore_decision_boundary(&scenarios);
80
81 let result = WhatIfAnalysisResult {
82 timestamp: Utc::now(),
83 base_scenario,
84 scenarios,
85 impact_analysis,
86 sensitivity_analysis,
87 counterfactual_insights,
88 decision_boundary_exploration,
89 };
90
91 self.what_if_results.push(result.clone());
92 Ok(result)
93 }
94
95 pub async fn test_perturbations(
97 &mut self,
98 base_input: &HashMap<String, f64>,
99 model_fn: Box<dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync>,
100 ) -> Result<PerturbationTestResult> {
101 if !self.config.enable_perturbation_testing {
102 return Err(anyhow::anyhow!("Perturbation testing is disabled"));
103 }
104
105 let mut results_by_intensity = HashMap::new();
106
107 for &intensity in &self.config.perturbation_intensities {
109 let intensity_result =
110 self.test_perturbation_intensity(base_input, &model_fn, intensity).await?;
111 results_by_intensity.insert(intensity.to_string(), intensity_result);
112 }
113
114 let robustness_assessment = self.assess_robustness(&results_by_intensity);
116
117 let sensitivity_hotspots = self.identify_sensitivity_hotspots(&results_by_intensity);
119
120 let failure_modes = self.analyze_failure_modes(&results_by_intensity);
122
123 let result = PerturbationTestResult {
124 timestamp: Utc::now(),
125 base_input: base_input.clone(),
126 results_by_intensity,
127 robustness_assessment,
128 sensitivity_hotspots,
129 failure_modes,
130 };
131
132 self.perturbation_results.push(result.clone());
133 Ok(result)
134 }
135
136 pub async fn probe_adversarial(
138 &mut self,
139 base_input: &HashMap<String, f64>,
140 model_fn: Box<dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync>,
141 ) -> Result<AdversarialProbingResult> {
142 if !self.config.enable_adversarial_probing {
143 return Err(anyhow::anyhow!("Adversarial probing is disabled"));
144 }
145
146 let mut adversarial_examples = HashMap::new();
147
148 for method in &self.config.adversarial_methods {
150 let examples =
151 self.generate_adversarial_examples(base_input, &model_fn, method).await?;
152 adversarial_examples.insert(method.clone(), examples);
153 }
154
155 let attack_success_analysis = self.analyze_attack_success(&adversarial_examples);
157
158 let robustness_assessment = self.assess_adversarial_robustness(&adversarial_examples);
160
161 let defense_recommendations = self.generate_defense_recommendations(&adversarial_examples);
163
164 let result = AdversarialProbingResult {
165 timestamp: Utc::now(),
166 base_input: base_input.clone(),
167 adversarial_examples,
168 attack_success_analysis,
169 robustness_assessment,
170 defense_recommendations,
171 };
172
173 self.adversarial_results.push(result.clone());
174 Ok(result)
175 }
176
177 pub async fn discover_edge_cases(
179 &mut self,
180 input_space: &HashMap<String, (f64, f64)>,
181 model_fn: Box<dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync>,
182 ) -> Result<EdgeCaseDiscoveryResult> {
183 if !self.config.enable_edge_case_discovery {
184 return Err(anyhow::anyhow!("Edge case discovery is disabled"));
185 }
186
187 let edge_cases = self.search_edge_cases(input_space, &model_fn).await?;
189
190 let classification = self.classify_edge_cases(&edge_cases);
192
193 let coverage_analysis = self.analyze_edge_case_coverage(&edge_cases, input_space);
195
196 let risk_assessment = self.assess_edge_case_risks(&edge_cases);
198
199 let result = EdgeCaseDiscoveryResult {
200 timestamp: Utc::now(),
201 edge_cases,
202 classification,
203 coverage_analysis,
204 risk_assessment,
205 };
206
207 self.edge_case_results.push(result.clone());
208 Ok(result)
209 }
210
211 pub async fn generate_report(&self) -> Result<SimulationReport> {
213 Ok(SimulationReport {
214 timestamp: Utc::now(),
215 config: self.config.clone(),
216 what_if_analyses_count: self.what_if_results.len(),
217 perturbation_tests_count: self.perturbation_results.len(),
218 adversarial_probes_count: self.adversarial_results.len(),
219 edge_case_discoveries_count: self.edge_case_results.len(),
220 recent_what_if_results: self.what_if_results.iter().rev().take(3).cloned().collect(),
221 recent_perturbation_results: self
222 .perturbation_results
223 .iter()
224 .rev()
225 .take(3)
226 .cloned()
227 .collect(),
228 recent_adversarial_results: self
229 .adversarial_results
230 .iter()
231 .rev()
232 .take(3)
233 .cloned()
234 .collect(),
235 recent_edge_case_results: self
236 .edge_case_results
237 .iter()
238 .rev()
239 .take(3)
240 .cloned()
241 .collect(),
242 simulation_summary: self.generate_simulation_summary(),
243 })
244 }
245
246 async fn generate_what_if_scenarios(
249 &self,
250 base_input: &HashMap<String, f64>,
251 model_fn: &(dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync),
252 ) -> Result<Vec<Scenario>> {
253 let mut scenarios = Vec::new();
254 let _base_prediction = model_fn(base_input);
255
256 for i in 0..self.config.num_what_if_scenarios {
257 let mut scenario_input = base_input.clone();
258 let mut changed_features = Vec::new();
259
260 use scirs2_core::random::*; let mut rng = thread_rng();
263 let num_features_to_change = 1 + (rng.random_range(0..3)); let features: Vec<String> = base_input.keys().cloned().collect();
265
266 for _ in 0..num_features_to_change {
267 if let Some(feature_name) = features.get(rng.random_range(0..features.len())) {
268 let original_value = base_input[feature_name];
269 let change_factor = 0.8 + (rng.random::<f64>() * 0.4); let new_value = original_value * change_factor;
271
272 scenario_input.insert(feature_name.clone(), new_value);
273
274 changed_features.push(FeatureChange {
275 feature_name: feature_name.clone(),
276 original_value,
277 new_value,
278 change_magnitude: (new_value - original_value).abs(),
279 change_direction: if new_value > original_value {
280 ChangeDirection::Increase
281 } else {
282 ChangeDirection::Decrease
283 },
284 change_type: if (new_value - original_value).abs() / original_value.abs()
285 > 0.1
286 {
287 ChangeType::Significant
288 } else {
289 ChangeType::Incremental
290 },
291 });
292 }
293 }
294
295 let prediction = model_fn(&scenario_input);
296 let distance_from_base = self.calculate_distance(base_input, &scenario_input);
297
298 scenarios.push(Scenario {
299 id: format!("scenario_{}", i),
300 description: format!("What-if scenario {}", i),
301 features: scenario_input,
302 prediction,
303 confidence: None,
307 changed_features,
308 distance_from_base,
309 plausibility: 1.0 - (distance_from_base / 10.0).min(1.0), });
311 }
312
313 Ok(scenarios)
314 }
315
316 fn calculate_distance(
317 &self,
318 input1: &HashMap<String, f64>,
319 input2: &HashMap<String, f64>,
320 ) -> f64 {
321 input1
322 .iter()
323 .map(|(key, value)| {
324 let other_value = input2.get(key).unwrap_or(&0.0);
325 (value - other_value).powi(2)
326 })
327 .sum::<f64>()
328 .sqrt()
329 }
330
331 fn analyze_scenario_impacts(
332 &self,
333 base_scenario: &Scenario,
334 scenarios: &[Scenario],
335 ) -> ScenarioImpactAnalysis {
336 let prediction_changes: Vec<f64> = scenarios
337 .iter()
338 .map(|s| (s.prediction - base_scenario.prediction).abs())
339 .collect();
340
341 let avg_prediction_change =
342 prediction_changes.iter().sum::<f64>() / prediction_changes.len() as f64;
343 let max_prediction_change = prediction_changes.iter().cloned().fold(0.0, f64::max);
344
345 let high_impact_scenarios: Vec<String> = scenarios
346 .iter()
347 .filter(|s| {
348 (s.prediction - base_scenario.prediction).abs() > avg_prediction_change * 2.0
349 })
350 .map(|s| s.id.clone())
351 .collect();
352
353 let prediction_flip_scenarios: Vec<String> = scenarios
354 .iter()
355 .filter(|s| (s.prediction > 0.5) != (base_scenario.prediction > 0.5))
356 .map(|s| s.id.clone())
357 .collect();
358
359 let mut feature_impacts: HashMap<String, Vec<f64>> = HashMap::new();
361 for scenario in scenarios {
362 for change in &scenario.changed_features {
363 feature_impacts
364 .entry(change.feature_name.clone())
365 .or_default()
366 .push((scenario.prediction - base_scenario.prediction).abs());
367 }
368 }
369
370 let feature_importance_ranking: Vec<FeatureImportanceRank> = feature_impacts
371 .iter()
372 .enumerate()
373 .map(|(rank, (feature_name, impacts))| {
374 let avg_impact = impacts.iter().sum::<f64>() / impacts.len() as f64;
375 FeatureImportanceRank {
376 feature_name: feature_name.clone(),
377 importance_score: avg_impact,
378 rank: rank + 1,
379 avg_impact,
380 change_frequency: impacts.len(),
381 }
382 })
383 .collect();
384
385 let stability_analysis = PredictionStabilityAnalysis {
386 stability_score: 1.0
387 - (max_prediction_change / base_scenario.prediction.abs()).min(1.0),
388 prediction_variance: {
389 let predictions: Vec<f64> = scenarios.iter().map(|s| s.prediction).collect();
390 let mean = predictions.iter().sum::<f64>() / predictions.len() as f64;
391 predictions.iter().map(|p| (p - mean).powi(2)).sum::<f64>()
392 / predictions.len() as f64
393 },
394 prediction_flips: prediction_flip_scenarios.len(),
395 stability_by_magnitude: HashMap::new(),
399 };
400
401 ScenarioImpactAnalysis {
402 high_impact_scenarios,
403 prediction_flip_scenarios,
404 avg_prediction_change,
405 max_prediction_change,
406 stability_analysis,
407 feature_importance_ranking,
408 }
409 }
410
411 fn analyze_feature_sensitivity_from_scenarios(
412 &self,
413 scenarios: &[Scenario],
414 ) -> FeatureSensitivityAnalysis {
415 let mut feature_sensitivities = HashMap::new();
416 let mut feature_change_counts = HashMap::new();
417
418 for scenario in scenarios {
419 for change in &scenario.changed_features {
420 let sensitivity = change.change_magnitude / scenario.distance_from_base;
421 *feature_sensitivities.entry(change.feature_name.clone()).or_insert(0.0) +=
422 sensitivity;
423 *feature_change_counts.entry(change.feature_name.clone()).or_insert(0) += 1;
424 }
425 }
426
427 for (feature, sensitivity) in feature_sensitivities.iter_mut() {
429 let count = feature_change_counts[feature] as f64;
430 if count > 0.0 {
431 *sensitivity /= count;
432 }
433 }
434
435 let mut sorted_features: Vec<_> = feature_sensitivities.iter().collect();
436 sorted_features.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
437
438 let most_sensitive_features: Vec<String> =
439 sorted_features.iter().take(5).map(|(name, _)| (*name).clone()).collect();
440
441 let least_sensitive_features: Vec<String> =
442 sorted_features.iter().rev().take(5).map(|(name, _)| (*name).clone()).collect();
443
444 FeatureSensitivityAnalysis {
445 feature_sensitivities,
446 most_sensitive_features,
447 least_sensitive_features,
448 non_linear_features: vec![], interaction_sensitivities: vec![], }
451 }
452
453 fn generate_counterfactual_insights(
454 &self,
455 base_scenario: &Scenario,
456 scenarios: &[Scenario],
457 ) -> Vec<CounterfactualInsight> {
458 let mut insights = Vec::new();
459
460 for scenario in scenarios {
462 let prediction_change = (scenario.prediction - base_scenario.prediction).abs();
463 if prediction_change > 0.1 {
464 insights.push(CounterfactualInsight {
466 description: format!(
467 "Changing {} features can alter prediction by {:.3}",
468 scenario.changed_features.len(),
469 prediction_change
470 ),
471 required_changes: scenario.changed_features.clone(),
472 predicted_outcome: scenario.prediction,
473 confidence: scenario.confidence,
474 feasibility: if scenario.changed_features.len() <= 2 {
475 ImplementationFeasibility::Easy
476 } else {
477 ImplementationFeasibility::Moderate
478 },
479 });
480 }
481 }
482
483 insights
484 }
485
486 fn explore_decision_boundary(&self, scenarios: &[Scenario]) -> DecisionBoundaryExploration {
487 let boundary_points: Vec<BoundaryPoint> = scenarios.iter()
489 .filter(|s| (s.prediction - 0.5).abs() < 0.1) .take(10)
491 .map(|s| BoundaryPoint {
492 coordinates: s.features.clone(),
493 distance_to_boundary: (s.prediction - 0.5).abs(),
494 prediction: s.prediction,
495 gradient_direction: HashMap::new(), })
497 .collect();
498
499 DecisionBoundaryExploration {
500 boundary_points: boundary_points.clone(),
501 boundary_complexity: BoundaryComplexity {
502 complexity_score: 0.6,
503 curvature: 0.3,
504 inflection_points: 2,
505 complexity_class: ComplexityClass::Polynomial,
506 },
507 local_linearity: LocalLinearityAnalysis {
508 avg_linearity: 0.7,
509 linearity_by_region: HashMap::new(),
510 most_linear_regions: vec![],
511 most_nonlinear_regions: vec![],
512 },
513 crossing_analysis: BoundaryCrossingAnalysis {
514 crossing_count: boundary_points.len(),
515 avg_crossing_distance: boundary_points
516 .iter()
517 .map(|p| p.distance_to_boundary)
518 .sum::<f64>()
519 / boundary_points.len() as f64,
520 crossing_directions: vec![],
521 common_crossing_features: vec![],
522 },
523 }
524 }
525
526 async fn test_perturbation_intensity(
527 &self,
528 base_input: &HashMap<String, f64>,
529 model_fn: &(dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync),
530 intensity: f64,
531 ) -> Result<PerturbationIntensityResult> {
532 let base_prediction = model_fn(base_input);
533 let mut perturbation_details = Vec::new();
534 let mut successful_perturbations = 0;
535 let mut failed_perturbations = 0;
536 let mut prediction_changes = Vec::new();
537
538 for i in 0..self.config.num_perturbation_samples {
540 let perturbed_input = self.generate_perturbation(base_input, intensity);
541 let perturbed_prediction = model_fn(&perturbed_input);
542 let prediction_change = (perturbed_prediction - base_prediction).abs();
543
544 let is_successful = prediction_change < 0.1; if is_successful {
547 successful_perturbations += 1;
548 } else {
549 failed_perturbations += 1;
550 }
551
552 prediction_changes.push(prediction_change);
553
554 let perturbation_vector: HashMap<String, f64> = base_input
555 .iter()
556 .map(|(key, &base_val)| {
557 let perturbed_val = perturbed_input.get(key).unwrap_or(&base_val);
558 (key.clone(), perturbed_val - base_val)
559 })
560 .collect();
561
562 let perturbation_magnitude =
563 perturbation_vector.values().map(|&v| v.powi(2)).sum::<f64>().sqrt();
564
565 perturbation_details.push(PerturbationDetail {
566 id: format!("pert_{}_{}", intensity, i),
567 original_input: base_input.clone(),
568 perturbed_input,
569 original_prediction: base_prediction,
570 perturbed_prediction,
571 prediction_change,
572 perturbation_vector,
573 perturbation_magnitude,
574 is_successful,
575 });
576 }
577
578 let avg_prediction_change =
579 prediction_changes.iter().sum::<f64>() / prediction_changes.len() as f64;
580 let max_prediction_change = prediction_changes.iter().cloned().fold(0.0, f64::max);
581 let std_prediction_change = {
582 let variance = prediction_changes
583 .iter()
584 .map(|&x| (x - avg_prediction_change).powi(2))
585 .sum::<f64>()
586 / prediction_changes.len() as f64;
587 variance.sqrt()
588 };
589
590 Ok(PerturbationIntensityResult {
591 intensity,
592 num_perturbations: self.config.num_perturbation_samples,
593 successful_perturbations,
594 failed_perturbations,
595 avg_prediction_change,
596 max_prediction_change,
597 std_prediction_change,
598 perturbation_details,
599 })
600 }
601
602 fn generate_perturbation(
603 &self,
604 base_input: &HashMap<String, f64>,
605 intensity: f64,
606 ) -> HashMap<String, f64> {
607 let mut perturbed_input = base_input.clone();
608 use scirs2_core::random::*; let mut rng = thread_rng();
610
611 for (_key, value) in perturbed_input.iter_mut() {
612 let noise = (rng.random::<f64>() - 0.5) * 2.0 * intensity;
614 *value += noise;
615 }
616
617 perturbed_input
618 }
619
620 fn assess_robustness(
621 &self,
622 results: &HashMap<String, PerturbationIntensityResult>,
623 ) -> RobustnessAssessment {
624 let success_rates: Vec<f64> = results
626 .values()
627 .map(|r| r.successful_perturbations as f64 / r.num_perturbations as f64)
628 .collect();
629
630 let robustness_score = success_rates.iter().sum::<f64>() / success_rates.len() as f64;
631
632 let robustness_class = match robustness_score {
633 x if x > 0.9 => RobustnessClass::VeryRobust,
634 x if x > 0.7 => RobustnessClass::Robust,
635 x if x > 0.5 => RobustnessClass::SomewhatRobust,
636 x if x > 0.3 => RobustnessClass::Sensitive,
637 _ => RobustnessClass::Fragile,
638 };
639
640 let critical_threshold = results
642 .iter()
643 .find(|(_, result)| {
644 let success_rate =
645 result.successful_perturbations as f64 / result.num_perturbations as f64;
646 success_rate < 0.5
647 })
648 .map(|(intensity, _)| intensity.parse::<f64>().unwrap_or(1.0))
649 .unwrap_or(1.0);
650
651 RobustnessAssessment {
652 robustness_score,
653 robustness_class,
654 feature_robustness: HashMap::new(), critical_threshold,
656 improvement_recommendations: vec![
657 "Consider adding regularization".to_string(),
658 "Increase training data diversity".to_string(),
659 ],
660 }
661 }
662
663 fn identify_sensitivity_hotspots(
664 &self,
665 _results: &HashMap<String, PerturbationIntensityResult>,
666 ) -> Vec<SensitivityHotspot> {
667 vec![SensitivityHotspot {
669 location: HashMap::new(), sensitivity_score: 0.8,
671 sensitivity_radius: 0.1,
672 sensitive_features: vec!["feature1".to_string()],
673 hotspot_type: HotspotType::Local,
674 }]
675 }
676
677 fn analyze_failure_modes(
678 &self,
679 _results: &HashMap<String, PerturbationIntensityResult>,
680 ) -> FailureModesAnalysis {
681 let failure_modes = vec![FailureMode {
683 id: "noise_sensitivity".to_string(),
684 description: "Model sensitive to input noise".to_string(),
685 triggering_conditions: vec![TriggeringCondition {
686 feature: "any".to_string(),
687 condition_type: ConditionType::Exceeds,
688 threshold: 0.1,
689 description: "Noise level exceeds 10%".to_string(),
690 }],
691 severity: FailureSeverity::Moderate,
692 frequency: 0.3,
693 example_inputs: vec![],
694 }];
695
696 FailureModesAnalysis {
697 failure_modes,
698 failure_frequency: FailureFrequencyAnalysis {
699 overall_failure_rate: 0.3,
700 failure_rate_by_intensity: HashMap::new(),
701 failure_rate_by_feature: HashMap::new(),
702 time_to_failure: TimeToFailureAnalysis {
703 avg_time_to_failure: 5.0,
704 median_time_to_failure: 3.0,
705 distribution_parameters: HashMap::new(),
706 },
707 },
708 failure_severity: FailureSeverityAnalysis {
709 avg_severity: 2.5,
710 severity_distribution: HashMap::new(),
711 most_severe_modes: vec!["noise_sensitivity".to_string()],
712 cascading_failures: CascadingFailureAnalysis {
713 cascading_events: 0,
714 avg_cascade_length: 0.0,
715 cascade_triggers: vec![],
716 amplification_factors: HashMap::new(),
717 },
718 },
719 mitigation_strategies: vec![MitigationStrategy {
720 name: "Data Augmentation".to_string(),
721 description: "Add noise during training".to_string(),
722 target_failure_modes: vec!["noise_sensitivity".to_string()],
723 effectiveness: 0.8,
724 implementation_cost: ImplementationCost::Medium,
725 implementation_steps: vec![
726 "Add noise to training data".to_string(),
727 "Retrain model".to_string(),
728 ],
729 }],
730 }
731 }
732
733 async fn generate_adversarial_examples(
734 &self,
735 base_input: &HashMap<String, f64>,
736 model_fn: &(dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync),
737 method: &AdversarialMethod,
738 ) -> Result<Vec<AdversarialExample>> {
739 let mut examples = Vec::new();
740 let base_prediction = model_fn(base_input);
741
742 for i in 0..self.config.num_adversarial_examples {
743 let epsilon = Self::FGSM_BASE_EPSILON * (i + 1) as f64;
746 let adversarial_input = match method {
747 AdversarialMethod::FGSM => Self::fgsm_example(base_input, model_fn, epsilon),
748 AdversarialMethod::PGD => Self::pgd_example(base_input, model_fn, epsilon),
749 other => {
758 return Err(anyhow::anyhow!(
759 "adversarial method {other:?} is not implemented; only FGSM and PGD have \
760 real implementations (finite-difference gradient). Requesting {other:?} \
761 used to silently run FGSM under that name."
762 ))
763 },
764 };
765
766 let adversarial_prediction = model_fn(&adversarial_input);
767
768 let perturbation: HashMap<String, f64> = base_input
769 .iter()
770 .map(|(key, &base_val)| {
771 let adv_val = adversarial_input.get(key).unwrap_or(&base_val);
772 (key.clone(), adv_val - base_val)
773 })
774 .collect();
775
776 let perturbation_norm = perturbation.values().map(|&v| v.powi(2)).sum::<f64>().sqrt();
777
778 let is_successful =
779 (adversarial_prediction - base_prediction).abs() > Self::ATTACK_SUCCESS_THRESHOLD;
780
781 examples.push(AdversarialExample {
782 id: format!("adv_{:?}_{}", method, i),
783 attack_method: method.clone(),
784 original_input: base_input.clone(),
785 adversarial_input,
786 original_prediction: base_prediction,
787 adversarial_prediction,
788 perturbation,
789 perturbation_norm,
790 is_successful,
791 confidence: ((adversarial_prediction - base_prediction).abs()
795 / Self::ATTACK_SUCCESS_THRESHOLD)
796 .clamp(0.0, 1.0),
797 });
798 }
799
800 Ok(examples)
801 }
802
803 const FGSM_BASE_EPSILON: f64 = 0.01;
805 const ATTACK_SUCCESS_THRESHOLD: f64 = 0.1;
807 const GRADIENT_STEP: f64 = 1e-4;
809 const PGD_ITERATIONS: usize = 10;
811
812 fn numerical_gradient(
820 input: &HashMap<String, f64>,
821 model_fn: &(dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync),
822 ) -> HashMap<String, f64> {
823 let mut gradient = HashMap::with_capacity(input.len());
824 for key in input.keys() {
825 let mut probe = input.clone();
826 let original = input.get(key).copied().unwrap_or(0.0);
827
828 probe.insert(key.clone(), original + Self::GRADIENT_STEP);
829 let forward = model_fn(&probe);
830 probe.insert(key.clone(), original - Self::GRADIENT_STEP);
831 let backward = model_fn(&probe);
832
833 gradient.insert(
834 key.clone(),
835 (forward - backward) / (2.0 * Self::GRADIENT_STEP),
836 );
837 }
838 gradient
839 }
840
841 fn fgsm_example(
849 base_input: &HashMap<String, f64>,
850 model_fn: &(dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync),
851 epsilon: f64,
852 ) -> HashMap<String, f64> {
853 let gradient = Self::numerical_gradient(base_input, model_fn);
854 let mut adversarial = base_input.clone();
855 for (key, value) in adversarial.iter_mut() {
856 let sign = match gradient.get(key) {
857 Some(g) if *g > 0.0 => 1.0,
858 Some(g) if *g < 0.0 => -1.0,
859 _ => 0.0,
862 };
863 *value += epsilon * sign;
864 }
865 adversarial
866 }
867
868 fn pgd_example(
875 base_input: &HashMap<String, f64>,
876 model_fn: &(dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync),
877 epsilon: f64,
878 ) -> HashMap<String, f64> {
879 let step = epsilon / Self::PGD_ITERATIONS as f64;
880 let mut adversarial = base_input.clone();
881 for _ in 0..Self::PGD_ITERATIONS {
882 let gradient = Self::numerical_gradient(&adversarial, model_fn);
883 for (key, value) in adversarial.iter_mut() {
884 let sign = match gradient.get(key) {
885 Some(g) if *g > 0.0 => 1.0,
886 Some(g) if *g < 0.0 => -1.0,
887 _ => 0.0,
888 };
889 *value += step * sign;
890 let origin = base_input.get(key).copied().unwrap_or(*value);
892 *value = value.clamp(origin - epsilon, origin + epsilon);
893 }
894 }
895 adversarial
896 }
897
898 fn analyze_attack_success(
899 &self,
900 adversarial_examples: &HashMap<AdversarialMethod, Vec<AdversarialExample>>,
901 ) -> AttackSuccessAnalysis {
902 let mut success_rate_by_method = HashMap::new();
903 let mut total_successful = 0;
904 let mut total_examples = 0;
905 let mut total_perturbation = 0.0;
906
907 for (method, examples) in adversarial_examples {
908 let successful = examples.iter().filter(|e| e.is_successful).count();
909 let success_rate = successful as f64 / examples.len() as f64;
910
911 success_rate_by_method.insert(method.clone(), success_rate);
912 total_successful += successful;
913 total_examples += examples.len();
914
915 total_perturbation += examples.iter().map(|e| e.perturbation_norm).sum::<f64>();
916 }
917
918 let overall_success_rate = total_successful as f64 / total_examples as f64;
919 let avg_perturbation_magnitude = total_perturbation / total_examples as f64;
920
921 let most_effective_methods: Vec<AdversarialMethod> = success_rate_by_method
922 .iter()
923 .filter(|(_, &rate)| rate > 0.5)
924 .map(|(method, _)| method.clone())
925 .collect();
926
927 AttackSuccessAnalysis {
928 success_rate_by_method,
929 overall_success_rate,
930 avg_perturbation_magnitude,
931 most_effective_methods,
932 attack_difficulty: AttackDifficultyAnalysis {
933 easy_targets: vec!["feature1".to_string()],
934 hard_targets: vec!["feature2".to_string()],
935 perturbation_by_feature: HashMap::new(),
936 complexity_assessment: ComplexityAssessment {
937 complexity_score: 0.6,
938 features_required: 2,
939 min_perturbation: 0.01,
940 sophistication_level: SophisticationLevel::Intermediate,
941 },
942 },
943 }
944 }
945
946 fn assess_adversarial_robustness(
947 &self,
948 adversarial_examples: &HashMap<AdversarialMethod, Vec<AdversarialExample>>,
949 ) -> AdversarialRobustnessAssessment {
950 let robustness_by_attack: HashMap<AdversarialMethod, f64> = adversarial_examples
952 .iter()
953 .map(|(method, examples)| {
954 let failed_attacks = examples.iter().filter(|e| !e.is_successful).count();
955 let robustness = failed_attacks as f64 / examples.len() as f64;
956 (method.clone(), robustness)
957 })
958 .collect();
959
960 let overall_robustness =
961 robustness_by_attack.values().sum::<f64>() / robustness_by_attack.len() as f64;
962
963 AdversarialRobustnessAssessment {
964 robustness_score: overall_robustness,
965 robustness_by_attack,
966 vulnerability_hotspots: vec![], certified_robustness: CertifiedRobustnessAnalysis {
968 certified_radius: 0.01,
969 certification_confidence: 0.8,
970 certification_method: "Simplified".to_string(),
971 robustness_guarantees: vec![],
972 },
973 }
974 }
975
976 fn generate_defense_recommendations(
977 &self,
978 _adversarial_examples: &HashMap<AdversarialMethod, Vec<AdversarialExample>>,
979 ) -> Vec<DefenseRecommendation> {
980 vec![
981 DefenseRecommendation {
982 name: "Adversarial Training".to_string(),
983 description: "Train with adversarial examples".to_string(),
984 target_vulnerabilities: vec!["FGSM".to_string(), "PGD".to_string()],
985 effectiveness: 0.8,
986 complexity: DefenseComplexity::Moderate,
987 performance_impact: PerformanceImpact::Medium,
988 },
989 DefenseRecommendation {
990 name: "Input Preprocessing".to_string(),
991 description: "Add noise reduction preprocessing".to_string(),
992 target_vulnerabilities: vec!["All".to_string()],
993 effectiveness: 0.6,
994 complexity: DefenseComplexity::Simple,
995 performance_impact: PerformanceImpact::Low,
996 },
997 ]
998 }
999
1000 async fn search_edge_cases(
1001 &self,
1002 input_space: &HashMap<String, (f64, f64)>,
1003 model_fn: &(dyn Fn(&HashMap<String, f64>) -> f64 + Send + Sync),
1004 ) -> Result<Vec<EdgeCase>> {
1005 let mut edge_cases = Vec::new();
1006 use scirs2_core::random::*; let mut rng = thread_rng();
1008
1009 for i in 0..100 {
1011 let mut test_input = HashMap::new();
1013
1014 for (feature, (min_val, max_val)) in input_space {
1016 let value = if rng.random::<f64>() > 0.5 {
1017 *min_val + (*max_val - *min_val) * 0.01 } else {
1019 *max_val - (*max_val - *min_val) * 0.01 };
1021 test_input.insert(feature.clone(), value);
1022 }
1023
1024 let prediction = model_fn(&test_input);
1025
1026 if !(0.1..=0.9).contains(&prediction) || prediction.is_nan() {
1028 edge_cases.push(EdgeCase {
1029 id: format!("edge_{}", i),
1030 description: format!("Edge case with extreme prediction: {:.3}", prediction),
1031 trigger_input: test_input,
1032 model_output: prediction,
1033 expected_output: None,
1034 edge_case_type: if prediction.is_nan() {
1035 EdgeCaseType::ModelConfusion
1036 } else if !(0.1..=0.9).contains(&prediction) {
1037 EdgeCaseType::DistributionBoundary
1038 } else {
1039 EdgeCaseType::Outlier
1040 },
1041 severity: if prediction.is_nan() {
1042 EdgeCaseSeverity::Critical
1043 } else {
1044 EdgeCaseSeverity::Medium
1045 },
1046 likelihood: 0.1, detection_method: "Boundary exploration".to_string(),
1048 });
1049 }
1050 }
1051
1052 Ok(edge_cases)
1053 }
1054
1055 fn classify_edge_cases(&self, edge_cases: &[EdgeCase]) -> EdgeCaseClassification {
1056 let mut by_type = HashMap::new();
1057 let mut by_severity = HashMap::new();
1058
1059 for edge_case in edge_cases {
1060 *by_type.entry(edge_case.edge_case_type.clone()).or_insert(0) += 1;
1061 *by_severity.entry(edge_case.severity.clone()).or_insert(0) += 1;
1062 }
1063
1064 EdgeCaseClassification {
1065 by_type,
1066 by_severity,
1067 common_patterns: vec![], systematic_issues: vec![], }
1070 }
1071
1072 fn analyze_edge_case_coverage(
1073 &self,
1074 _edge_cases: &[EdgeCase],
1075 _input_space: &HashMap<String, (f64, f64)>,
1076 ) -> CoverageAnalysis {
1077 CoverageAnalysis {
1079 feature_space_coverage: 0.3, boundary_coverage: 0.8, uncovered_regions: vec![], coverage_gaps: vec![], }
1084 }
1085
1086 fn assess_edge_case_risks(&self, edge_cases: &[EdgeCase]) -> EdgeCaseRiskAssessment {
1087 let overall_risk = edge_cases
1088 .iter()
1089 .map(|ec| match ec.severity {
1090 EdgeCaseSeverity::Critical => 1.0,
1091 EdgeCaseSeverity::High => 0.8,
1092 EdgeCaseSeverity::Medium => 0.5,
1093 EdgeCaseSeverity::Low => 0.2,
1094 })
1095 .sum::<f64>()
1096 / edge_cases.len() as f64;
1097
1098 let high_risk_cases: Vec<String> = edge_cases
1099 .iter()
1100 .filter(|ec| {
1101 matches!(
1102 ec.severity,
1103 EdgeCaseSeverity::High | EdgeCaseSeverity::Critical
1104 )
1105 })
1106 .map(|ec| ec.id.clone())
1107 .collect();
1108
1109 EdgeCaseRiskAssessment {
1110 overall_risk,
1111 risk_by_type: HashMap::new(), high_risk_cases,
1113 mitigation_priorities: vec![], }
1115 }
1116
1117 fn generate_simulation_summary(&self) -> HashMap<String, String> {
1118 let mut summary = HashMap::new();
1119
1120 summary.insert(
1121 "total_what_if_analyses".to_string(),
1122 self.what_if_results.len().to_string(),
1123 );
1124 summary.insert(
1125 "total_perturbation_tests".to_string(),
1126 self.perturbation_results.len().to_string(),
1127 );
1128 summary.insert(
1129 "total_adversarial_probes".to_string(),
1130 self.adversarial_results.len().to_string(),
1131 );
1132 summary.insert(
1133 "total_edge_case_discoveries".to_string(),
1134 self.edge_case_results.len().to_string(),
1135 );
1136
1137 if let Some(latest_perturbation) = self.perturbation_results.last() {
1138 summary.insert(
1139 "latest_robustness_score".to_string(),
1140 format!(
1141 "{:.2}",
1142 latest_perturbation.robustness_assessment.robustness_score
1143 ),
1144 );
1145 }
1146
1147 if let Some(latest_adversarial) = self.adversarial_results.last() {
1148 summary.insert(
1149 "latest_adversarial_robustness".to_string(),
1150 format!(
1151 "{:.2}",
1152 latest_adversarial.robustness_assessment.robustness_score
1153 ),
1154 );
1155 }
1156
1157 summary
1158 }
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163 use super::*;
1164
1165 #[test]
1171 fn fgsm_steps_along_the_real_gradient_sign() {
1172 let model = |x: &HashMap<String, f64>| {
1173 3.0 * x.get("a").copied().unwrap_or(0.0) - 5.0 * x.get("b").copied().unwrap_or(0.0)
1174 };
1175 let base: HashMap<String, f64> =
1176 [("a".to_string(), 1.0), ("b".to_string(), 1.0)].into_iter().collect();
1177
1178 let adversarial = SimulationAnalyzer::fgsm_example(&base, &model, 0.1);
1179 assert!(
1180 (adversarial["a"] - 1.1).abs() < 1e-9,
1181 "positive gradient => step up: {}",
1182 adversarial["a"]
1183 );
1184 assert!(
1185 (adversarial["b"] - 0.9).abs() < 1e-9,
1186 "negative gradient => step down: {}",
1187 adversarial["b"]
1188 );
1189 assert!(model(&adversarial) > model(&base));
1191 }
1192
1193 #[test]
1194 fn fgsm_leaves_gradient_free_inputs_untouched() {
1195 let model = |x: &HashMap<String, f64>| x.get("a").copied().unwrap_or(0.0);
1198 let base: HashMap<String, f64> =
1199 [("a".to_string(), 0.0), ("c".to_string(), 7.0)].into_iter().collect();
1200 let adversarial = SimulationAnalyzer::fgsm_example(&base, &model, 0.5);
1201 assert!(
1202 (adversarial["c"] - 7.0).abs() < 1e-12,
1203 "zero-gradient input must not move"
1204 );
1205 assert!((adversarial["a"] - 0.5).abs() < 1e-9);
1206 }
1207
1208 #[test]
1212 fn pgd_stays_inside_the_l_infinity_ball() {
1213 let model = |x: &HashMap<String, f64>| x.values().sum::<f64>();
1214 let base: HashMap<String, f64> =
1215 [("a".to_string(), 0.0), ("b".to_string(), 0.0)].into_iter().collect();
1216 let epsilon = 0.05;
1217 let adversarial = SimulationAnalyzer::pgd_example(&base, &model, epsilon);
1218 for (key, value) in &adversarial {
1219 let delta = (value - base[key]).abs();
1220 assert!(
1221 delta <= epsilon + 1e-9,
1222 "{key} moved {delta}, outside the {epsilon} L-inf ball"
1223 );
1224 }
1225 }
1226
1227 #[tokio::test]
1228 async fn unimplemented_attacks_are_refused_not_aliased_to_fgsm() {
1229 let analyzer = SimulationAnalyzer::new(SimulationConfig::default());
1230 let model = |x: &HashMap<String, f64>| x.values().sum::<f64>();
1231 let base: HashMap<String, f64> = [("a".to_string(), 1.0)].into_iter().collect();
1232 for method in [
1233 AdversarialMethod::CW,
1234 AdversarialMethod::DeepFool,
1235 AdversarialMethod::UAP,
1236 AdversarialMethod::Boundary,
1237 ] {
1238 let err = analyzer
1239 .generate_adversarial_examples(&base, &model, &method)
1240 .await
1241 .expect_err("{method:?} has no implementation");
1242 assert!(
1243 err.to_string().contains("not implemented"),
1244 "{method:?}: {err}"
1245 );
1246 }
1247 assert!(analyzer
1249 .generate_adversarial_examples(&base, &model, &AdversarialMethod::FGSM)
1250 .await
1251 .is_ok());
1252 }
1253
1254 #[tokio::test]
1255 async fn test_simulation_analyzer_creation() {
1256 let config = SimulationConfig::default();
1257 let analyzer = SimulationAnalyzer::new(config);
1258 assert_eq!(analyzer.what_if_results.len(), 0);
1259 }
1260
1261 #[tokio::test]
1262 async fn test_what_if_analysis() {
1263 let config = SimulationConfig::default();
1264 let mut analyzer = SimulationAnalyzer::new(config);
1265
1266 let mut base_input = HashMap::new();
1267 base_input.insert("feature1".to_string(), 1.0);
1268 base_input.insert("feature2".to_string(), 2.0);
1269
1270 let model_fn =
1271 Box::new(|input: &HashMap<String, f64>| -> f64 { input.values().sum::<f64>() * 0.1 });
1272
1273 let result = analyzer.analyze_what_if(&base_input, model_fn).await;
1274 assert!(result.is_ok());
1275 }
1276
1277 #[tokio::test]
1278 async fn test_perturbation_testing() {
1279 let config = SimulationConfig::default();
1280 let mut analyzer = SimulationAnalyzer::new(config);
1281
1282 let mut base_input = HashMap::new();
1283 base_input.insert("feature1".to_string(), 1.0);
1284 base_input.insert("feature2".to_string(), 2.0);
1285
1286 let model_fn =
1287 Box::new(|input: &HashMap<String, f64>| -> f64 { input.values().sum::<f64>() * 0.1 });
1288
1289 let result = analyzer.test_perturbations(&base_input, model_fn).await;
1290 assert!(result.is_ok());
1291 }
1292}