Skip to main content

trustformers_debug/simulation_tools/
analyzer.rs

1//! Main Simulation Analyzer Implementation
2//!
3//! This module provides the core SimulationAnalyzer that orchestrates all simulation
4//! analysis capabilities including what-if analysis, perturbation testing, adversarial
5//! probing, and edge case discovery.
6
7use 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/// Main simulation tools analyzer
18#[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    /// Create a new simulation analyzer
29    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    /// Perform what-if analysis
40    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` here would be the MODEL's confidence in its own
56            // prediction, which `model_fn` (a bare `-> f64`) never reports --
57            // for the base scenario exactly as much as for a perturbed one. It
58            // used to be `1.0`, asserting perfect certainty.
59            confidence: None,
60            changed_features: vec![],
61            distance_from_base: 0.0,
62            plausibility: 1.0,
63        };
64
65        // Generate what-if scenarios
66        let scenarios = self.generate_what_if_scenarios(base_input, &model_fn).await?;
67
68        // Analyze scenario impacts
69        let impact_analysis = self.analyze_scenario_impacts(&base_scenario, &scenarios);
70
71        // Perform sensitivity analysis
72        let sensitivity_analysis = self.analyze_feature_sensitivity_from_scenarios(&scenarios);
73
74        // Generate counterfactual insights
75        let counterfactual_insights =
76            self.generate_counterfactual_insights(&base_scenario, &scenarios);
77
78        // Explore decision boundary
79        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    /// Perform perturbation testing
96    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        // Test different perturbation intensities
108        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        // Assess overall robustness
115        let robustness_assessment = self.assess_robustness(&results_by_intensity);
116
117        // Identify sensitivity hotspots
118        let sensitivity_hotspots = self.identify_sensitivity_hotspots(&results_by_intensity);
119
120        // Analyze failure modes
121        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    /// Perform adversarial probing
137    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        // Generate adversarial examples using different methods
149        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        // Analyze attack success
156        let attack_success_analysis = self.analyze_attack_success(&adversarial_examples);
157
158        // Assess adversarial robustness
159        let robustness_assessment = self.assess_adversarial_robustness(&adversarial_examples);
160
161        // Generate defense recommendations
162        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    /// Discover edge cases
178    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        // Search for edge cases using different strategies
188        let edge_cases = self.search_edge_cases(input_space, &model_fn).await?;
189
190        // Classify edge cases
191        let classification = self.classify_edge_cases(&edge_cases);
192
193        // Analyze coverage
194        let coverage_analysis = self.analyze_edge_case_coverage(&edge_cases, input_space);
195
196        // Assess risks
197        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    /// Generate comprehensive simulation report
212    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    // Helper methods.
247
248    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            // Randomly modify features
261            use scirs2_core::random::*; // SciRS2 Integration Policy
262            let mut rng = thread_rng();
263            let num_features_to_change = 1 + (rng.random_range(0..3)); // 1-3 features
264            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); // 0.8-1.2 multiplier
270                    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                // No confidence is derivable: `model_fn` returns a bare
304                // scalar prediction with no uncertainty, and the scenario is
305                // evaluated exactly once. Previously a flat 0.8.
306                confidence: None,
307                changed_features,
308                distance_from_base,
309                plausibility: 1.0 - (distance_from_base / 10.0).min(1.0), // Simple plausibility
310            });
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        // Feature importance analysis
360        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            // Per-magnitude stability would need the perturbation sweep
396            // re-run and bucketed by magnitude; this pass evaluates one
397            // magnitude per scenario, so there is nothing to bucket.
398            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        // Average sensitivities
428        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![], // Would detect non-linearity
449            interaction_sensitivities: vec![], // Would compute interactions
450        }
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        // Find scenarios with significant prediction changes
461        for scenario in scenarios {
462            let prediction_change = (scenario.prediction - base_scenario.prediction).abs();
463            if prediction_change > 0.1 {
464                // Significant change threshold
465                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        // Simplified boundary exploration
488        let boundary_points: Vec<BoundaryPoint> = scenarios.iter()
489            .filter(|s| (s.prediction - 0.5).abs() < 0.1) // Near decision boundary
490            .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(), // Would compute actual gradient
496            })
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        // Generate perturbations
539        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; // Threshold for "successful" perturbation
545
546            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::*; // SciRS2 Integration Policy
609        let mut rng = thread_rng();
610
611        for (_key, value) in perturbed_input.iter_mut() {
612            // Add Gaussian noise proportional to intensity
613            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        // Calculate overall robustness score
625        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        // Find critical threshold
641        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(), // Would compute per-feature robustness
655            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        // Simplified hotspot identification
668        vec![SensitivityHotspot {
669            location: HashMap::new(), // Would identify actual locations
670            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        // Simplified failure mode analysis
682        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            // `epsilon` grows with the example index so a batch sweeps a range
744            // of perturbation budgets instead of repeating one attack.
745            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                // C&W (Carlini-Wagner), DeepFool, UAP and the Boundary attack
750                // are distinct algorithms -- a Lagrangian optimisation, an
751                // iterative linearisation to the decision boundary, a
752                // cross-input universal perturbation, and a decision-based
753                // random walk respectively. None is implemented here. They used
754                // to call `generate_fgsm_example` and be reported under their
755                // own names, so a caller comparing "six attacks" was really
756                // comparing six runs of the same one.
757                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                // Real confidence: how far past the success threshold the
792                // prediction moved, saturating at 1. Previously a flat 0.8 for
793                // every example, successful or not.
794                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    /// Base L-infinity perturbation budget for the first generated example.
804    const FGSM_BASE_EPSILON: f64 = 0.01;
805    /// Prediction change beyond which an attack counts as successful.
806    const ATTACK_SUCCESS_THRESHOLD: f64 = 0.1;
807    /// Step size for the central-difference gradient estimate.
808    const GRADIENT_STEP: f64 = 1e-4;
809    /// Iterations for the PGD loop.
810    const PGD_ITERATIONS: usize = 10;
811
812    /// Central-difference estimate of `d model_fn / d input[key]` for every
813    /// key, using `2 * d` model evaluations.
814    ///
815    /// The model is a black-box `Fn(&HashMap<String, f64>) -> f64`, so no
816    /// analytic gradient is available; the central difference has error
817    /// `O(h^2 * |f'''|)`, which is what makes this a real -- if numerical --
818    /// gradient rather than a guess.
819    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    /// Real FGSM (Goodfellow, Shlens & Szegedy 2015): step `epsilon` along the
842    /// SIGN of the input gradient.
843    ///
844    /// The gradient comes from [`Self::numerical_gradient`]. The previous
845    /// implementation ignored `model_fn` entirely and stepped in a RANDOM sign
846    /// direction, which is a random perturbation, not a gradient attack -- and
847    /// so was uninformative about the model's actual sensitivity.
848    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                // Zero (or missing) gradient: this input has no first-order
860                // influence, so perturbing it is not an attack.
861                _ => 0.0,
862            };
863            *value += epsilon * sign;
864        }
865        adversarial
866    }
867
868    /// Real PGD (Madry et al. 2018): iterated FGSM steps of size
869    /// `epsilon / iterations`, each projected back into the L-infinity ball of
870    /// radius `epsilon` around the original input.
871    ///
872    /// The previous implementation looped random-sign steps with no projection
873    /// at all, so the perturbation was an unbounded random walk.
874    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                // Projection onto the L-infinity ball around the original.
891                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        // Calculate robustness scores by attack method
951        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![], // Would identify actual hotspots
967            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::*; // SciRS2 Integration Policy
1007        let mut rng = thread_rng();
1008
1009        // Search strategies: boundary exploration, outlier generation, etc.
1010        for i in 0..100 {
1011            // Simplified edge case search
1012            let mut test_input = HashMap::new();
1013
1014            // Generate boundary/extreme inputs
1015            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 // Near minimum
1018                } else {
1019                    *max_val - (*max_val - *min_val) * 0.01 // Near maximum
1020                };
1021                test_input.insert(feature.clone(), value);
1022            }
1023
1024            let prediction = model_fn(&test_input);
1025
1026            // Check if this is an edge case (extreme prediction, unexpected behavior, etc.)
1027            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, // Low likelihood for edge cases
1047                    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![],   // Would analyze patterns
1068            systematic_issues: vec![], // Would identify systematic issues
1069        }
1070    }
1071
1072    fn analyze_edge_case_coverage(
1073        &self,
1074        _edge_cases: &[EdgeCase],
1075        _input_space: &HashMap<String, (f64, f64)>,
1076    ) -> CoverageAnalysis {
1077        // Simplified coverage analysis
1078        CoverageAnalysis {
1079            feature_space_coverage: 0.3, // Low coverage is expected for edge cases
1080            boundary_coverage: 0.8,      // High boundary coverage
1081            uncovered_regions: vec![],   // Would identify uncovered regions
1082            coverage_gaps: vec![],       // Would identify gaps
1083        }
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(), // Would compute risk by type
1112            high_risk_cases,
1113            mitigation_priorities: vec![], // Would generate priorities
1114        }
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    // ---- Wave 6c debug-sweep2: real gradient-based attacks ---------------
1166
1167    /// `f(x) = 3*a - 5*b`: gradient is (+3, -5), so FGSM must step `a` UP and
1168    /// `b` DOWN by exactly epsilon. The old implementation stepped both in a
1169    /// random direction and never consulted `model_fn` at all.
1170    #[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        // And the attack must really increase the model output.
1190        assert!(model(&adversarial) > model(&base));
1191    }
1192
1193    #[test]
1194    fn fgsm_leaves_gradient_free_inputs_untouched() {
1195        // `c` does not appear in the model, so its gradient is 0 and
1196        // perturbing it would not be an attack.
1197        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    /// PGD must project every iterate back into the L-infinity ball. The old
1209    /// loop had no projection at all, so 10 random steps of `epsilon` each
1210    /// wandered up to `10 * epsilon` away.
1211    #[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        // FGSM and PGD really do run.
1248        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}