1use ndarray::Array2;
8use num_complex::Complex64;
9use scirs2_core::parallel_ops::*;
10use serde::{Deserialize, Serialize};
11
12use crate::error::{Result, SimulatorError};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ZNEResult {
17 pub zero_noise_value: f64,
19 pub error_estimate: f64,
21 pub noise_factors: Vec<f64>,
23 pub measured_values: Vec<f64>,
25 pub uncertainties: Vec<f64>,
27 pub method: ExtrapolationMethod,
29 pub confidence: f64,
31 pub fit_stats: FitStatistics,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct VirtualDistillationResult {
38 pub mitigated_value: f64,
40 pub overhead: usize,
42 pub efficiency: f64,
44 pub error_reduction: f64,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct SymmetryVerificationResult {
51 pub corrected_value: f64,
53 pub symmetry_violation: f64,
55 pub post_selection_prob: f64,
57 pub symmetries: Vec<String>,
59}
60
61#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
63pub enum ExtrapolationMethod {
64 Linear,
66 Exponential,
68 Polynomial(usize),
70 Richardson,
72 Adaptive,
74}
75
76#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
78pub enum NoiseScalingMethod {
79 UnitaryFolding,
81 LocalFolding,
83 ParameterScaling,
85 IdentityInsertion,
87 PauliTwirling,
89}
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
93pub struct FitStatistics {
94 pub r_squared: f64,
96 pub chi_squared_reduced: f64,
98 pub aic: f64,
100 pub num_parameters: usize,
102 pub residuals: Vec<f64>,
104}
105
106pub struct ZeroNoiseExtrapolator {
108 scaling_method: NoiseScalingMethod,
110 extrapolation_method: ExtrapolationMethod,
112 noise_factors: Vec<f64>,
114 shots_per_level: usize,
116 max_poly_order: usize,
118}
119
120impl ZeroNoiseExtrapolator {
121 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 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 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 let measurements: Result<Vec<(f64, f64, f64)>> = self
156 .noise_factors
157 .par_iter()
158 .map(|&noise_factor| {
159 let measured_value = noisy_executor(noise_factor)?;
161
162 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 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 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 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 let (a, b, fit_stats) =
225 self.weighted_linear_fit(noise_factors, measured_values, uncertainties)?;
226
227 let zero_noise_value = a;
229
230 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 fn exponential_extrapolation(
239 &self,
240 noise_factors: &[f64],
241 measured_values: &[f64],
242 uncertainties: &[f64],
243 ) -> Result<(f64, f64, FitStatistics)> {
244 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 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 let log_uncertainties: Vec<f64> = uncertainties.iter().zip(measured_values.iter())
259 .map(|(&u, &y)| u / y.abs()) .collect();
261
262 let (ln_a, b, mut fit_stats) =
263 self.weighted_linear_fit(noise_factors, &log_values, &log_uncertainties)?;
264
265 let a = ln_a.exp();
267 let zero_noise_value = a; let error_estimate = a * fit_stats.residuals.iter().map(|r| r.abs()).sum::<f64>()
271 / fit_stats.residuals.len() as f64;
272
273 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 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 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 let coefficients =
314 self.solve_weighted_least_squares(&design_matrix, measured_values, uncertainties)?;
315
316 let zero_noise_value = coefficients[0];
318
319 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 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 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 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 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 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 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 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 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 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; let b = (sum_w * sum_wxy - sum_wx * sum_wy) / delta; 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 let aic = n as f64 * (ss_res / n as f64).ln() + 2.0 * 2.0; 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 fn solve_weighted_least_squares(
520 &self,
521 design_matrix: &Array2<f64>,
522 y: &[f64],
523 weights: &[f64],
524 ) -> Result<Vec<f64>> {
525 let n_params = design_matrix.ncols();
528 let mut coefficients = vec![0.0; n_params];
529
530 coefficients[0] = y[0];
533
534 Ok(coefficients)
535 }
536
537 fn estimate_measurement_uncertainty(&self, measured_value: f64) -> f64 {
539 let p = (measured_value + 1.0) / 2.0; let shot_noise = (p * (1.0 - p) / self.shots_per_level as f64).sqrt();
542 shot_noise * 2.0 }
544
545 fn calculate_confidence(&self, fit_stats: &FitStatistics) -> f64 {
547 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
559pub struct VirtualDistillation {
561 num_copies: usize,
563 protocol: DistillationProtocol,
565}
566
567#[derive(Debug, Clone, Copy)]
569pub enum DistillationProtocol {
570 Standard,
572 Optimized,
574 Adaptive,
576}
577
578impl VirtualDistillation {
579 pub fn new(num_copies: usize, protocol: DistillationProtocol) -> Self {
581 Self {
582 num_copies,
583 protocol,
584 }
585 }
586
587 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 let measurements: Result<Vec<f64>> =
598 (0..self.num_copies).map(|_| noisy_executor()).collect();
599
600 let measurements = measurements?;
601
602 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 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 fn standard_distillation(&self, measurements: &[f64]) -> Result<f64> {
623 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 fn optimized_distillation(&self, measurements: &[f64]) -> Result<f64> {
636 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 fn adaptive_distillation(&self, measurements: &[f64]) -> Result<f64> {
645 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
658pub struct SymmetryVerification {
660 symmetries: Vec<SymmetryOperation>,
662}
663
664#[derive(Debug, Clone)]
666pub struct SymmetryOperation {
667 pub name: String,
669 pub eigenvalue: Complex64,
671 pub tolerance: f64,
673}
674
675impl SymmetryVerification {
676 pub fn new(symmetries: Vec<SymmetryOperation>) -> Self {
678 Self { symmetries }
679 }
680
681 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 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 let corrected_value = if valid_measurements.is_empty() {
710 main_value } 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
728trait 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
749pub fn benchmark_noise_extrapolation() -> Result<(ZNEResult, VirtualDistillationResult)> {
751 let noisy_executor = |noise_factor: f64| -> Result<f64> {
753 let ideal_value = 1.0;
755 let noise_rate = 0.1;
756 Ok(ideal_value * (-noise_rate * noise_factor).exp())
757 };
758
759 let zne = ZeroNoiseExtrapolator::default_config();
761 let zne_result = zne.extrapolate(noisy_executor, "Z0")?;
762
763 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 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), _ => 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]; let (zero_noise, _error, _stats) = zne
837 .richardson_extrapolation(&noise_factors, &measured_values)
838 .unwrap();
839
840 assert!(zero_noise > 0.0);
841 }
842}