Skip to main content

quantrs2_circuit/
scirs2_benchmarking.rs

1//! `SciRS2` statistical tools for circuit benchmarking
2//!
3//! This module leverages `SciRS2`'s advanced statistical analysis capabilities to provide
4//! comprehensive benchmarking, performance analysis, and statistical insights for quantum circuits.
5
6use crate::builder::Circuit;
7use crate::noise_models::{NoiseAnalysisResult, NoiseAnalyzer, NoiseModel};
8use crate::simulator_interface::{ExecutionResult, SimulatorBackend};
9use quantrs2_core::{
10    error::{QuantRS2Error, QuantRS2Result},
11    gate::GateOp,
12};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::time::{Duration, Instant};
16
17// Placeholder types representing SciRS2 statistical interface
18// In the real implementation, these would be imported from SciRS2
19
20/// Statistical distribution types supported by `SciRS2`
21#[derive(Debug, Clone, PartialEq)]
22pub enum Distribution {
23    /// Normal distribution
24    Normal { mean: f64, std_dev: f64 },
25    /// Uniform distribution
26    Uniform { min: f64, max: f64 },
27    /// Exponential distribution
28    Exponential { rate: f64 },
29    /// Beta distribution
30    Beta { alpha: f64, beta: f64 },
31    /// Gamma distribution
32    Gamma { shape: f64, scale: f64 },
33    /// Poisson distribution
34    Poisson { lambda: f64 },
35    /// Chi-squared distribution
36    ChiSquared { degrees_of_freedom: usize },
37    /// Student's t-distribution
38    StudentT { degrees_of_freedom: usize },
39}
40
41/// Statistical test types
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum StatisticalTest {
44    /// Kolmogorov-Smirnov test
45    KolmogorovSmirnov,
46    /// Anderson-Darling test
47    AndersonDarling,
48    /// Shapiro-Wilk test for normality
49    ShapiroWilk,
50    /// Mann-Whitney U test
51    MannWhitney,
52    /// Wilcoxon signed-rank test
53    Wilcoxon,
54    /// Chi-squared goodness of fit
55    ChiSquaredGoodnessOfFit,
56    /// ANOVA F-test
57    ANOVA,
58    /// Kruskal-Wallis test
59    KruskalWallis,
60}
61
62/// Hypothesis test result
63#[derive(Debug, Clone)]
64pub struct HypothesisTestResult {
65    /// Test statistic value
66    pub test_statistic: f64,
67    /// P-value
68    pub p_value: f64,
69    /// Critical value at significance level
70    pub critical_value: f64,
71    /// Whether null hypothesis is rejected
72    pub reject_null: bool,
73    /// Significance level used
74    pub significance_level: f64,
75    /// Effect size (if applicable)
76    pub effect_size: Option<f64>,
77    /// Confidence interval
78    pub confidence_interval: Option<(f64, f64)>,
79}
80
81/// Descriptive statistics
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct DescriptiveStats {
84    /// Sample size
85    pub count: usize,
86    /// Mean
87    pub mean: f64,
88    /// Standard deviation
89    pub std_dev: f64,
90    /// Variance
91    pub variance: f64,
92    /// Minimum value
93    pub min: f64,
94    /// Maximum value
95    pub max: f64,
96    /// Median (50th percentile)
97    pub median: f64,
98    /// First quartile (25th percentile)
99    pub q1: f64,
100    /// Third quartile (75th percentile)
101    pub q3: f64,
102    /// Interquartile range
103    pub iqr: f64,
104    /// Skewness
105    pub skewness: f64,
106    /// Kurtosis
107    pub kurtosis: f64,
108    /// Mode (most frequent value)
109    pub mode: Option<f64>,
110}
111
112/// Benchmarking configuration
113#[derive(Debug, Clone)]
114pub struct BenchmarkConfig {
115    /// Number of benchmark runs
116    pub num_runs: usize,
117    /// Warm-up runs to exclude from statistics
118    pub warmup_runs: usize,
119    /// Timeout per run
120    pub timeout: Duration,
121    /// Significance level for statistical tests
122    pub significance_level: f64,
123    /// Whether to collect detailed timing data
124    pub collect_timing: bool,
125    /// Whether to collect memory usage data
126    pub collect_memory: bool,
127    /// Whether to collect error statistics
128    pub collect_errors: bool,
129    /// Random seed for reproducible benchmarks
130    pub seed: Option<u64>,
131}
132
133impl Default for BenchmarkConfig {
134    fn default() -> Self {
135        Self {
136            num_runs: 100,
137            warmup_runs: 10,
138            timeout: Duration::from_secs(60),
139            significance_level: 0.05,
140            collect_timing: true,
141            collect_memory: false,
142            collect_errors: true,
143            seed: None,
144        }
145    }
146}
147
148/// Circuit benchmarking suite using `SciRS2` statistical tools
149pub struct CircuitBenchmark {
150    /// Benchmark configuration
151    config: BenchmarkConfig,
152    /// Collected benchmark data
153    benchmark_data: Vec<BenchmarkRun>,
154    /// Statistical analyzer
155    stats_analyzer: StatisticalAnalyzer,
156}
157
158/// Single benchmark run data
159#[derive(Debug, Clone)]
160pub struct BenchmarkRun {
161    /// Run identifier
162    pub run_id: usize,
163    /// Execution time
164    pub execution_time: Duration,
165    /// Memory usage in bytes
166    pub memory_usage: Option<usize>,
167    /// Success/failure status
168    pub success: bool,
169    /// Error message if failed
170    pub error_message: Option<String>,
171    /// Circuit metrics
172    pub circuit_metrics: CircuitMetrics,
173    /// Execution results
174    pub execution_results: Option<ExecutionResult>,
175    /// Noise analysis results
176    pub noise_analysis: Option<NoiseAnalysisResult>,
177    /// Custom metrics
178    pub custom_metrics: HashMap<String, f64>,
179}
180
181/// Circuit performance metrics
182#[derive(Debug, Clone)]
183pub struct CircuitMetrics {
184    /// Circuit depth
185    pub depth: usize,
186    /// Total gate count
187    pub gate_count: usize,
188    /// Gate count by type
189    pub gate_counts: HashMap<String, usize>,
190    /// Two-qubit gate count
191    pub two_qubit_gates: usize,
192    /// Circuit fidelity estimate
193    pub fidelity: Option<f64>,
194    /// Error rate estimate
195    pub error_rate: Option<f64>,
196}
197
198/// Comprehensive benchmark report
199#[derive(Debug, Clone)]
200pub struct BenchmarkReport {
201    /// Benchmark configuration used
202    pub config: BenchmarkConfig,
203    /// Total runs completed
204    pub completed_runs: usize,
205    /// Success rate
206    pub success_rate: f64,
207    /// Timing statistics
208    pub timing_stats: DescriptiveStats,
209    /// Raw per-run timing samples (successful runs only), retained so later
210    /// baseline comparisons can operate on the real distribution rather than
211    /// a single summary statistic.
212    pub timing_samples: Vec<f64>,
213    /// Memory statistics (if collected)
214    pub memory_stats: Option<DescriptiveStats>,
215    /// Performance regression analysis
216    pub regression_analysis: Option<RegressionAnalysis>,
217    /// Distribution fitting results
218    pub distribution_fit: Option<DistributionFit>,
219    /// Outlier analysis
220    pub outlier_analysis: OutlierAnalysis,
221    /// Performance comparison with baseline
222    pub baseline_comparison: Option<BaselineComparison>,
223    /// Statistical test results
224    pub statistical_tests: Vec<HypothesisTestResult>,
225    /// Performance insights and recommendations
226    pub insights: Vec<PerformanceInsight>,
227}
228
229/// Regression analysis results
230#[derive(Debug, Clone)]
231pub struct RegressionAnalysis {
232    /// Linear regression slope
233    pub slope: f64,
234    /// Y-intercept
235    pub intercept: f64,
236    /// R-squared correlation coefficient
237    pub r_squared: f64,
238    /// P-value for slope significance
239    pub slope_p_value: f64,
240    /// Whether there's a significant trend
241    pub significant_trend: bool,
242    /// Predicted performance degradation per run
243    pub degradation_per_run: f64,
244}
245
246/// Distribution fitting analysis
247#[derive(Debug, Clone)]
248pub struct DistributionFit {
249    /// Best fitting distribution
250    pub best_distribution: Distribution,
251    /// Goodness of fit score
252    pub goodness_of_fit: f64,
253    /// P-value for fit test
254    pub fit_p_value: f64,
255    /// Alternative distributions tested
256    pub alternative_fits: Vec<(Distribution, f64)>,
257}
258
259/// Outlier detection and analysis
260#[derive(Debug, Clone)]
261pub struct OutlierAnalysis {
262    /// Number of outliers detected
263    pub num_outliers: usize,
264    /// Outlier indices
265    pub outlier_indices: Vec<usize>,
266    /// Outlier detection method used
267    pub detection_method: OutlierDetectionMethod,
268    /// Outlier threshold used
269    pub threshold: f64,
270    /// Impact of outliers on statistics
271    pub outlier_impact: OutlierImpact,
272}
273
274/// Outlier detection methods
275#[derive(Debug, Clone, PartialEq)]
276pub enum OutlierDetectionMethod {
277    /// Interquartile range method
278    IQR { multiplier: f64 },
279    /// Z-score method
280    ZScore { threshold: f64 },
281    /// Modified Z-score (median absolute deviation)
282    ModifiedZScore { threshold: f64 },
283    /// Isolation forest
284    IsolationForest,
285    /// Local outlier factor
286    LocalOutlierFactor,
287}
288
289/// Impact of outliers on statistical measures
290#[derive(Debug, Clone)]
291pub struct OutlierImpact {
292    /// Change in mean when outliers removed
293    pub mean_change: f64,
294    /// Change in standard deviation when outliers removed
295    pub std_dev_change: f64,
296    /// Change in median when outliers removed
297    pub median_change: f64,
298    /// Relative impact percentage
299    pub relative_impact: f64,
300}
301
302/// Baseline performance comparison
303#[derive(Debug, Clone)]
304pub struct BaselineComparison {
305    /// Baseline benchmark name
306    pub baseline_name: String,
307    /// Performance improvement/degradation factor
308    pub performance_factor: f64,
309    /// Statistical significance of difference
310    pub significance: HypothesisTestResult,
311    /// Confidence interval for difference
312    pub difference_ci: (f64, f64),
313    /// Effect size
314    pub effect_size: f64,
315    /// Practical significance assessment
316    pub practical_significance: PracticalSignificance,
317}
318
319/// Practical significance assessment
320#[derive(Debug, Clone, PartialEq, Eq)]
321pub enum PracticalSignificance {
322    /// Negligible difference
323    Negligible,
324    /// Small effect
325    Small,
326    /// Medium effect
327    Medium,
328    /// Large effect
329    Large,
330    /// Very large effect
331    VeryLarge,
332}
333
334/// Performance insights and recommendations
335#[derive(Debug, Clone)]
336pub struct PerformanceInsight {
337    /// Insight category
338    pub category: InsightCategory,
339    /// Insight message
340    pub message: String,
341    /// Confidence level (0.0 to 1.0)
342    pub confidence: f64,
343    /// Supporting evidence
344    pub evidence: Vec<String>,
345    /// Recommended actions
346    pub recommendations: Vec<String>,
347}
348
349/// Performance insight categories
350#[derive(Debug, Clone, PartialEq, Eq)]
351pub enum InsightCategory {
352    /// Performance degradation detected
353    PerformanceDegradation,
354    /// Performance improvement detected
355    PerformanceImprovement,
356    /// High variability in results
357    HighVariability,
358    /// Outliers detected
359    OutliersDetected,
360    /// Memory usage concerns
361    MemoryUsage,
362    /// Error rate concerns
363    ErrorRate,
364    /// Circuit optimization opportunity
365    OptimizationOpportunity,
366}
367
368impl CircuitBenchmark {
369    /// Create a new circuit benchmark suite
370    #[must_use]
371    pub const fn new(config: BenchmarkConfig) -> Self {
372        Self {
373            config,
374            benchmark_data: Vec::new(),
375            stats_analyzer: StatisticalAnalyzer::new(),
376        }
377    }
378
379    /// Run comprehensive benchmark suite
380    pub fn run_benchmark<const N: usize>(
381        &mut self,
382        circuit: &Circuit<N>,
383        simulator: &dyn SimulatorExecutor,
384        noise_model: Option<&NoiseModel>,
385    ) -> QuantRS2Result<BenchmarkReport> {
386        self.benchmark_data.clear();
387
388        let total_runs = self.config.num_runs + self.config.warmup_runs;
389
390        for run_id in 0..total_runs {
391            let is_warmup = run_id < self.config.warmup_runs;
392
393            match self.run_single_benchmark(circuit, simulator, noise_model, run_id) {
394                Ok(run_data) => {
395                    if !is_warmup {
396                        self.benchmark_data.push(run_data);
397                    }
398                }
399                Err(e) => {
400                    if !is_warmup {
401                        // Record failed run
402                        let failed_run = BenchmarkRun {
403                            run_id,
404                            execution_time: Duration::from_millis(0),
405                            memory_usage: None,
406                            success: false,
407                            error_message: Some(e.to_string()),
408                            circuit_metrics: self.calculate_circuit_metrics(circuit),
409                            execution_results: None,
410                            noise_analysis: None,
411                            custom_metrics: HashMap::new(),
412                        };
413                        self.benchmark_data.push(failed_run);
414                    }
415                }
416            }
417        }
418
419        self.generate_benchmark_report()
420    }
421
422    /// Run a single benchmark iteration
423    fn run_single_benchmark<const N: usize>(
424        &self,
425        circuit: &Circuit<N>,
426        simulator: &dyn SimulatorExecutor,
427        noise_model: Option<&NoiseModel>,
428        run_id: usize,
429    ) -> QuantRS2Result<BenchmarkRun> {
430        let start_time = Instant::now();
431        let start_memory = if self.config.collect_memory {
432            Some(self.get_memory_usage())
433        } else {
434            None
435        };
436
437        // Actually execute the circuit through the injected simulator so the
438        // measured execution_time reflects real work, and the captured
439        // ExecutionResult reflects what the simulator produced.
440        let execution_outcome = simulator.execute(circuit as &dyn std::any::Any);
441
442        let end_time = Instant::now();
443        let end_memory = if self.config.collect_memory {
444            Some(self.get_memory_usage())
445        } else {
446            None
447        };
448
449        let execution_time = end_time - start_time;
450        let memory_usage = match (start_memory, end_memory) {
451            (Some(start), Some(end)) => Some(end.saturating_sub(start)),
452            _ => None,
453        };
454
455        let (success, error_message, execution_results) = match execution_outcome {
456            Ok(result) => (true, None, Some(result)),
457            Err(e) => (false, Some(e.to_string()), None),
458        };
459
460        // Perform real noise analysis when a noise model is provided, by
461        // registering it with a `NoiseAnalyzer` and running its full
462        // circuit-noise analysis (gate errors, decoherence, readout,
463        // crosstalk) against the actual circuit.
464        let noise_analysis = if let Some(noise) = noise_model {
465            let mut analyzer = NoiseAnalyzer::new();
466            let device_key = format!("__circuit_benchmark_run_{run_id}__");
467            analyzer.add_noise_model(device_key.clone(), noise.clone());
468            Some(analyzer.analyze_circuit_noise(circuit, &device_key)?)
469        } else {
470            None
471        };
472
473        Ok(BenchmarkRun {
474            run_id,
475            execution_time,
476            memory_usage,
477            success,
478            error_message,
479            circuit_metrics: self.calculate_circuit_metrics(circuit),
480            execution_results,
481            noise_analysis,
482            custom_metrics: HashMap::new(),
483        })
484    }
485
486    /// Calculate circuit metrics
487    fn calculate_circuit_metrics<const N: usize>(&self, circuit: &Circuit<N>) -> CircuitMetrics {
488        let gate_count = circuit.gates().len();
489        let mut gate_counts = HashMap::new();
490        let mut two_qubit_gates = 0;
491
492        for gate in circuit.gates() {
493            let gate_name = gate.name();
494            *gate_counts.entry(gate_name.to_string()).or_insert(0) += 1;
495
496            if gate.qubits().len() == 2 {
497                two_qubit_gates += 1;
498            }
499        }
500
501        CircuitMetrics {
502            depth: gate_count, // Simplified depth calculation
503            gate_count,
504            gate_counts,
505            two_qubit_gates,
506            fidelity: None,
507            error_rate: None,
508        }
509    }
510
511    /// Get current memory usage (placeholder)
512    const fn get_memory_usage(&self) -> usize {
513        // In real implementation, this would use system APIs to get memory usage
514        0
515    }
516
517    /// Generate comprehensive benchmark report
518    fn generate_benchmark_report(&self) -> QuantRS2Result<BenchmarkReport> {
519        let completed_runs = self.benchmark_data.len();
520        let successful_runs: Vec<_> = self
521            .benchmark_data
522            .iter()
523            .filter(|run| run.success)
524            .collect();
525
526        let success_rate = successful_runs.len() as f64 / completed_runs as f64;
527
528        // Extract timing data
529        let timing_data: Vec<f64> = successful_runs
530            .iter()
531            .map(|run| run.execution_time.as_secs_f64())
532            .collect();
533
534        let timing_stats = self
535            .stats_analyzer
536            .calculate_descriptive_stats(&timing_data)?;
537
538        // Extract memory data if available
539        let memory_stats = if self.config.collect_memory {
540            let memory_data: Vec<f64> = successful_runs
541                .iter()
542                .filter_map(|run| run.memory_usage.map(|m| m as f64))
543                .collect();
544
545            if memory_data.is_empty() {
546                None
547            } else {
548                Some(
549                    self.stats_analyzer
550                        .calculate_descriptive_stats(&memory_data)?,
551                )
552            }
553        } else {
554            None
555        };
556
557        // Perform regression analysis to detect performance trends
558        let regression_analysis = self
559            .stats_analyzer
560            .perform_regression_analysis(&timing_data)?;
561
562        // Fit distributions to timing data
563        let distribution_fit = self.stats_analyzer.fit_distributions(&timing_data)?;
564
565        // Detect outliers
566        let outlier_analysis = self.stats_analyzer.detect_outliers(
567            &timing_data,
568            OutlierDetectionMethod::IQR { multiplier: 1.5 },
569        )?;
570
571        // Generate insights
572        let insights = self.generate_performance_insights(
573            &timing_stats,
574            &regression_analysis,
575            &outlier_analysis,
576            success_rate,
577        );
578
579        Ok(BenchmarkReport {
580            config: self.config.clone(),
581            completed_runs,
582            success_rate,
583            timing_stats,
584            timing_samples: timing_data,
585            memory_stats,
586            regression_analysis: Some(regression_analysis),
587            distribution_fit: Some(distribution_fit),
588            outlier_analysis,
589            baseline_comparison: None,
590            statistical_tests: Vec::new(),
591            insights,
592        })
593    }
594
595    /// Generate performance insights based on statistical analysis
596    fn generate_performance_insights(
597        &self,
598        timing_stats: &DescriptiveStats,
599        regression: &RegressionAnalysis,
600        outliers: &OutlierAnalysis,
601        success_rate: f64,
602    ) -> Vec<PerformanceInsight> {
603        let mut insights = Vec::new();
604
605        // Check for performance degradation
606        if regression.significant_trend && regression.slope > 0.0 {
607            insights.push(PerformanceInsight {
608                category: InsightCategory::PerformanceDegradation,
609                message: format!(
610                    "Significant performance degradation detected: {:.4} seconds per run increase",
611                    regression.degradation_per_run
612                ),
613                confidence: 1.0 - regression.slope_p_value,
614                evidence: vec![
615                    format!("Linear trend slope: {:.6}", regression.slope),
616                    format!("R-squared: {:.4}", regression.r_squared),
617                    format!("P-value: {:.4}", regression.slope_p_value),
618                ],
619                recommendations: vec![
620                    "Investigate potential memory leaks".to_string(),
621                    "Check for resource contention".to_string(),
622                    "Profile execution to identify bottlenecks".to_string(),
623                ],
624            });
625        }
626
627        // Check for high variability
628        let coefficient_of_variation = timing_stats.std_dev / timing_stats.mean;
629        if coefficient_of_variation > 0.2 {
630            insights.push(PerformanceInsight {
631                category: InsightCategory::HighVariability,
632                message: format!(
633                    "High performance variability detected: CV = {:.2}%",
634                    coefficient_of_variation * 100.0
635                ),
636                confidence: 0.8,
637                evidence: vec![
638                    format!("Standard deviation: {:.4} seconds", timing_stats.std_dev),
639                    format!("Mean: {:.4} seconds", timing_stats.mean),
640                    format!(
641                        "Coefficient of variation: {:.2}%",
642                        coefficient_of_variation * 100.0
643                    ),
644                ],
645                recommendations: vec![
646                    "Increase warm-up runs to stabilize performance".to_string(),
647                    "Check for system load variations".to_string(),
648                    "Consider running benchmarks in isolated environment".to_string(),
649                ],
650            });
651        }
652
653        // Check for outliers
654        if outliers.num_outliers > 0 {
655            let outlier_percentage =
656                outliers.num_outliers as f64 / timing_stats.count as f64 * 100.0;
657            insights.push(PerformanceInsight {
658                category: InsightCategory::OutliersDetected,
659                message: format!(
660                    "Performance outliers detected: {} outliers ({:.1}% of runs)",
661                    outliers.num_outliers, outlier_percentage
662                ),
663                confidence: 0.9,
664                evidence: vec![
665                    format!("Number of outliers: {}", outliers.num_outliers),
666                    format!("Outlier percentage: {:.1}%", outlier_percentage),
667                    format!("Detection method: {:?}", outliers.detection_method),
668                ],
669                recommendations: vec![
670                    "Investigate causes of outlier runs".to_string(),
671                    "Consider removing outliers from performance metrics".to_string(),
672                    "Check for system interruptions during benchmarking".to_string(),
673                ],
674            });
675        }
676
677        // Check success rate
678        if success_rate < 0.95 {
679            insights.push(PerformanceInsight {
680                category: InsightCategory::ErrorRate,
681                message: format!("Low success rate detected: {:.1}%", success_rate * 100.0),
682                confidence: 1.0,
683                evidence: vec![
684                    format!("Success rate: {:.1}%", success_rate * 100.0),
685                    format!(
686                        "Failed runs: {}",
687                        timing_stats.count - (timing_stats.count as f64 * success_rate) as usize
688                    ),
689                ],
690                recommendations: vec![
691                    "Investigate failure causes".to_string(),
692                    "Check circuit validity and simulator compatibility".to_string(),
693                    "Increase timeout limits if timeouts are occurring".to_string(),
694                ],
695            });
696        }
697
698        insights
699    }
700
701    /// Compare with baseline benchmark
702    pub fn compare_with_baseline(
703        &self,
704        baseline: &BenchmarkReport,
705    ) -> QuantRS2Result<BaselineComparison> {
706        if self.benchmark_data.is_empty() {
707            return Err(QuantRS2Error::InvalidInput(
708                "No benchmark data available for comparison".to_string(),
709            ));
710        }
711
712        let current_timing: Vec<f64> = self
713            .benchmark_data
714            .iter()
715            .filter(|run| run.success)
716            .map(|run| run.execution_time.as_secs_f64())
717            .collect();
718
719        let baseline_mean = baseline.timing_stats.mean;
720        let current_mean = self
721            .stats_analyzer
722            .calculate_descriptive_stats(&current_timing)?
723            .mean;
724
725        let performance_factor = current_mean / baseline_mean;
726
727        // Perform statistical test for significance using the real retained
728        // per-run baseline timing samples (falling back to the summary mean
729        // only if the baseline report predates sample retention).
730        let baseline_samples: Vec<f64> = if baseline.timing_samples.is_empty() {
731            vec![baseline_mean]
732        } else {
733            baseline.timing_samples.clone()
734        };
735        let significance = self.stats_analyzer.mann_whitney_test(
736            &current_timing,
737            &baseline_samples,
738            self.config.significance_level,
739        )?;
740
741        // Calculate effect size (Cohen's d)
742        let effect_size = (current_mean - baseline_mean) / baseline.timing_stats.std_dev;
743
744        // Assess practical significance
745        let practical_significance = match effect_size.abs() {
746            x if x < 0.2 => PracticalSignificance::Negligible,
747            x if x < 0.5 => PracticalSignificance::Small,
748            x if x < 0.8 => PracticalSignificance::Medium,
749            x if x < 1.2 => PracticalSignificance::Large,
750            _ => PracticalSignificance::VeryLarge,
751        };
752
753        // Compute a real normal-approximation confidence interval for the
754        // difference in means (current - baseline), using the pooled
755        // standard error from both samples' variances.
756        let current_variance = self
757            .stats_analyzer
758            .calculate_descriptive_stats(&current_timing)?
759            .variance;
760        let baseline_variance = if baseline_samples.len() > 1 {
761            self.stats_analyzer
762                .calculate_descriptive_stats(&baseline_samples)?
763                .variance
764        } else {
765            baseline.timing_stats.std_dev * baseline.timing_stats.std_dev
766        };
767        let n_current = current_timing.len() as f64;
768        let n_baseline = baseline_samples.len().max(1) as f64;
769        let standard_error = (current_variance / n_current + baseline_variance / n_baseline).sqrt();
770        let z_critical = inverse_normal_cdf(1.0 - self.config.significance_level / 2.0);
771        let mean_difference = current_mean - baseline_mean;
772        let difference_ci = (
773            z_critical.mul_add(-standard_error, mean_difference),
774            z_critical.mul_add(standard_error, mean_difference),
775        );
776
777        Ok(BaselineComparison {
778            baseline_name: "baseline".to_string(),
779            performance_factor,
780            significance,
781            difference_ci,
782            effect_size,
783            practical_significance,
784        })
785    }
786}
787
788/// Statistical analyzer using `SciRS2` capabilities
789pub struct StatisticalAnalyzer;
790
791impl StatisticalAnalyzer {
792    /// Create a new statistical analyzer
793    #[must_use]
794    pub const fn new() -> Self {
795        Self
796    }
797
798    /// Calculate descriptive statistics
799    pub fn calculate_descriptive_stats(&self, data: &[f64]) -> QuantRS2Result<DescriptiveStats> {
800        if data.is_empty() {
801            return Err(QuantRS2Error::InvalidInput("Empty data".to_string()));
802        }
803
804        let mut sorted_data = data.to_vec();
805        sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
806
807        let count = data.len();
808        let mean = data.iter().sum::<f64>() / count as f64;
809        let variance = data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count as f64;
810        let std_dev = variance.sqrt();
811        let min = sorted_data[0];
812        let max = sorted_data[count - 1];
813
814        let median = if count % 2 == 0 {
815            f64::midpoint(sorted_data[count / 2 - 1], sorted_data[count / 2])
816        } else {
817            sorted_data[count / 2]
818        };
819
820        let q1 = self.percentile(&sorted_data, 0.25);
821        let q3 = self.percentile(&sorted_data, 0.75);
822        let iqr = q3 - q1;
823
824        // Calculate skewness and kurtosis
825        let skewness = self.calculate_skewness(data, mean, std_dev);
826        let kurtosis = self.calculate_kurtosis(data, mean, std_dev);
827
828        Ok(DescriptiveStats {
829            count,
830            mean,
831            std_dev,
832            variance,
833            min,
834            max,
835            median,
836            q1,
837            q3,
838            iqr,
839            skewness,
840            kurtosis,
841            mode: None, // Would implement mode calculation
842        })
843    }
844
845    /// Calculate percentile
846    fn percentile(&self, sorted_data: &[f64], p: f64) -> f64 {
847        let index = (p * (sorted_data.len() - 1) as f64).round() as usize;
848        sorted_data[index.min(sorted_data.len() - 1)]
849    }
850
851    /// Calculate skewness
852    fn calculate_skewness(&self, data: &[f64], mean: f64, std_dev: f64) -> f64 {
853        let n = data.len() as f64;
854        let skew_sum = data
855            .iter()
856            .map(|x| ((x - mean) / std_dev).powi(3))
857            .sum::<f64>();
858        skew_sum / n
859    }
860
861    /// Calculate kurtosis
862    fn calculate_kurtosis(&self, data: &[f64], mean: f64, std_dev: f64) -> f64 {
863        let n = data.len() as f64;
864        let kurt_sum = data
865            .iter()
866            .map(|x| ((x - mean) / std_dev).powi(4))
867            .sum::<f64>();
868        kurt_sum / n - 3.0 // Excess kurtosis
869    }
870
871    /// Perform linear regression analysis
872    pub fn perform_regression_analysis(&self, data: &[f64]) -> QuantRS2Result<RegressionAnalysis> {
873        if data.len() < 3 {
874            return Err(QuantRS2Error::InvalidInput(
875                "Insufficient data for regression".to_string(),
876            ));
877        }
878
879        let n = data.len() as f64;
880        let x_values: Vec<f64> = (0..data.len()).map(|i| i as f64).collect();
881
882        let x_mean = x_values.iter().sum::<f64>() / n;
883        let y_mean = data.iter().sum::<f64>() / n;
884
885        let numerator: f64 = x_values
886            .iter()
887            .zip(data.iter())
888            .map(|(x, y)| (x - x_mean) * (y - y_mean))
889            .sum();
890
891        let denominator: f64 = x_values.iter().map(|x| (x - x_mean).powi(2)).sum();
892
893        let slope = numerator / denominator;
894        let intercept = slope.mul_add(-x_mean, y_mean);
895
896        // Calculate R-squared
897        let ss_tot: f64 = data.iter().map(|y| (y - y_mean).powi(2)).sum();
898        let ss_res: f64 = x_values
899            .iter()
900            .zip(data.iter())
901            .map(|(x, y)| {
902                let predicted = slope * x + intercept;
903                (y - predicted).powi(2)
904            })
905            .sum();
906
907        let r_squared = 1.0 - (ss_res / ss_tot);
908
909        // Calculate a real p-value for the slope estimate from Student's
910        // t-distribution: t = slope / se(slope), with n - 2 residual
911        // degrees of freedom, using the standard OLS standard-error formula
912        // se(slope) = sqrt(MSE / sum((x - x_mean)^2)).
913        let degrees_of_freedom = n - 2.0;
914        let slope_p_value = if degrees_of_freedom > 0.0 && denominator > 0.0 {
915            let mean_squared_error = (ss_res / degrees_of_freedom).max(0.0);
916            let standard_error_slope = (mean_squared_error / denominator).sqrt();
917            if standard_error_slope > 0.0 {
918                let t_statistic = slope / standard_error_slope;
919                student_t_two_sided_p_value(t_statistic, degrees_of_freedom)
920            } else if slope.abs() > 0.0 {
921                // Zero residual variance with a non-zero slope: perfect
922                // linear fit, so the trend is maximally significant.
923                0.0
924            } else {
925                1.0
926            }
927        } else {
928            1.0
929        };
930        let significant_trend = slope_p_value < 0.05;
931
932        Ok(RegressionAnalysis {
933            slope,
934            intercept,
935            r_squared,
936            slope_p_value,
937            significant_trend,
938            degradation_per_run: slope,
939        })
940    }
941
942    /// Fit probability distributions to data
943    pub fn fit_distributions(&self, data: &[f64]) -> QuantRS2Result<DistributionFit> {
944        let stats = self.calculate_descriptive_stats(data)?;
945
946        // Fit normal distribution
947        let normal_dist = Distribution::Normal {
948            mean: stats.mean,
949            std_dev: stats.std_dev,
950        };
951
952        // Real goodness-of-fit assessment: a one-sample Kolmogorov-Smirnov
953        // test of the empirical distribution against the fitted normal.
954        let (ks_statistic, fit_p_value) =
955            Self::kolmogorov_smirnov_normal_test(data, stats.mean, stats.std_dev);
956        let goodness_of_fit = (1.0 - ks_statistic).clamp(0.0, 1.0);
957
958        Ok(DistributionFit {
959            best_distribution: normal_dist,
960            goodness_of_fit,
961            fit_p_value,
962            alternative_fits: Vec::new(),
963        })
964    }
965
966    /// One-sample Kolmogorov-Smirnov test of `data` against a
967    /// `Normal(mean, std_dev)` distribution. Returns `(D statistic, p-value)`
968    /// using the asymptotic Kolmogorov distribution for the p-value.
969    fn kolmogorov_smirnov_normal_test(data: &[f64], mean: f64, std_dev: f64) -> (f64, f64) {
970        if data.is_empty() || std_dev <= 0.0 {
971            return (1.0, 0.0);
972        }
973
974        let mut sorted = data.to_vec();
975        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
976        let n = sorted.len() as f64;
977
978        let mut max_diff = 0.0_f64;
979        for (i, &value) in sorted.iter().enumerate() {
980            let z = (value - mean) / std_dev;
981            let cdf = normal_cdf(z);
982            let empirical_upper = (i as f64 + 1.0) / n;
983            let empirical_lower = i as f64 / n;
984            max_diff = max_diff.max((empirical_upper - cdf).abs());
985            max_diff = max_diff.max((cdf - empirical_lower).abs());
986        }
987
988        let p_value = kolmogorov_smirnov_p_value(max_diff, sorted.len());
989        (max_diff, p_value)
990    }
991
992    /// Detect outliers using specified method
993    pub fn detect_outliers(
994        &self,
995        data: &[f64],
996        method: OutlierDetectionMethod,
997    ) -> QuantRS2Result<OutlierAnalysis> {
998        let (outlier_indices, threshold) = match method {
999            OutlierDetectionMethod::IQR { multiplier } => {
1000                (self.detect_outliers_iqr(data, multiplier)?, multiplier)
1001            }
1002            OutlierDetectionMethod::ZScore { threshold } => {
1003                (self.detect_outliers_zscore(data, threshold)?, threshold)
1004            }
1005            OutlierDetectionMethod::ModifiedZScore { threshold } => (
1006                self.detect_outliers_modified_zscore(data, threshold)?,
1007                threshold,
1008            ),
1009            OutlierDetectionMethod::IsolationForest
1010            | OutlierDetectionMethod::LocalOutlierFactor => {
1011                // These require a trained ensemble/density model that this
1012                // analyzer does not implement; report honestly instead of
1013                // silently claiming zero outliers were found.
1014                return Err(QuantRS2Error::UnsupportedOperation(format!(
1015                    "Outlier detection method {method:?} is not yet implemented"
1016                )));
1017            }
1018        };
1019
1020        let num_outliers = outlier_indices.len();
1021
1022        // Calculate outlier impact
1023        let outlier_impact = if num_outliers > 0 {
1024            self.calculate_outlier_impact(data, &outlier_indices)?
1025        } else {
1026            OutlierImpact {
1027                mean_change: 0.0,
1028                std_dev_change: 0.0,
1029                median_change: 0.0,
1030                relative_impact: 0.0,
1031            }
1032        };
1033
1034        Ok(OutlierAnalysis {
1035            num_outliers,
1036            outlier_indices,
1037            detection_method: method,
1038            threshold,
1039            outlier_impact,
1040        })
1041    }
1042
1043    /// Detect outliers using IQR method
1044    fn detect_outliers_iqr(&self, data: &[f64], multiplier: f64) -> QuantRS2Result<Vec<usize>> {
1045        let stats = self.calculate_descriptive_stats(data)?;
1046        let lower_bound = multiplier.mul_add(-stats.iqr, stats.q1);
1047        let upper_bound = multiplier.mul_add(stats.iqr, stats.q3);
1048
1049        Ok(data
1050            .iter()
1051            .enumerate()
1052            .filter_map(|(i, &value)| {
1053                if value < lower_bound || value > upper_bound {
1054                    Some(i)
1055                } else {
1056                    None
1057                }
1058            })
1059            .collect())
1060    }
1061
1062    /// Detect outliers using Z-score method
1063    fn detect_outliers_zscore(&self, data: &[f64], threshold: f64) -> QuantRS2Result<Vec<usize>> {
1064        let stats = self.calculate_descriptive_stats(data)?;
1065
1066        Ok(data
1067            .iter()
1068            .enumerate()
1069            .filter_map(|(i, &value)| {
1070                let z_score = (value - stats.mean) / stats.std_dev;
1071                if z_score.abs() > threshold {
1072                    Some(i)
1073                } else {
1074                    None
1075                }
1076            })
1077            .collect())
1078    }
1079
1080    /// Detect outliers using the modified Z-score method (median absolute
1081    /// deviation based), which is more robust to extreme values than the
1082    /// mean/std-dev based Z-score method above.
1083    fn detect_outliers_modified_zscore(
1084        &self,
1085        data: &[f64],
1086        threshold: f64,
1087    ) -> QuantRS2Result<Vec<usize>> {
1088        if data.is_empty() {
1089            return Err(QuantRS2Error::InvalidInput("Empty data".to_string()));
1090        }
1091
1092        let mut sorted = data.to_vec();
1093        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1094        let median = Self::median_of_sorted(&sorted);
1095
1096        let mut abs_deviations: Vec<f64> = data.iter().map(|&v| (v - median).abs()).collect();
1097        abs_deviations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1098        let mad = Self::median_of_sorted(&abs_deviations);
1099
1100        if mad == 0.0 {
1101            // No spread to normalize against: fall back to flagging any
1102            // value that differs at all from the median, rather than
1103            // dividing by zero or silently reporting no outliers.
1104            return Ok(data
1105                .iter()
1106                .enumerate()
1107                .filter_map(|(i, &value)| {
1108                    if (value - median).abs() > f64::EPSILON {
1109                        Some(i)
1110                    } else {
1111                        None
1112                    }
1113                })
1114                .collect());
1115        }
1116
1117        // 0.6745 is the constant that makes the MAD comparable to the
1118        // standard deviation for normally distributed data (Iglewicz & Hoaglin).
1119        Ok(data
1120            .iter()
1121            .enumerate()
1122            .filter_map(|(i, &value)| {
1123                let modified_z = 0.6745 * (value - median) / mad;
1124                if modified_z.abs() > threshold {
1125                    Some(i)
1126                } else {
1127                    None
1128                }
1129            })
1130            .collect())
1131    }
1132
1133    /// Median of an already-sorted slice
1134    fn median_of_sorted(sorted: &[f64]) -> f64 {
1135        let count = sorted.len();
1136        if count == 0 {
1137            return 0.0;
1138        }
1139        if count % 2 == 0 {
1140            f64::midpoint(sorted[count / 2 - 1], sorted[count / 2])
1141        } else {
1142            sorted[count / 2]
1143        }
1144    }
1145
1146    /// Calculate impact of outliers on statistics
1147    fn calculate_outlier_impact(
1148        &self,
1149        data: &[f64],
1150        outlier_indices: &[usize],
1151    ) -> QuantRS2Result<OutlierImpact> {
1152        let original_stats = self.calculate_descriptive_stats(data)?;
1153
1154        // Create data without outliers
1155        let filtered_data: Vec<f64> = data
1156            .iter()
1157            .enumerate()
1158            .filter_map(|(i, &value)| {
1159                if outlier_indices.contains(&i) {
1160                    None
1161                } else {
1162                    Some(value)
1163                }
1164            })
1165            .collect();
1166
1167        let filtered_stats = self.calculate_descriptive_stats(&filtered_data)?;
1168
1169        let mean_change = (original_stats.mean - filtered_stats.mean).abs();
1170        let std_dev_change = (original_stats.std_dev - filtered_stats.std_dev).abs();
1171        let median_change = (original_stats.median - filtered_stats.median).abs();
1172        let relative_impact = mean_change / original_stats.mean * 100.0;
1173
1174        Ok(OutlierImpact {
1175            mean_change,
1176            std_dev_change,
1177            median_change,
1178            relative_impact,
1179        })
1180    }
1181
1182    /// Perform Mann-Whitney U test
1183    pub fn mann_whitney_test(
1184        &self,
1185        sample1: &[f64],
1186        sample2: &[f64],
1187        significance_level: f64,
1188    ) -> QuantRS2Result<HypothesisTestResult> {
1189        let n1 = sample1.len();
1190        let n2 = sample2.len();
1191        if n1 == 0 || n2 == 0 {
1192            return Err(QuantRS2Error::InvalidInput(
1193                "Mann-Whitney U test requires two non-empty samples".to_string(),
1194            ));
1195        }
1196
1197        // Combine both samples, tagging which group each value came from,
1198        // then rank the combined data (ties receive the average of the
1199        // ranks they span).
1200        let mut combined: Vec<(f64, u8)> = sample1
1201            .iter()
1202            .map(|&value| (value, 0u8))
1203            .chain(sample2.iter().map(|&value| (value, 1u8)))
1204            .collect();
1205        combined.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1206
1207        let n_total = combined.len();
1208        let mut ranks = vec![0.0_f64; n_total];
1209        let mut tie_correction_sum = 0.0_f64;
1210        let mut i = 0;
1211        while i < n_total {
1212            let mut j = i;
1213            while j + 1 < n_total && (combined[j + 1].0 - combined[i].0).abs() < f64::EPSILON {
1214                j += 1;
1215            }
1216            // Average (1-based) rank shared by the tied block [i, j]
1217            let average_rank = ((i + 1) + (j + 1)) as f64 / 2.0;
1218            for rank in ranks.iter_mut().take(j + 1).skip(i) {
1219                *rank = average_rank;
1220            }
1221            let tie_group_size = (j - i + 1) as f64;
1222            tie_correction_sum += tie_group_size.powi(3) - tie_group_size;
1223            i = j + 1;
1224        }
1225
1226        let rank_sum_sample1: f64 = combined
1227            .iter()
1228            .zip(ranks.iter())
1229            .filter(|((_, group), _)| *group == 0)
1230            .map(|(_, &rank)| rank)
1231            .sum();
1232
1233        let n1_f = n1 as f64;
1234        let n2_f = n2 as f64;
1235        let n_total_f = n1_f + n2_f;
1236
1237        let u1 = rank_sum_sample1 - n1_f * (n1_f + 1.0) / 2.0;
1238        let u2 = n1_f * n2_f - u1;
1239        let u_statistic = u1.min(u2);
1240
1241        let mean_u = n1_f * n2_f / 2.0;
1242        let tie_term = if n_total_f > 1.0 {
1243            tie_correction_sum / (n_total_f * (n_total_f - 1.0))
1244        } else {
1245            0.0
1246        };
1247        let variance_u = (n1_f * n2_f / 12.0) * (n_total_f + 1.0 - tie_term);
1248        let std_dev_u = variance_u.max(0.0).sqrt();
1249
1250        let p_value = if std_dev_u > 0.0 {
1251            let difference = u1 - mean_u;
1252            // Continuity correction toward the mean
1253            let z_score = if difference > 0.0 {
1254                (difference - 0.5) / std_dev_u
1255            } else if difference < 0.0 {
1256                (difference + 0.5) / std_dev_u
1257            } else {
1258                0.0
1259            };
1260            (2.0 * (1.0 - normal_cdf(z_score.abs()))).clamp(0.0, 1.0)
1261        } else {
1262            1.0
1263        };
1264
1265        let critical_value = inverse_normal_cdf(1.0 - significance_level / 2.0);
1266        let reject_null = p_value < significance_level;
1267
1268        // Rank-biserial correlation as the effect size for a Mann-Whitney U test
1269        let effect_size = 1.0 - (2.0 * u1) / (n1_f * n2_f);
1270
1271        Ok(HypothesisTestResult {
1272            test_statistic: u_statistic,
1273            p_value,
1274            critical_value,
1275            reject_null,
1276            significance_level,
1277            effect_size: Some(effect_size),
1278            confidence_interval: None,
1279        })
1280    }
1281}
1282
1283/// Trait for simulator execution (placeholder)
1284pub trait SimulatorExecutor {
1285    fn execute(&self, circuit: &dyn std::any::Any) -> QuantRS2Result<ExecutionResult>;
1286}
1287
1288impl Default for StatisticalAnalyzer {
1289    fn default() -> Self {
1290        Self::new()
1291    }
1292}
1293
1294// ---------------------------------------------------------------------
1295// Real statistical primitives backing the hypothesis tests, regression
1296// significance, and goodness-of-fit tests above. These are standard
1297// numerical-analysis approximations (Abramowitz & Stegun error function,
1298// Acklam's inverse normal CDF, Lanczos log-gamma, and the Numerical
1299// Recipes continued-fraction for the regularized incomplete beta
1300// function), not statistical placeholders.
1301// ---------------------------------------------------------------------
1302
1303/// Error function approximation (Abramowitz & Stegun 7.1.26), maximum
1304/// absolute error ~1.5e-7.
1305fn erf(x: f64) -> f64 {
1306    let sign = if x < 0.0 { -1.0 } else { 1.0 };
1307    let x = x.abs();
1308
1309    let a1 = 0.254_829_592_f64;
1310    let a2 = -0.284_496_736_f64;
1311    let a3 = 1.421_413_741_f64;
1312    let a4 = -1.453_152_027_f64;
1313    let a5 = 1.061_405_429_f64;
1314    let p = 0.327_591_1_f64;
1315
1316    let t = 1.0 / p.mul_add(x, 1.0);
1317    let poly = ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t;
1318    let y = 1.0 - poly * (-x * x).exp();
1319
1320    sign * y
1321}
1322
1323/// Standard normal cumulative distribution function.
1324fn normal_cdf(z: f64) -> f64 {
1325    0.5 * (1.0 + erf(z / std::f64::consts::SQRT_2))
1326}
1327
1328/// Inverse standard normal CDF (quantile function) via Peter Acklam's
1329/// rational approximation, accurate to about 1.15e-9.
1330fn inverse_normal_cdf(p: f64) -> f64 {
1331    if p <= 0.0 {
1332        return f64::NEG_INFINITY;
1333    }
1334    if p >= 1.0 {
1335        return f64::INFINITY;
1336    }
1337
1338    const A: [f64; 6] = [
1339        -3.969_683_028_665_376e+01,
1340        2.209_460_984_245_205e+02,
1341        -2.759_285_104_469_687e+02,
1342        1.383_577_518_672_69e+02,
1343        -3.066_479_806_614_716e+01,
1344        2.506_628_277_459_239e+00,
1345    ];
1346    const B: [f64; 5] = [
1347        -5.447_609_879_822_406e+01,
1348        1.615_858_368_580_409e+02,
1349        -1.556_989_798_598_866e+02,
1350        6.680_131_188_771_972e+01,
1351        -1.328_068_155_288_572e+01,
1352    ];
1353    const C: [f64; 6] = [
1354        -7.784_894_002_430_293e-03,
1355        -3.223_964_580_411_365e-01,
1356        -2.400_758_277_161_838e+00,
1357        -2.549_732_539_343_734e+00,
1358        4.374_664_141_464_968e+00,
1359        2.938_163_982_698_783e+00,
1360    ];
1361    const D: [f64; 4] = [
1362        7.784_695_709_041_462e-03,
1363        3.224_671_290_700_398e-01,
1364        2.445_134_137_142_996e+00,
1365        3.754_408_661_907_416e+00,
1366    ];
1367
1368    let p_low = 0.024_85;
1369    let p_high = 1.0 - p_low;
1370
1371    if p < p_low {
1372        let q = (-2.0 * p.ln()).sqrt();
1373        (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
1374            / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
1375    } else if p <= p_high {
1376        let q = p - 0.5;
1377        let r = q * q;
1378        (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
1379            / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
1380    } else {
1381        let q = (-2.0 * (1.0 - p).ln()).sqrt();
1382        -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
1383            / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
1384    }
1385}
1386
1387/// Natural log of the gamma function via the Lanczos approximation
1388/// (g = 7, n = 9), accurate to double precision over the domain used here.
1389fn ln_gamma(x: f64) -> f64 {
1390    const COEFFICIENTS: [f64; 9] = [
1391        0.999_999_999_999_809_9,
1392        676.520_368_121_885_1,
1393        -1_259.139_216_722_402_8,
1394        771.323_428_777_653_1,
1395        -176.615_029_162_140_6,
1396        12.507_343_278_686_905,
1397        -0.138_571_095_265_720_12,
1398        9.984_369_578_019_572e-6,
1399        1.505_632_735_149_311_6e-7,
1400    ];
1401
1402    if x < 0.5 {
1403        // Reflection formula for arguments below 0.5
1404        (std::f64::consts::PI / (std::f64::consts::PI * x).sin()).ln() - ln_gamma(1.0 - x)
1405    } else {
1406        let x = x - 1.0;
1407        let mut a = COEFFICIENTS[0];
1408        let t = x + 7.5;
1409        for (i, coeff) in COEFFICIENTS.iter().enumerate().skip(1) {
1410            a += coeff / (x + i as f64);
1411        }
1412        0.5_f64.mul_add(
1413            (2.0 * std::f64::consts::PI).ln(),
1414            (x + 0.5) * t.ln() - t + a.ln(),
1415        )
1416    }
1417}
1418
1419/// Continued-fraction evaluation used by the regularized incomplete beta
1420/// function (Numerical Recipes `betacf`).
1421fn incomplete_beta_continued_fraction(x: f64, a: f64, b: f64) -> f64 {
1422    const MAX_ITERATIONS: usize = 200;
1423    const EPSILON: f64 = 1e-12;
1424    const FP_MIN: f64 = 1e-300;
1425
1426    let qab = a + b;
1427    let qap = a + 1.0;
1428    let qam = a - 1.0;
1429    let mut c = 1.0_f64;
1430    let mut d = 1.0 - qab * x / qap;
1431    if d.abs() < FP_MIN {
1432        d = FP_MIN;
1433    }
1434    d = 1.0 / d;
1435    let mut h = d;
1436
1437    for m in 1..=MAX_ITERATIONS {
1438        let m_f = m as f64;
1439        let m2 = 2.0 * m_f;
1440
1441        let aa_even = m_f * (b - m_f) * x / ((qam + m2) * (a + m2));
1442        d = 1.0 + aa_even * d;
1443        if d.abs() < FP_MIN {
1444            d = FP_MIN;
1445        }
1446        c = 1.0 + aa_even / c;
1447        if c.abs() < FP_MIN {
1448            c = FP_MIN;
1449        }
1450        d = 1.0 / d;
1451        h *= d * c;
1452
1453        let aa_odd = -(a + m_f) * (qab + m_f) * x / ((a + m2) * (qap + m2));
1454        d = 1.0 + aa_odd * d;
1455        if d.abs() < FP_MIN {
1456            d = FP_MIN;
1457        }
1458        c = 1.0 + aa_odd / c;
1459        if c.abs() < FP_MIN {
1460            c = FP_MIN;
1461        }
1462        d = 1.0 / d;
1463        let delta = d * c;
1464        h *= delta;
1465
1466        if (delta - 1.0).abs() < EPSILON {
1467            break;
1468        }
1469    }
1470
1471    h
1472}
1473
1474/// Regularized incomplete beta function `I_x(a, b)`.
1475fn incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
1476    if x <= 0.0 {
1477        return 0.0;
1478    }
1479    if x >= 1.0 {
1480        return 1.0;
1481    }
1482
1483    let ln_beta_fn = ln_gamma(a + b) - ln_gamma(a) - ln_gamma(b);
1484    let front = (a.mul_add(x.ln(), ln_beta_fn) + b * (1.0 - x).ln()).exp();
1485
1486    if x < (a + 1.0) / (a + b + 2.0) {
1487        front * incomplete_beta_continued_fraction(x, a, b) / a
1488    } else {
1489        1.0 - front * incomplete_beta_continued_fraction(1.0 - x, b, a) / b
1490    }
1491}
1492
1493/// Two-sided p-value for a Student's t-distributed statistic with the
1494/// given (real-valued) degrees of freedom, computed via the regularized
1495/// incomplete beta function: `p = I_{df / (df + t^2)}(df/2, 1/2)`.
1496fn student_t_two_sided_p_value(t_statistic: f64, degrees_of_freedom: f64) -> f64 {
1497    if degrees_of_freedom <= 0.0 {
1498        return 1.0;
1499    }
1500    let x = degrees_of_freedom / (degrees_of_freedom + t_statistic * t_statistic);
1501    incomplete_beta(x, degrees_of_freedom / 2.0, 0.5).clamp(0.0, 1.0)
1502}
1503
1504/// Asymptotic p-value for the one-sample Kolmogorov-Smirnov statistic `d`
1505/// computed from `n` observations, using the Kolmogorov distribution's
1506/// alternating-series form (Marsaglia-Kolmogorov asymptotic expansion).
1507fn kolmogorov_smirnov_p_value(d: f64, n: usize) -> f64 {
1508    if n == 0 {
1509        return 1.0;
1510    }
1511    let n_f = n as f64;
1512    let lambda = (n_f.sqrt() + 0.12 + 0.11 / n_f.sqrt()) * d;
1513    if lambda < 0.2 {
1514        return 1.0;
1515    }
1516
1517    let mut sum = 0.0_f64;
1518    for k in 1..=100_i32 {
1519        let sign = if k % 2 == 1 { 1.0 } else { -1.0 };
1520        sum += sign * (-2.0 * f64::from(k * k) * lambda * lambda).exp();
1521    }
1522
1523    (2.0 * sum).clamp(0.0, 1.0)
1524}
1525
1526#[cfg(test)]
1527mod tests {
1528    use super::*;
1529
1530    #[test]
1531    fn test_descriptive_stats() {
1532        let analyzer = StatisticalAnalyzer::new();
1533        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1534
1535        let stats = analyzer
1536            .calculate_descriptive_stats(&data)
1537            .expect("calculate_descriptive_stats should succeed");
1538        assert_eq!(stats.mean, 3.0);
1539        assert_eq!(stats.median, 3.0);
1540        assert_eq!(stats.min, 1.0);
1541        assert_eq!(stats.max, 5.0);
1542    }
1543
1544    #[test]
1545    fn test_outlier_detection_iqr() {
1546        let analyzer = StatisticalAnalyzer::new();
1547        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 100.0]; // 100.0 is an outlier
1548
1549        let outliers = analyzer
1550            .detect_outliers_iqr(&data, 1.5)
1551            .expect("outlier detection should succeed");
1552        assert_eq!(outliers.len(), 1);
1553        assert_eq!(outliers[0], 5); // Index of 100.0
1554    }
1555
1556    #[test]
1557    fn test_regression_analysis() {
1558        let analyzer = StatisticalAnalyzer::new();
1559        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0]; // Perfect linear trend
1560
1561        let regression = analyzer
1562            .perform_regression_analysis(&data)
1563            .expect("perform_regression_analysis should succeed");
1564        assert!((regression.slope - 1.0).abs() < 1e-10);
1565        assert!((regression.r_squared - 1.0).abs() < 1e-10);
1566    }
1567
1568    #[test]
1569    fn test_benchmark_config() {
1570        let config = BenchmarkConfig::default();
1571        assert_eq!(config.num_runs, 100);
1572        assert_eq!(config.warmup_runs, 10);
1573        assert_eq!(config.significance_level, 0.05);
1574    }
1575
1576    #[test]
1577    fn test_distribution_creation() {
1578        let normal = Distribution::Normal {
1579            mean: 0.0,
1580            std_dev: 1.0,
1581        };
1582        match normal {
1583            Distribution::Normal { mean, std_dev } => {
1584                assert_eq!(mean, 0.0);
1585                assert_eq!(std_dev, 1.0);
1586            }
1587            _ => panic!("Wrong distribution type"),
1588        }
1589    }
1590
1591    /// A mock simulator that records whether/how many times it was invoked,
1592    /// used to prove `run_single_benchmark` actually calls the injected
1593    /// simulator instead of fabricating timing/results around no-op work.
1594    struct RecordingSimulator {
1595        calls: std::cell::Cell<usize>,
1596    }
1597
1598    impl SimulatorExecutor for RecordingSimulator {
1599        fn execute(&self, _circuit: &dyn std::any::Any) -> QuantRS2Result<ExecutionResult> {
1600            self.calls.set(self.calls.get() + 1);
1601            Ok(ExecutionResult {
1602                measurements: HashMap::new(),
1603                final_state: None,
1604                execution_stats: crate::simulator_interface::ExecutionStats {
1605                    execution_time: Duration::from_millis(1),
1606                    memory_used: 0,
1607                    shots: 1,
1608                    success_rate: 1.0,
1609                },
1610                backend_results: HashMap::new(),
1611            })
1612        }
1613    }
1614
1615    #[test]
1616    fn test_run_single_benchmark_executes_simulator_and_noise_analysis() {
1617        let mut circ: Circuit<2> = Circuit::new();
1618        circ.h(0)
1619            .expect("h gate should apply")
1620            .cnot(0, 1)
1621            .expect("cnot gate should apply");
1622
1623        let benchmark = CircuitBenchmark::new(BenchmarkConfig::default());
1624        let simulator = RecordingSimulator {
1625            calls: std::cell::Cell::new(0),
1626        };
1627        let noise_model = NoiseModel::ibm_quantum();
1628
1629        let run = benchmark
1630            .run_single_benchmark(&circ, &simulator, Some(&noise_model), 0)
1631            .expect("run_single_benchmark should succeed");
1632
1633        assert_eq!(
1634            simulator.calls.get(),
1635            1,
1636            "run_single_benchmark must actually invoke simulator.execute()"
1637        );
1638        assert!(run.success);
1639        assert!(
1640            run.execution_results.is_some(),
1641            "execution_results must be populated from the simulator's real output"
1642        );
1643
1644        let noise_analysis = run
1645            .noise_analysis
1646            .expect("noise analysis must be computed when a noise model is supplied");
1647        assert!(noise_analysis.total_error >= 0.0);
1648        assert!(noise_analysis.total_fidelity <= 1.0);
1649        assert!(
1650            !noise_analysis.gate_errors.is_empty(),
1651            "noise analysis should report per-gate errors for a circuit with gates"
1652        );
1653    }
1654
1655    #[test]
1656    fn test_mann_whitney_clear_difference_rejects_null() {
1657        let analyzer = StatisticalAnalyzer::new();
1658        let low = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1659        let high = vec![101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0];
1660
1661        let result = analyzer
1662            .mann_whitney_test(&low, &high, 0.05)
1663            .expect("mann_whitney_test should succeed");
1664
1665        // U statistic for two completely non-overlapping equal-size samples is 0.
1666        assert_eq!(result.test_statistic, 0.0);
1667        assert!(
1668            result.p_value < 0.01,
1669            "p-value should be tiny for clearly separated distributions, got {}",
1670            result.p_value
1671        );
1672        assert!(result.reject_null);
1673        assert!((result.critical_value - 1.959_963_985).abs() < 1e-6);
1674    }
1675
1676    #[test]
1677    fn test_mann_whitney_identical_distributions_do_not_reject() {
1678        let analyzer = StatisticalAnalyzer::new();
1679        let sample = vec![1.0, 5.0, 3.0, 8.0, 2.0, 9.0, 4.0, 7.0];
1680        let other = sample.clone();
1681
1682        let result = analyzer
1683            .mann_whitney_test(&sample, &other, 0.05)
1684            .expect("mann_whitney_test should succeed");
1685
1686        assert!(
1687            result.p_value > 0.05,
1688            "identical distributions should not show a significant difference, got p={}",
1689            result.p_value
1690        );
1691        assert!(!result.reject_null);
1692    }
1693
1694    #[test]
1695    fn test_regression_slope_p_value_is_real() {
1696        let analyzer = StatisticalAnalyzer::new();
1697
1698        // Perfect, noiseless linear trend: p-value should indicate strong significance.
1699        let trending = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1700        let strong = analyzer
1701            .perform_regression_analysis(&trending)
1702            .expect("perform_regression_analysis should succeed");
1703        assert!(strong.slope_p_value < 1e-6);
1704        assert!(strong.significant_trend);
1705
1706        // Perfectly flat data (zero slope, zero residual variance): the
1707        // real t-test must report no significant trend, unlike the old
1708        // heuristic which only checked `slope.abs() > 0.001`.
1709        let flat = vec![5.0; 10];
1710        let none = analyzer
1711            .perform_regression_analysis(&flat)
1712            .expect("perform_regression_analysis should succeed");
1713        assert!((none.slope_p_value - 1.0).abs() < 1e-9);
1714        assert!(!none.significant_trend);
1715    }
1716
1717    #[test]
1718    fn test_fit_distributions_not_hardcoded() {
1719        let analyzer = StatisticalAnalyzer::new();
1720        let data = vec![
1721            -2.0, -1.5, -1.2, -0.8, -0.5, -0.3, -0.1, 0.0, 0.1, 0.3, 0.5, 0.8, 1.2, 1.5, 2.0,
1722        ];
1723
1724        let fit = analyzer
1725            .fit_distributions(&data)
1726            .expect("fit_distributions should succeed");
1727
1728        assert!((0.0..=1.0).contains(&fit.goodness_of_fit));
1729        assert!((0.0..=1.0).contains(&fit.fit_p_value));
1730        assert!(
1731            (fit.goodness_of_fit - 0.8).abs() > 1e-9 || (fit.fit_p_value - 0.3).abs() > 1e-9,
1732            "goodness_of_fit/fit_p_value must be computed from the data, not the old hardcoded placeholders"
1733        );
1734    }
1735
1736    #[test]
1737    fn test_detect_outliers_modified_zscore() {
1738        let analyzer = StatisticalAnalyzer::new();
1739        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 100.0]; // 100.0 is a clear outlier
1740
1741        let outliers = analyzer
1742            .detect_outliers_modified_zscore(&data, 3.5)
1743            .expect("modified z-score detection should succeed");
1744        assert_eq!(outliers, vec![5]);
1745    }
1746
1747    #[test]
1748    fn test_detect_outliers_unsupported_method_errors_honestly() {
1749        let analyzer = StatisticalAnalyzer::new();
1750        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1751
1752        let isolation_forest_result =
1753            analyzer.detect_outliers(&data, OutlierDetectionMethod::IsolationForest);
1754        assert!(
1755            isolation_forest_result.is_err(),
1756            "unimplemented IsolationForest must return an honest error, not silently report zero outliers"
1757        );
1758
1759        let lof_result =
1760            analyzer.detect_outliers(&data, OutlierDetectionMethod::LocalOutlierFactor);
1761        assert!(lof_result.is_err());
1762    }
1763
1764    #[test]
1765    fn test_benchmark_report_retains_timing_samples_for_baseline_comparison() {
1766        let mut circ: Circuit<1> = Circuit::new();
1767        circ.h(0).expect("h gate should apply");
1768
1769        let config = BenchmarkConfig {
1770            num_runs: 20,
1771            warmup_runs: 2,
1772            ..BenchmarkConfig::default()
1773        };
1774
1775        let simulator = RecordingSimulator {
1776            calls: std::cell::Cell::new(0),
1777        };
1778
1779        let mut baseline_benchmark = CircuitBenchmark::new(config.clone());
1780        let baseline_report = baseline_benchmark
1781            .run_benchmark(&circ, &simulator, None)
1782            .expect("run_benchmark should succeed");
1783
1784        assert_eq!(
1785            baseline_report.timing_samples.len(),
1786            baseline_report.completed_runs,
1787            "the real per-run timing samples must be retained on the report"
1788        );
1789
1790        let mut current_benchmark = CircuitBenchmark::new(config);
1791        current_benchmark
1792            .run_benchmark(&circ, &simulator, None)
1793            .expect("run_benchmark should succeed");
1794
1795        let comparison = current_benchmark
1796            .compare_with_baseline(&baseline_report)
1797            .expect("compare_with_baseline should succeed");
1798
1799        // With the real Mann-Whitney implementation operating on the full
1800        // retained baseline distribution, the U statistic must reflect an
1801        // actual rank comparison rather than the old fixed 0.0 placeholder
1802        // derived from a single scalar baseline sample.
1803        assert!(comparison.significance.test_statistic >= 0.0);
1804        assert!((0.0..=1.0).contains(&comparison.significance.p_value));
1805        assert!(comparison.difference_ci.0 <= comparison.difference_ci.1);
1806    }
1807}