1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ZNEResult {
19 pub zero_noise_value: f64,
21 pub error_estimate: f64,
23 pub noise_factors: Vec<f64>,
25 pub measured_values: Vec<f64>,
27 pub uncertainties: Vec<f64>,
29 pub method: ExtrapolationMethod,
31 pub confidence: f64,
33 pub fit_stats: FitStatistics,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct VirtualDistillationResult {
40 pub mitigated_value: f64,
42 pub overhead: usize,
44 pub efficiency: f64,
46 pub error_reduction: f64,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct SymmetryVerificationResult {
53 pub corrected_value: f64,
55 pub symmetry_violation: f64,
57 pub post_selection_prob: f64,
59 pub symmetries: Vec<String>,
61}
62
63#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
65pub enum ExtrapolationMethod {
66 Linear,
68 Exponential,
70 Polynomial(usize),
72 Richardson,
74 Adaptive,
76}
77
78#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
80pub enum NoiseScalingMethod {
81 UnitaryFolding,
83 LocalFolding,
85 ParameterScaling,
87 IdentityInsertion,
89 PauliTwirling,
91}
92
93#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95pub struct FitStatistics {
96 pub r_squared: f64,
98 pub chi_squared_reduced: f64,
100 pub aic: f64,
102 pub num_parameters: usize,
104 pub residuals: Vec<f64>,
106}
107
108pub struct ZeroNoiseExtrapolator {
110 scaling_method: NoiseScalingMethod,
112 extrapolation_method: ExtrapolationMethod,
114 noise_factors: Vec<f64>,
116 shots_per_level: usize,
118 max_poly_order: usize,
120}
121
122impl ZeroNoiseExtrapolator {
123 #[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 #[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 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 let measurements: Result<Vec<(f64, f64, f64)>> = self
160 .noise_factors
161 .par_iter()
162 .map(|&noise_factor| {
163 let measured_value = noisy_executor(noise_factor)?;
165
166 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 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 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 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 let (a, b, fit_stats) =
229 self.weighted_linear_fit(noise_factors, measured_values, uncertainties)?;
230
231 let zero_noise_value = a;
233
234 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 fn exponential_extrapolation(
243 &self,
244 noise_factors: &[f64],
245 measured_values: &[f64],
246 uncertainties: &[f64],
247 ) -> Result<(f64, f64, FitStatistics)> {
248 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 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 let log_uncertainties: Vec<f64> = uncertainties.iter().zip(measured_values.iter())
263 .map(|(&u, &y)| u / y.abs()) .collect();
265
266 let (ln_a, b, mut fit_stats) =
267 self.weighted_linear_fit(noise_factors, &log_values, &log_uncertainties)?;
268
269 let a = ln_a.exp();
271 let zero_noise_value = a; let error_estimate = a * fit_stats.residuals.iter().map(|r| r.abs()).sum::<f64>()
275 / fit_stats.residuals.len() as f64;
276
277 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 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 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 = (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 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 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 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 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 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 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; let b = sum_w.mul_add(sum_wxy, -(sum_wx * sum_wy)) / delta; 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 let aic = (n as f64).mul_add((ss_res / n as f64).ln(), 2.0 * 2.0); 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 fn solve_weighted_least_squares(
522 &self,
523 design_matrix: &Array2<f64>,
524 y: &[f64],
525 weights: &[f64],
526 ) -> Result<Vec<f64>> {
527 let n_params = design_matrix.ncols();
530 let mut coefficients = vec![0.0; n_params];
531
532 coefficients[0] = y[0];
535
536 Ok(coefficients)
537 }
538
539 fn estimate_measurement_uncertainty(&self, measured_value: f64) -> f64 {
541 let p = f64::midpoint(measured_value, 1.0); let shot_noise = (p * (1.0 - p) / self.shots_per_level as f64).sqrt();
544 shot_noise * 2.0 }
546
547 fn calculate_confidence(&self, fit_stats: &FitStatistics) -> f64 {
549 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
561pub struct VirtualDistillation {
563 num_copies: usize,
565 protocol: DistillationProtocol,
567}
568
569#[derive(Debug, Clone, Copy)]
571pub enum DistillationProtocol {
572 Standard,
574 Optimized,
576 Adaptive,
578}
579
580impl VirtualDistillation {
581 #[must_use]
583 pub const fn new(num_copies: usize, protocol: DistillationProtocol) -> Self {
584 Self {
585 num_copies,
586 protocol,
587 }
588 }
589
590 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 let measurements: Result<Vec<f64>> =
601 (0..self.num_copies).map(|_| noisy_executor()).collect();
602
603 let measurements = measurements?;
604
605 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 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 fn standard_distillation(&self, measurements: &[f64]) -> Result<f64> {
626 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 fn optimized_distillation(&self, measurements: &[f64]) -> Result<f64> {
639 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 fn adaptive_distillation(&self, measurements: &[f64]) -> Result<f64> {
648 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
661pub struct SymmetryVerification {
663 symmetries: Vec<SymmetryOperation>,
665}
666
667#[derive(Debug, Clone)]
669pub struct SymmetryOperation {
670 pub name: String,
672 pub eigenvalue: Complex64,
674 pub tolerance: f64,
676}
677
678impl SymmetryVerification {
679 #[must_use]
681 pub const fn new(symmetries: Vec<SymmetryOperation>) -> Self {
682 Self { symmetries }
683 }
684
685 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 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 let corrected_value = if valid_measurements.is_empty() {
714 main_value } 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
732trait 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
753pub fn benchmark_noise_extrapolation() -> Result<(ZNEResult, VirtualDistillationResult)> {
755 let noisy_executor = |noise_factor: f64| -> Result<f64> {
757 let ideal_value = 1.0;
759 let noise_rate = 0.1;
760 Ok(ideal_value * (-noise_rate * noise_factor).exp())
761 };
762
763 let zne = ZeroNoiseExtrapolator::default_config();
765 let zne_result = zne.extrapolate(noisy_executor, "Z0")?;
766
767 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 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), _ => 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]; 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}