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