quantrs2_sim/
noise_extrapolation.rs

1//! Noise extrapolation techniques for quantum error mitigation.
2//!
3//! This module implements various error mitigation techniques including
4//! Zero-Noise Extrapolation (ZNE), Virtual Distillation, Symmetry Verification,
5//! and other methods to extrapolate quantum results to the zero-noise limit.
6
7use scirs2_core::ndarray::Array2;
8use scirs2_core::parallel_ops::{
9    IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator,
10};
11use scirs2_core::Complex64;
12use serde::{Deserialize, Serialize};
13
14use crate::error::{Result, SimulatorError};
15
16/// Zero-Noise Extrapolation result
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ZNEResult {
19    /// Extrapolated expectation value at zero noise
20    pub zero_noise_value: f64,
21    /// Extrapolation error estimate
22    pub error_estimate: f64,
23    /// Noise scaling factors used
24    pub noise_factors: Vec<f64>,
25    /// Measured expectation values at each noise level
26    pub measured_values: Vec<f64>,
27    /// Measurement uncertainties
28    pub uncertainties: Vec<f64>,
29    /// Extrapolation method used
30    pub method: ExtrapolationMethod,
31    /// Confidence in the extrapolation
32    pub confidence: f64,
33    /// Fitting statistics
34    pub fit_stats: FitStatistics,
35}
36
37/// Virtual Distillation result
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct VirtualDistillationResult {
40    /// Mitigated expectation value
41    pub mitigated_value: f64,
42    /// Overhead factor (number of additional circuits)
43    pub overhead: usize,
44    /// Distillation efficiency
45    pub efficiency: f64,
46    /// Error reduction factor
47    pub error_reduction: f64,
48}
49
50/// Symmetry Verification result
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct SymmetryVerificationResult {
53    /// Corrected expectation value
54    pub corrected_value: f64,
55    /// Symmetry violation measure
56    pub symmetry_violation: f64,
57    /// Post-selection probability
58    pub post_selection_prob: f64,
59    /// Symmetries tested
60    pub symmetries: Vec<String>,
61}
62
63/// Extrapolation methods for ZNE
64#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
65pub enum ExtrapolationMethod {
66    /// Linear extrapolation
67    Linear,
68    /// Exponential extrapolation
69    Exponential,
70    /// Polynomial extrapolation (order specified)
71    Polynomial(usize),
72    /// Richardson extrapolation
73    Richardson,
74    /// Adaptive method selection
75    Adaptive,
76}
77
78/// Noise scaling methods
79#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
80pub enum NoiseScalingMethod {
81    /// Global unitary folding
82    UnitaryFolding,
83    /// Local gate folding
84    LocalFolding,
85    /// Parameter scaling
86    ParameterScaling,
87    /// Identity insertion
88    IdentityInsertion,
89    /// Pauli twirling
90    PauliTwirling,
91}
92
93/// Fitting statistics for extrapolation
94#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95pub struct FitStatistics {
96    /// R-squared value
97    pub r_squared: f64,
98    /// Reduced chi-squared
99    pub chi_squared_reduced: f64,
100    /// Akaike Information Criterion
101    pub aic: f64,
102    /// Number of parameters in fit
103    pub num_parameters: usize,
104    /// Residuals
105    pub residuals: Vec<f64>,
106}
107
108/// Zero-Noise Extrapolation engine
109pub struct ZeroNoiseExtrapolator {
110    /// Noise scaling method
111    scaling_method: NoiseScalingMethod,
112    /// Extrapolation method
113    extrapolation_method: ExtrapolationMethod,
114    /// Noise factors to sample
115    noise_factors: Vec<f64>,
116    /// Number of shots per noise level
117    shots_per_level: usize,
118    /// Maximum polynomial order for adaptive fitting
119    max_poly_order: usize,
120}
121
122impl ZeroNoiseExtrapolator {
123    /// Create new ZNE extrapolator
124    #[must_use]
125    pub const fn new(
126        scaling_method: NoiseScalingMethod,
127        extrapolation_method: ExtrapolationMethod,
128        noise_factors: Vec<f64>,
129        shots_per_level: usize,
130    ) -> Self {
131        Self {
132            scaling_method,
133            extrapolation_method,
134            noise_factors,
135            shots_per_level,
136            max_poly_order: 4,
137        }
138    }
139
140    /// Default configuration for ZNE
141    #[must_use]
142    pub fn default_config() -> Self {
143        Self::new(
144            NoiseScalingMethod::UnitaryFolding,
145            ExtrapolationMethod::Linear,
146            vec![1.0, 1.5, 2.0, 2.5, 3.0],
147            1000,
148        )
149    }
150
151    /// Perform zero-noise extrapolation
152    pub fn extrapolate<F>(&self, noisy_executor: F, observable: &str) -> Result<ZNEResult>
153    where
154        F: Fn(f64) -> Result<f64> + Sync + Send,
155    {
156        let start_time = std::time::Instant::now();
157
158        // Measure expectation values at different noise levels
159        let measurements: Result<Vec<(f64, f64, f64)>> = self
160            .noise_factors
161            .par_iter()
162            .map(|&noise_factor| {
163                // Execute circuit with scaled noise
164                let measured_value = noisy_executor(noise_factor)?;
165
166                // Estimate uncertainty (would use shot noise in practice)
167                let uncertainty = self.estimate_measurement_uncertainty(measured_value);
168
169                Ok((noise_factor, measured_value, uncertainty))
170            })
171            .collect();
172
173        let measurements = measurements?;
174        let (noise_factors, measured_values, uncertainties): (Vec<f64>, Vec<f64>, Vec<f64>) =
175            measurements.unzip3();
176
177        // Perform extrapolation based on method
178        let (zero_noise_value, error_estimate, fit_stats) = match self.extrapolation_method {
179            ExtrapolationMethod::Linear => {
180                self.linear_extrapolation(&noise_factors, &measured_values, &uncertainties)?
181            }
182            ExtrapolationMethod::Exponential => {
183                self.exponential_extrapolation(&noise_factors, &measured_values, &uncertainties)?
184            }
185            ExtrapolationMethod::Polynomial(order) => self.polynomial_extrapolation(
186                &noise_factors,
187                &measured_values,
188                &uncertainties,
189                order,
190            )?,
191            ExtrapolationMethod::Richardson => {
192                self.richardson_extrapolation(&noise_factors, &measured_values)?
193            }
194            ExtrapolationMethod::Adaptive => {
195                self.adaptive_extrapolation(&noise_factors, &measured_values, &uncertainties)?
196            }
197        };
198
199        // Calculate confidence based on fit quality
200        let confidence = self.calculate_confidence(&fit_stats);
201
202        Ok(ZNEResult {
203            zero_noise_value,
204            error_estimate,
205            noise_factors,
206            measured_values,
207            uncertainties,
208            method: self.extrapolation_method,
209            confidence,
210            fit_stats,
211        })
212    }
213
214    /// Linear extrapolation y = a + b*x, extrapolate to x=0
215    fn linear_extrapolation(
216        &self,
217        noise_factors: &[f64],
218        measured_values: &[f64],
219        uncertainties: &[f64],
220    ) -> Result<(f64, f64, FitStatistics)> {
221        if noise_factors.len() < 2 {
222            return Err(SimulatorError::InvalidInput(
223                "Need at least 2 data points for linear extrapolation".to_string(),
224            ));
225        }
226
227        // Weighted least squares fit
228        let (a, b, fit_stats) =
229            self.weighted_linear_fit(noise_factors, measured_values, uncertainties)?;
230
231        // Extrapolate to zero noise (x=0)
232        let zero_noise_value = a;
233
234        // Error estimate from fit uncertainty
235        let error_estimate = fit_stats.residuals.iter().map(|r| r.abs()).sum::<f64>()
236            / fit_stats.residuals.len() as f64;
237
238        Ok((zero_noise_value, error_estimate, fit_stats))
239    }
240
241    /// Exponential extrapolation y = a * exp(b*x), extrapolate to x=0
242    fn exponential_extrapolation(
243        &self,
244        noise_factors: &[f64],
245        measured_values: &[f64],
246        uncertainties: &[f64],
247    ) -> Result<(f64, f64, FitStatistics)> {
248        // Transform to linear: ln(y) = ln(a) + b*x
249        let log_values: Vec<f64> = measured_values
250            .iter()
251            .map(|&y| if y > 0.0 { y.ln() } else { f64::NEG_INFINITY })
252            .collect();
253
254        // Check for negative values
255        if log_values.iter().any(|&x| x.is_infinite()) {
256            return Err(SimulatorError::NumericalError(
257                "Cannot take logarithm of non-positive values for exponential fit".to_string(),
258            ));
259        }
260
261        // Linear fit in log space
262        let log_uncertainties: Vec<f64> = uncertainties.iter().zip(measured_values.iter())
263            .map(|(&u, &y)| u / y.abs()) // Propagate uncertainty: σ(ln(y)) ≈ σ(y)/|y|
264            .collect();
265
266        let (ln_a, b, mut fit_stats) =
267            self.weighted_linear_fit(noise_factors, &log_values, &log_uncertainties)?;
268
269        // Transform back
270        let a = ln_a.exp();
271        let zero_noise_value = a; // At x=0: y = a * exp(0) = a
272
273        // Error propagation
274        let error_estimate = a * fit_stats.residuals.iter().map(|r| r.abs()).sum::<f64>()
275            / fit_stats.residuals.len() as f64;
276
277        // Transform residuals back to original space
278        fit_stats.residuals = fit_stats
279            .residuals
280            .iter()
281            .zip(noise_factors.iter().enumerate())
282            .map(|(&_res, (idx, &x))| a.mul_add((b * x).exp(), -measured_values[idx]))
283            .collect();
284
285        Ok((zero_noise_value, error_estimate, fit_stats))
286    }
287
288    /// Polynomial extrapolation
289    fn polynomial_extrapolation(
290        &self,
291        noise_factors: &[f64],
292        measured_values: &[f64],
293        uncertainties: &[f64],
294        order: usize,
295    ) -> Result<(f64, f64, FitStatistics)> {
296        if noise_factors.len() <= order {
297            return Err(SimulatorError::InvalidInput(format!(
298                "Need more than {order} data points for order {order} polynomial"
299            )));
300        }
301
302        // Construct Vandermonde matrix
303        let n = noise_factors.len();
304        let mut design_matrix = Array2::zeros((n, order + 1));
305
306        for (i, &x) in noise_factors.iter().enumerate() {
307            for j in 0..=order {
308                design_matrix[[i, j]] = x.powi(j as i32);
309            }
310        }
311
312        // Weighted least squares (simplified implementation)
313        let coefficients =
314            self.solve_weighted_least_squares(&design_matrix, measured_values, uncertainties)?;
315
316        // Zero-noise value is the constant term (coefficient of x^0)
317        let zero_noise_value = coefficients[0];
318
319        // Calculate residuals
320        let mut residuals = Vec::with_capacity(n);
321        for (i, &x) in noise_factors.iter().enumerate() {
322            let predicted: f64 = coefficients
323                .iter()
324                .enumerate()
325                .map(|(j, &coeff)| coeff * x.powi(j as i32))
326                .sum();
327            residuals.push(measured_values[i] - predicted);
328        }
329
330        let error_estimate =
331            residuals.iter().map(|r| r.abs()).sum::<f64>() / residuals.len() as f64;
332
333        let fit_stats = FitStatistics {
334            residuals,
335            num_parameters: order + 1,
336            ..Default::default()
337        };
338
339        Ok((zero_noise_value, error_estimate, fit_stats))
340    }
341
342    /// Richardson extrapolation
343    fn richardson_extrapolation(
344        &self,
345        noise_factors: &[f64],
346        measured_values: &[f64],
347    ) -> Result<(f64, f64, FitStatistics)> {
348        if noise_factors.len() < 3 {
349            return Err(SimulatorError::InvalidInput(
350                "Richardson extrapolation requires at least 3 data points".to_string(),
351            ));
352        }
353
354        // Use first three points for Richardson extrapolation
355        let x1 = noise_factors[0];
356        let x2 = noise_factors[1];
357        let x3 = noise_factors[2];
358        let y1 = measured_values[0];
359        let y2 = measured_values[1];
360        let y3 = measured_values[2];
361
362        // Richardson extrapolation formula assuming y = a + b*x + c*x^2
363        let denominator = (x1 - x2) * (x1 - x3) * (x2 - x3);
364        if denominator.abs() < 1e-12 {
365            return Err(SimulatorError::NumericalError(
366                "Noise factors too close for Richardson extrapolation".to_string(),
367            ));
368        }
369
370        let a = (y3 * x1 * x2).mul_add(
371            x1 - x2,
372            (y1 * x2 * x3).mul_add(x2 - x3, y2 * x1 * x3 * (x3 - x1)),
373        ) / denominator;
374
375        let zero_noise_value = a;
376
377        // Error estimate (simplified)
378        let error_estimate = (y1 - y2).abs().max((y2 - y3).abs()) * 0.1;
379
380        let fit_stats = FitStatistics {
381            num_parameters: 3,
382            ..Default::default()
383        };
384
385        Ok((zero_noise_value, error_estimate, fit_stats))
386    }
387
388    /// Adaptive extrapolation method selection
389    fn adaptive_extrapolation(
390        &self,
391        noise_factors: &[f64],
392        measured_values: &[f64],
393        uncertainties: &[f64],
394    ) -> Result<(f64, f64, FitStatistics)> {
395        let mut best_result = None;
396        let mut best_aic = f64::INFINITY;
397
398        // Try different extrapolation methods
399        let methods = vec![
400            ExtrapolationMethod::Linear,
401            ExtrapolationMethod::Exponential,
402            ExtrapolationMethod::Polynomial(2),
403            ExtrapolationMethod::Polynomial(3),
404        ];
405
406        for method in methods {
407            let result = match method {
408                ExtrapolationMethod::Linear => {
409                    self.linear_extrapolation(noise_factors, measured_values, uncertainties)
410                }
411                ExtrapolationMethod::Exponential => {
412                    self.exponential_extrapolation(noise_factors, measured_values, uncertainties)
413                }
414                ExtrapolationMethod::Polynomial(order) => self.polynomial_extrapolation(
415                    noise_factors,
416                    measured_values,
417                    uncertainties,
418                    order,
419                ),
420                _ => continue,
421            };
422
423            if let Ok((value, error, stats)) = result {
424                let aic = stats.aic;
425                if aic < best_aic {
426                    best_aic = aic;
427                    best_result = Some((value, error, stats));
428                }
429            }
430        }
431
432        best_result.ok_or_else(|| {
433            SimulatorError::ComputationError("No extrapolation method succeeded".to_string())
434        })
435    }
436
437    /// Weighted linear fit
438    fn weighted_linear_fit(
439        &self,
440        x: &[f64],
441        y: &[f64],
442        weights: &[f64],
443    ) -> Result<(f64, f64, FitStatistics)> {
444        let n = x.len();
445        if n < 2 {
446            return Err(SimulatorError::InvalidInput(
447                "Need at least 2 points for linear fit".to_string(),
448            ));
449        }
450
451        // Convert uncertainties to weights
452        let w: Vec<f64> = weights
453            .iter()
454            .map(|&sigma| {
455                if sigma > 0.0 {
456                    1.0 / (sigma * sigma)
457                } else {
458                    1.0
459                }
460            })
461            .collect();
462
463        // Weighted least squares formulas
464        let sum_w: f64 = w.iter().sum();
465        let sum_wx: f64 = w.iter().zip(x.iter()).map(|(&wi, &xi)| wi * xi).sum();
466        let sum_wy: f64 = w.iter().zip(y.iter()).map(|(&wi, &yi)| wi * yi).sum();
467        let sum_wxx: f64 = w.iter().zip(x.iter()).map(|(&wi, &xi)| wi * xi * xi).sum();
468        let sum_wxy: f64 = w
469            .iter()
470            .zip(x.iter())
471            .zip(y.iter())
472            .map(|((&wi, &xi), &yi)| wi * xi * yi)
473            .sum();
474
475        let delta = sum_w.mul_add(sum_wxx, -(sum_wx * sum_wx));
476        if delta.abs() < 1e-12 {
477            return Err(SimulatorError::NumericalError(
478                "Singular matrix in linear fit".to_string(),
479            ));
480        }
481
482        let a = sum_wxx.mul_add(sum_wy, -(sum_wx * sum_wxy)) / delta; // intercept
483        let b = sum_w.mul_add(sum_wxy, -(sum_wx * sum_wy)) / delta; // slope
484
485        // Calculate residuals and statistics
486        let mut residuals = Vec::with_capacity(n);
487        let mut ss_res = 0.0;
488        let mut ss_tot = 0.0;
489        let y_mean = y.iter().sum::<f64>() / n as f64;
490
491        for i in 0..n {
492            let predicted = b.mul_add(x[i], a);
493            let residual = y[i] - predicted;
494            residuals.push(residual);
495            ss_res += residual * residual;
496            ss_tot += (y[i] - y_mean) * (y[i] - y_mean);
497        }
498
499        let r_squared = if ss_tot > 0.0 {
500            1.0 - ss_res / ss_tot
501        } else {
502            0.0
503        };
504        let chi_squared_reduced = ss_res / (n - 2) as f64;
505
506        // AIC for linear model
507        let aic = (n as f64).mul_add((ss_res / n as f64).ln(), 2.0 * 2.0); // 2 parameters
508
509        let fit_stats = FitStatistics {
510            r_squared,
511            chi_squared_reduced,
512            aic,
513            num_parameters: 2,
514            residuals,
515        };
516
517        Ok((a, b, fit_stats))
518    }
519
520    /// Solve weighted least squares (simplified)
521    fn solve_weighted_least_squares(
522        &self,
523        design_matrix: &Array2<f64>,
524        y: &[f64],
525        weights: &[f64],
526    ) -> Result<Vec<f64>> {
527        // This is a simplified implementation
528        // In practice, would use proper numerical linear algebra
529        let n_params = design_matrix.ncols();
530        let mut coefficients = vec![0.0; n_params];
531
532        // For now, just return intercept as first measured value
533        // Proper implementation would solve the normal equations
534        coefficients[0] = y[0];
535
536        Ok(coefficients)
537    }
538
539    /// Estimate measurement uncertainty
540    fn estimate_measurement_uncertainty(&self, measured_value: f64) -> f64 {
541        // Shot noise estimate: σ ≈ √(p(1-p)/N) for probability p and N shots
542        let p = f64::midpoint(measured_value, 1.0); // Convert from [-1,1] to [0,1]
543        let shot_noise = (p * (1.0 - p) / self.shots_per_level as f64).sqrt();
544        shot_noise * 2.0 // Convert back to [-1,1] scale
545    }
546
547    /// Calculate confidence in extrapolation
548    fn calculate_confidence(&self, fit_stats: &FitStatistics) -> f64 {
549        // Confidence based on R-squared and reduced chi-squared
550        let r_sq_factor = fit_stats.r_squared.clamp(0.0, 1.0);
551        let chi_sq_factor = if fit_stats.chi_squared_reduced > 0.0 {
552            1.0 / (1.0 + fit_stats.chi_squared_reduced)
553        } else {
554            0.5
555        };
556
557        (r_sq_factor * chi_sq_factor).sqrt()
558    }
559}
560
561/// Virtual Distillation implementation
562pub struct VirtualDistillation {
563    /// Number of copies to use
564    num_copies: usize,
565    /// Distillation protocol
566    protocol: DistillationProtocol,
567}
568
569/// Distillation protocols
570#[derive(Debug, Clone, Copy)]
571pub enum DistillationProtocol {
572    /// Standard virtual distillation
573    Standard,
574    /// Optimized protocol for specific observables
575    Optimized,
576    /// Adaptive protocol
577    Adaptive,
578}
579
580impl VirtualDistillation {
581    /// Create new virtual distillation instance
582    #[must_use]
583    pub const fn new(num_copies: usize, protocol: DistillationProtocol) -> Self {
584        Self {
585            num_copies,
586            protocol,
587        }
588    }
589
590    /// Perform virtual distillation
591    pub fn distill<F>(
592        &self,
593        noisy_executor: F,
594        observable: &str,
595    ) -> Result<VirtualDistillationResult>
596    where
597        F: Fn() -> Result<f64>,
598    {
599        // Execute circuit multiple times
600        let measurements: Result<Vec<f64>> =
601            (0..self.num_copies).map(|_| noisy_executor()).collect();
602
603        let measurements = measurements?;
604
605        // Apply distillation protocol
606        let mitigated_value = match self.protocol {
607            DistillationProtocol::Standard => self.standard_distillation(&measurements)?,
608            DistillationProtocol::Optimized => self.optimized_distillation(&measurements)?,
609            DistillationProtocol::Adaptive => self.adaptive_distillation(&measurements)?,
610        };
611
612        // Calculate efficiency metrics
613        let raw_value = measurements.iter().sum::<f64>() / measurements.len() as f64;
614        let error_reduction = (raw_value - mitigated_value).abs() / raw_value.abs().max(1e-10);
615
616        Ok(VirtualDistillationResult {
617            mitigated_value,
618            overhead: self.num_copies,
619            efficiency: 1.0 / self.num_copies as f64,
620            error_reduction,
621        })
622    }
623
624    /// Standard virtual distillation
625    fn standard_distillation(&self, measurements: &[f64]) -> Result<f64> {
626        // Compute product of measurements and take appropriate root
627        let product: f64 = measurements.iter().product();
628        let mitigated = if product >= 0.0 {
629            product.powf(1.0 / self.num_copies as f64)
630        } else {
631            -(-product).powf(1.0 / self.num_copies as f64)
632        };
633
634        Ok(mitigated)
635    }
636
637    /// Optimized distillation
638    fn optimized_distillation(&self, measurements: &[f64]) -> Result<f64> {
639        // Use median for robustness
640        let mut sorted = measurements.to_vec();
641        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
642        let median = sorted[sorted.len() / 2];
643        Ok(median)
644    }
645
646    /// Adaptive distillation
647    fn adaptive_distillation(&self, measurements: &[f64]) -> Result<f64> {
648        // Choose method based on measurement statistics
649        let mean = measurements.iter().sum::<f64>() / measurements.len() as f64;
650        let variance = measurements.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
651            / measurements.len() as f64;
652
653        if variance < 0.1 {
654            self.standard_distillation(measurements)
655        } else {
656            self.optimized_distillation(measurements)
657        }
658    }
659}
660
661/// Symmetry Verification implementation
662pub struct SymmetryVerification {
663    /// Symmetries to check
664    symmetries: Vec<SymmetryOperation>,
665}
666
667/// Symmetry operation
668#[derive(Debug, Clone)]
669pub struct SymmetryOperation {
670    /// Name of the symmetry
671    pub name: String,
672    /// Expected eigenvalue
673    pub eigenvalue: Complex64,
674    /// Tolerance for symmetry violation
675    pub tolerance: f64,
676}
677
678impl SymmetryVerification {
679    /// Create new symmetry verification
680    #[must_use]
681    pub const fn new(symmetries: Vec<SymmetryOperation>) -> Self {
682        Self { symmetries }
683    }
684
685    /// Verify symmetries and correct expectation value
686    pub fn verify_and_correct<F>(
687        &self,
688        executor: F,
689        observable: &str,
690    ) -> Result<SymmetryVerificationResult>
691    where
692        F: Fn(&str) -> Result<f64>,
693    {
694        let main_value = executor(observable)?;
695
696        let mut violations = Vec::new();
697        let mut valid_measurements = Vec::new();
698
699        // Check each symmetry
700        for symmetry in &self.symmetries {
701            let symmetry_value = executor(&symmetry.name)?;
702            let expected = symmetry.eigenvalue.re;
703            let violation = (symmetry_value - expected).abs();
704
705            violations.push(violation);
706
707            if violation <= symmetry.tolerance {
708                valid_measurements.push(main_value);
709            }
710        }
711
712        // Calculate corrected value
713        let corrected_value = if valid_measurements.is_empty() {
714            main_value // No valid measurements, return original
715        } else {
716            valid_measurements.iter().sum::<f64>() / valid_measurements.len() as f64
717        };
718
719        let avg_violation = violations.iter().sum::<f64>() / violations.len() as f64;
720        let post_selection_prob =
721            valid_measurements.len() as f64 / (self.symmetries.len() + 1) as f64;
722
723        Ok(SymmetryVerificationResult {
724            corrected_value,
725            symmetry_violation: avg_violation,
726            post_selection_prob,
727            symmetries: self.symmetries.iter().map(|s| s.name.clone()).collect(),
728        })
729    }
730}
731
732/// Utility trait for unzipping tuples
733trait Unzip3<A, B, C> {
734    fn unzip3(self) -> (Vec<A>, Vec<B>, Vec<C>);
735}
736
737impl<A, B, C> Unzip3<A, B, C> for Vec<(A, B, C)> {
738    fn unzip3(self) -> (Vec<A>, Vec<B>, Vec<C>) {
739        let mut vec_a = Vec::with_capacity(self.len());
740        let mut vec_b = Vec::with_capacity(self.len());
741        let mut vec_c = Vec::with_capacity(self.len());
742
743        for (a, b, c) in self {
744            vec_a.push(a);
745            vec_b.push(b);
746            vec_c.push(c);
747        }
748
749        (vec_a, vec_b, vec_c)
750    }
751}
752
753/// Benchmark noise extrapolation techniques
754pub fn benchmark_noise_extrapolation() -> Result<(ZNEResult, VirtualDistillationResult)> {
755    // Mock noisy executor
756    let noisy_executor = |noise_factor: f64| -> Result<f64> {
757        // Simulate exponential decay with noise
758        let ideal_value = 1.0;
759        let noise_rate = 0.1;
760        Ok(ideal_value * (-noise_rate * noise_factor).exp())
761    };
762
763    // Test ZNE
764    let zne = ZeroNoiseExtrapolator::default_config();
765    let zne_result = zne.extrapolate(noisy_executor, "Z0")?;
766
767    // Test virtual distillation
768    let vd = VirtualDistillation::new(3, DistillationProtocol::Standard);
769    let vd_result = vd.distill(|| Ok(0.8), "Z0")?;
770
771    Ok((zne_result, vd_result))
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    #[test]
779    fn test_zne_linear_extrapolation() {
780        let zne = ZeroNoiseExtrapolator::new(
781            NoiseScalingMethod::UnitaryFolding,
782            ExtrapolationMethod::Linear,
783            vec![1.0, 2.0, 3.0],
784            100,
785        );
786
787        // Mock data: y = 1.0 - 0.1*x (should extrapolate to 1.0)
788        let noise_factors = vec![1.0, 2.0, 3.0];
789        let measured_values = vec![0.9, 0.8, 0.7];
790        let uncertainties = vec![0.01, 0.01, 0.01];
791
792        let (zero_noise, _error, _stats) = zne
793            .linear_extrapolation(&noise_factors, &measured_values, &uncertainties)
794            .expect("Failed to perform linear extrapolation");
795
796        assert!((zero_noise - 1.0).abs() < 0.1);
797    }
798
799    #[test]
800    fn test_virtual_distillation() {
801        let vd = VirtualDistillation::new(3, DistillationProtocol::Standard);
802
803        let measurements = vec![0.8, 0.7, 0.9];
804        let result = vd
805            .standard_distillation(&measurements)
806            .expect("Failed to perform standard distillation");
807
808        assert!(result > 0.0);
809        assert!(result < 1.0);
810    }
811
812    #[test]
813    fn test_symmetry_verification() {
814        let symmetries = vec![SymmetryOperation {
815            name: "parity".to_string(),
816            eigenvalue: Complex64::new(1.0, 0.0),
817            tolerance: 0.1,
818        }];
819
820        let sv = SymmetryVerification::new(symmetries);
821
822        let executor = |obs: &str| -> Result<f64> {
823            match obs {
824                "Z0" => Ok(0.8),
825                "parity" => Ok(0.95), // Close to expected value 1.0
826                _ => Ok(0.0),
827            }
828        };
829
830        let result = sv
831            .verify_and_correct(executor, "Z0")
832            .expect("Failed to verify and correct");
833        assert!((result.corrected_value - 0.8).abs() < 1e-10);
834        assert!(result.post_selection_prob > 0.0);
835    }
836
837    #[test]
838    fn test_richardson_extrapolation() {
839        let zne = ZeroNoiseExtrapolator::default_config();
840
841        let noise_factors = vec![1.0, 2.0, 3.0];
842        let measured_values = vec![1.0, 0.8, 0.6]; // Quadratic decay
843
844        let (zero_noise, _error, _stats) = zne
845            .richardson_extrapolation(&noise_factors, &measured_values)
846            .expect("Failed to perform Richardson extrapolation");
847
848        assert!(zero_noise > 0.0);
849    }
850}