1use 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#[derive(Debug, Clone, PartialEq)]
22pub enum Distribution {
23 Normal { mean: f64, std_dev: f64 },
25 Uniform { min: f64, max: f64 },
27 Exponential { rate: f64 },
29 Beta { alpha: f64, beta: f64 },
31 Gamma { shape: f64, scale: f64 },
33 Poisson { lambda: f64 },
35 ChiSquared { degrees_of_freedom: usize },
37 StudentT { degrees_of_freedom: usize },
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum StatisticalTest {
44 KolmogorovSmirnov,
46 AndersonDarling,
48 ShapiroWilk,
50 MannWhitney,
52 Wilcoxon,
54 ChiSquaredGoodnessOfFit,
56 ANOVA,
58 KruskalWallis,
60}
61
62#[derive(Debug, Clone)]
64pub struct HypothesisTestResult {
65 pub test_statistic: f64,
67 pub p_value: f64,
69 pub critical_value: f64,
71 pub reject_null: bool,
73 pub significance_level: f64,
75 pub effect_size: Option<f64>,
77 pub confidence_interval: Option<(f64, f64)>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct DescriptiveStats {
84 pub count: usize,
86 pub mean: f64,
88 pub std_dev: f64,
90 pub variance: f64,
92 pub min: f64,
94 pub max: f64,
96 pub median: f64,
98 pub q1: f64,
100 pub q3: f64,
102 pub iqr: f64,
104 pub skewness: f64,
106 pub kurtosis: f64,
108 pub mode: Option<f64>,
110}
111
112#[derive(Debug, Clone)]
114pub struct BenchmarkConfig {
115 pub num_runs: usize,
117 pub warmup_runs: usize,
119 pub timeout: Duration,
121 pub significance_level: f64,
123 pub collect_timing: bool,
125 pub collect_memory: bool,
127 pub collect_errors: bool,
129 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
148pub struct CircuitBenchmark {
150 config: BenchmarkConfig,
152 benchmark_data: Vec<BenchmarkRun>,
154 stats_analyzer: StatisticalAnalyzer,
156}
157
158#[derive(Debug, Clone)]
160pub struct BenchmarkRun {
161 pub run_id: usize,
163 pub execution_time: Duration,
165 pub memory_usage: Option<usize>,
167 pub success: bool,
169 pub error_message: Option<String>,
171 pub circuit_metrics: CircuitMetrics,
173 pub execution_results: Option<ExecutionResult>,
175 pub noise_analysis: Option<NoiseAnalysisResult>,
177 pub custom_metrics: HashMap<String, f64>,
179}
180
181#[derive(Debug, Clone)]
183pub struct CircuitMetrics {
184 pub depth: usize,
186 pub gate_count: usize,
188 pub gate_counts: HashMap<String, usize>,
190 pub two_qubit_gates: usize,
192 pub fidelity: Option<f64>,
194 pub error_rate: Option<f64>,
196}
197
198#[derive(Debug, Clone)]
200pub struct BenchmarkReport {
201 pub config: BenchmarkConfig,
203 pub completed_runs: usize,
205 pub success_rate: f64,
207 pub timing_stats: DescriptiveStats,
209 pub timing_samples: Vec<f64>,
213 pub memory_stats: Option<DescriptiveStats>,
215 pub regression_analysis: Option<RegressionAnalysis>,
217 pub distribution_fit: Option<DistributionFit>,
219 pub outlier_analysis: OutlierAnalysis,
221 pub baseline_comparison: Option<BaselineComparison>,
223 pub statistical_tests: Vec<HypothesisTestResult>,
225 pub insights: Vec<PerformanceInsight>,
227}
228
229#[derive(Debug, Clone)]
231pub struct RegressionAnalysis {
232 pub slope: f64,
234 pub intercept: f64,
236 pub r_squared: f64,
238 pub slope_p_value: f64,
240 pub significant_trend: bool,
242 pub degradation_per_run: f64,
244}
245
246#[derive(Debug, Clone)]
248pub struct DistributionFit {
249 pub best_distribution: Distribution,
251 pub goodness_of_fit: f64,
253 pub fit_p_value: f64,
255 pub alternative_fits: Vec<(Distribution, f64)>,
257}
258
259#[derive(Debug, Clone)]
261pub struct OutlierAnalysis {
262 pub num_outliers: usize,
264 pub outlier_indices: Vec<usize>,
266 pub detection_method: OutlierDetectionMethod,
268 pub threshold: f64,
270 pub outlier_impact: OutlierImpact,
272}
273
274#[derive(Debug, Clone, PartialEq)]
276pub enum OutlierDetectionMethod {
277 IQR { multiplier: f64 },
279 ZScore { threshold: f64 },
281 ModifiedZScore { threshold: f64 },
283 IsolationForest,
285 LocalOutlierFactor,
287}
288
289#[derive(Debug, Clone)]
291pub struct OutlierImpact {
292 pub mean_change: f64,
294 pub std_dev_change: f64,
296 pub median_change: f64,
298 pub relative_impact: f64,
300}
301
302#[derive(Debug, Clone)]
304pub struct BaselineComparison {
305 pub baseline_name: String,
307 pub performance_factor: f64,
309 pub significance: HypothesisTestResult,
311 pub difference_ci: (f64, f64),
313 pub effect_size: f64,
315 pub practical_significance: PracticalSignificance,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
321pub enum PracticalSignificance {
322 Negligible,
324 Small,
326 Medium,
328 Large,
330 VeryLarge,
332}
333
334#[derive(Debug, Clone)]
336pub struct PerformanceInsight {
337 pub category: InsightCategory,
339 pub message: String,
341 pub confidence: f64,
343 pub evidence: Vec<String>,
345 pub recommendations: Vec<String>,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
351pub enum InsightCategory {
352 PerformanceDegradation,
354 PerformanceImprovement,
356 HighVariability,
358 OutliersDetected,
360 MemoryUsage,
362 ErrorRate,
364 OptimizationOpportunity,
366}
367
368impl CircuitBenchmark {
369 #[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 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 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 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 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 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 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, gate_count,
504 gate_counts,
505 two_qubit_gates,
506 fidelity: None,
507 error_rate: None,
508 }
509 }
510
511 const fn get_memory_usage(&self) -> usize {
513 0
515 }
516
517 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 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 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 let regression_analysis = self
559 .stats_analyzer
560 .perform_regression_analysis(&timing_data)?;
561
562 let distribution_fit = self.stats_analyzer.fit_distributions(&timing_data)?;
564
565 let outlier_analysis = self.stats_analyzer.detect_outliers(
567 &timing_data,
568 OutlierDetectionMethod::IQR { multiplier: 1.5 },
569 )?;
570
571 let insights = self.generate_performance_insights(
573 &timing_stats,
574 ®ression_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 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 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 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 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 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 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(¤t_timing)?
723 .mean;
724
725 let performance_factor = current_mean / baseline_mean;
726
727 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 ¤t_timing,
737 &baseline_samples,
738 self.config.significance_level,
739 )?;
740
741 let effect_size = (current_mean - baseline_mean) / baseline.timing_stats.std_dev;
743
744 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 let current_variance = self
757 .stats_analyzer
758 .calculate_descriptive_stats(¤t_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
788pub struct StatisticalAnalyzer;
790
791impl StatisticalAnalyzer {
792 #[must_use]
794 pub const fn new() -> Self {
795 Self
796 }
797
798 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 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, })
843 }
844
845 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 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 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 }
870
871 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 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 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 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 pub fn fit_distributions(&self, data: &[f64]) -> QuantRS2Result<DistributionFit> {
944 let stats = self.calculate_descriptive_stats(data)?;
945
946 let normal_dist = Distribution::Normal {
948 mean: stats.mean,
949 std_dev: stats.std_dev,
950 };
951
952 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1283pub 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
1294fn 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
1323fn normal_cdf(z: f64) -> f64 {
1325 0.5 * (1.0 + erf(z / std::f64::consts::SQRT_2))
1326}
1327
1328fn 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
1387fn 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 (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
1419fn 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
1474fn 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
1493fn 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
1504fn 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]; 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); }
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]; 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 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 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 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 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]; 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 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}