1use crate::{CircuitResult, DeviceError, DeviceResult};
7use quantrs2_circuit::prelude::*;
8use quantrs2_core::prelude::GateOp;
9use scirs2_core::random::thread_rng;
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum NoiseScalingMethod {
15 GlobalFolding,
17 LocalFolding,
19 PulseStretching,
21 DigitalRepetition,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ExtrapolationMethod {
28 Linear,
30 Polynomial(usize),
32 Exponential,
34 Richardson,
36 Adaptive,
38}
39
40#[derive(Debug, Clone)]
42pub struct ZNEConfig {
43 pub scale_factors: Vec<f64>,
45 pub scaling_method: NoiseScalingMethod,
47 pub extrapolation_method: ExtrapolationMethod,
49 pub bootstrap_samples: Option<usize>,
51 pub confidence_level: f64,
53}
54
55impl Default for ZNEConfig {
56 fn default() -> Self {
57 Self {
58 scale_factors: vec![1.0, 1.5, 2.0, 2.5, 3.0],
59 scaling_method: NoiseScalingMethod::GlobalFolding,
60 extrapolation_method: ExtrapolationMethod::Richardson,
61 bootstrap_samples: Some(100),
62 confidence_level: 0.95,
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct ZNEResult {
70 pub mitigated_value: f64,
72 pub error_estimate: Option<f64>,
74 pub raw_data: Vec<(f64, f64)>, pub fit_params: Vec<f64>,
78 pub r_squared: f64,
80 pub extrapolation_fn: String,
82}
83
84pub struct ZNEExecutor<E> {
86 executor: E,
88 config: ZNEConfig,
90}
91
92impl<E> ZNEExecutor<E> {
93 pub const fn new(executor: E, config: ZNEConfig) -> Self {
95 Self { executor, config }
96 }
97
98 pub fn with_defaults(executor: E) -> Self {
100 Self::new(executor, ZNEConfig::default())
101 }
102}
103
104pub trait ZNECapable {
106 fn execute_scaled<const N: usize>(
108 &self,
109 circuit: &Circuit<N>,
110 scale_factor: f64,
111 shots: usize,
112 ) -> DeviceResult<CircuitResult>;
113
114 fn supports_scaling_method(&self, method: NoiseScalingMethod) -> bool;
116}
117
118pub struct CircuitFolder;
120
121impl CircuitFolder {
122 pub fn fold_global<const N: usize>(
124 circuit: &Circuit<N>,
125 scale_factor: f64,
126 ) -> DeviceResult<Circuit<N>> {
127 if scale_factor < 1.0 {
128 return Err(DeviceError::APIError(
129 "Scale factor must be >= 1.0".to_string(),
130 ));
131 }
132
133 if (scale_factor - 1.0).abs() < f64::EPSILON {
134 return Ok(circuit.clone());
135 }
136
137 let num_folds = ((scale_factor - 1.0) / 2.0).floor() as usize;
139 let partial_fold = (scale_factor - 1.0) % 2.0;
140
141 let mut folded_circuit = circuit.clone();
142
143 for _ in 0..num_folds {
145 folded_circuit = Self::apply_full_fold(&folded_circuit)?;
146 }
147
148 if partial_fold > f64::EPSILON {
150 folded_circuit = Self::apply_partial_fold(&folded_circuit, partial_fold)?;
151 }
152
153 Ok(folded_circuit)
154 }
155
156 pub fn fold_local<const N: usize>(
158 circuit: &Circuit<N>,
159 scale_factor: f64,
160 gate_weights: Option<Vec<f64>>,
161 ) -> DeviceResult<Circuit<N>> {
162 if scale_factor < 1.0 {
163 return Err(DeviceError::APIError(
164 "Scale factor must be >= 1.0".to_string(),
165 ));
166 }
167
168 let num_gates = circuit.num_gates();
169 let weights = gate_weights.unwrap_or_else(|| vec![1.0; num_gates]);
170
171 if weights.len() != num_gates {
172 return Err(DeviceError::APIError(
173 "Gate weights length mismatch".to_string(),
174 ));
175 }
176
177 let total_weight: f64 = weights.iter().sum();
179 let normalized_weights: Vec<f64> = weights.iter().map(|w| w / total_weight).collect();
180
181 let extra_noise = scale_factor - 1.0;
183 let fold_amounts: Vec<f64> = normalized_weights
184 .iter()
185 .map(|w| 1.0 + extra_noise * w)
186 .collect();
187
188 let gates = circuit.gates();
190 let mut folded_circuit = Circuit::<N>::new();
191
192 for (idx, gate) in gates.iter().enumerate() {
193 let fold_factor = fold_amounts[idx];
194
195 if (fold_factor - 1.0).abs() < f64::EPSILON {
196 folded_circuit
198 .add_gate_arc(gate.clone())
199 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
200 } else {
201 let num_folds = ((fold_factor - 1.0) / 2.0).floor() as usize;
203 let partial = (fold_factor - 1.0) % 2.0;
204
205 folded_circuit
207 .add_gate_arc(gate.clone())
208 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
209
210 let inverse = Self::create_inverse_gate(gate.as_ref())?;
212 for _ in 0..num_folds {
213 folded_circuit.add_gate_arc(inverse.clone()).map_err(|e| {
214 DeviceError::APIError(format!("Failed to add inverse gate: {e:?}"))
215 })?;
216 folded_circuit
217 .add_gate_arc(gate.clone())
218 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
219 }
220
221 if partial > 0.5 {
223 folded_circuit.add_gate_arc(inverse).map_err(|e| {
225 DeviceError::APIError(format!("Failed to add inverse gate: {e:?}"))
226 })?;
227 folded_circuit
228 .add_gate_arc(gate.clone())
229 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
230 }
231 }
232 }
233
234 Ok(folded_circuit)
235 }
236
237 fn apply_full_fold<const N: usize>(circuit: &Circuit<N>) -> DeviceResult<Circuit<N>> {
239 let gates = circuit.gates();
240 let mut folded_circuit = Circuit::<N>::with_capacity(gates.len() * 3);
241
242 for gate in gates {
244 folded_circuit
246 .add_gate_arc(gate.clone())
247 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
248
249 let inverse = Self::create_inverse_gate(gate.as_ref())?;
251 folded_circuit
252 .add_gate_arc(inverse)
253 .map_err(|e| DeviceError::APIError(format!("Failed to add inverse gate: {e:?}")))?;
254
255 folded_circuit
257 .add_gate_arc(gate.clone())
258 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
259 }
260
261 Ok(folded_circuit)
262 }
263
264 fn apply_partial_fold<const N: usize>(
266 circuit: &Circuit<N>,
267 fraction: f64,
268 ) -> DeviceResult<Circuit<N>> {
269 let gates = circuit.gates();
270 let num_gates_to_fold = (gates.len() as f64 * fraction / 2.0).ceil() as usize;
271
272 let mut folded_circuit = Circuit::<N>::with_capacity(gates.len() + num_gates_to_fold * 2);
273
274 for (idx, gate) in gates.iter().enumerate() {
276 if idx < num_gates_to_fold {
277 folded_circuit
279 .add_gate_arc(gate.clone())
280 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
281
282 let inverse = Self::create_inverse_gate(gate.as_ref())?;
283 folded_circuit.add_gate_arc(inverse).map_err(|e| {
284 DeviceError::APIError(format!("Failed to add inverse gate: {e:?}"))
285 })?;
286
287 folded_circuit
288 .add_gate_arc(gate.clone())
289 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
290 } else {
291 folded_circuit
293 .add_gate_arc(gate.clone())
294 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
295 }
296 }
297
298 Ok(folded_circuit)
299 }
300
301 fn create_inverse_gate(
303 gate: &dyn GateOp,
304 ) -> DeviceResult<std::sync::Arc<dyn GateOp + Send + Sync>> {
305 use quantrs2_core::gate::{multi::*, single::*};
306 use std::sync::Arc;
307
308 match gate.name() {
310 "X" => {
311 let target = gate.qubits()[0];
312 Ok(Arc::new(PauliX { target }))
313 }
314 "Y" => {
315 let target = gate.qubits()[0];
316 Ok(Arc::new(PauliY { target }))
317 }
318 "Z" => {
319 let target = gate.qubits()[0];
320 Ok(Arc::new(PauliZ { target }))
321 }
322 "H" => {
323 let target = gate.qubits()[0];
324 Ok(Arc::new(Hadamard { target }))
325 }
326 "CNOT" => {
327 let qubits = gate.qubits();
328 Ok(Arc::new(CNOT {
329 control: qubits[0],
330 target: qubits[1],
331 }))
332 }
333 "CZ" => {
334 let qubits = gate.qubits();
335 Ok(Arc::new(CZ {
336 control: qubits[0],
337 target: qubits[1],
338 }))
339 }
340 "CY" => {
341 let qubits = gate.qubits();
342 Ok(Arc::new(CY {
343 control: qubits[0],
344 target: qubits[1],
345 }))
346 }
347 "SWAP" => {
348 let qubits = gate.qubits();
349 Ok(Arc::new(SWAP {
350 qubit1: qubits[0],
351 qubit2: qubits[1],
352 }))
353 }
354 "Fredkin" => {
355 let qubits = gate.qubits();
356 Ok(Arc::new(Fredkin {
357 control: qubits[0],
358 target1: qubits[1],
359 target2: qubits[2],
360 }))
361 }
362 "Toffoli" => {
363 let qubits = gate.qubits();
364 Ok(Arc::new(Toffoli {
365 control1: qubits[0],
366 control2: qubits[1],
367 target: qubits[2],
368 }))
369 }
370
371 "S" => {
373 let target = gate.qubits()[0];
374 Ok(Arc::new(PhaseDagger { target }))
375 }
376 "Sdg" => {
377 let target = gate.qubits()[0];
378 Ok(Arc::new(Phase { target }))
379 }
380 "T" => {
381 let target = gate.qubits()[0];
382 Ok(Arc::new(TDagger { target }))
383 }
384 "Tdg" => {
385 let target = gate.qubits()[0];
386 Ok(Arc::new(T { target }))
387 }
388 "SqrtX" => {
389 let target = gate.qubits()[0];
390 Ok(Arc::new(SqrtXDagger { target }))
391 }
392 "SqrtXDagger" => {
393 let target = gate.qubits()[0];
394 Ok(Arc::new(SqrtX { target }))
395 }
396
397 "RX" => {
399 if let Some(rx) = gate.as_any().downcast_ref::<RotationX>() {
400 Ok(Arc::new(RotationX {
401 target: rx.target,
402 theta: -rx.theta,
403 }))
404 } else {
405 Err(DeviceError::APIError(
406 "Failed to downcast RX gate".to_string(),
407 ))
408 }
409 }
410 "RY" => {
411 if let Some(ry) = gate.as_any().downcast_ref::<RotationY>() {
412 Ok(Arc::new(RotationY {
413 target: ry.target,
414 theta: -ry.theta,
415 }))
416 } else {
417 Err(DeviceError::APIError(
418 "Failed to downcast RY gate".to_string(),
419 ))
420 }
421 }
422 "RZ" => {
423 if let Some(rz) = gate.as_any().downcast_ref::<RotationZ>() {
424 Ok(Arc::new(RotationZ {
425 target: rz.target,
426 theta: -rz.theta,
427 }))
428 } else {
429 Err(DeviceError::APIError(
430 "Failed to downcast RZ gate".to_string(),
431 ))
432 }
433 }
434
435 "CRX" => {
437 if let Some(crx) = gate.as_any().downcast_ref::<CRX>() {
438 Ok(Arc::new(CRX {
439 control: crx.control,
440 target: crx.target,
441 theta: -crx.theta,
442 }))
443 } else {
444 Err(DeviceError::APIError(
445 "Failed to downcast CRX gate".to_string(),
446 ))
447 }
448 }
449 "CRY" => {
450 if let Some(cry) = gate.as_any().downcast_ref::<CRY>() {
451 Ok(Arc::new(CRY {
452 control: cry.control,
453 target: cry.target,
454 theta: -cry.theta,
455 }))
456 } else {
457 Err(DeviceError::APIError(
458 "Failed to downcast CRY gate".to_string(),
459 ))
460 }
461 }
462 "CRZ" => {
463 if let Some(crz) = gate.as_any().downcast_ref::<CRZ>() {
464 Ok(Arc::new(CRZ {
465 control: crz.control,
466 target: crz.target,
467 theta: -crz.theta,
468 }))
469 } else {
470 Err(DeviceError::APIError(
471 "Failed to downcast CRZ gate".to_string(),
472 ))
473 }
474 }
475
476 "CH" => {
478 let qubits = gate.qubits();
479 Ok(Arc::new(CH {
480 control: qubits[0],
481 target: qubits[1],
482 }))
483 }
484
485 "CS" => {
487 let qubits = gate.qubits();
488 Ok(Arc::new(CS {
489 control: qubits[0],
490 target: qubits[1],
491 }))
492 }
493
494 _ => Err(DeviceError::APIError(format!(
495 "Cannot create inverse for unsupported gate: {}",
496 gate.name()
497 ))),
498 }
499 }
500}
501
502pub struct ExtrapolationFitter;
504
505impl ExtrapolationFitter {
506 pub fn fit_and_extrapolate(
508 scale_factors: &[f64],
509 values: &[f64],
510 method: ExtrapolationMethod,
511 ) -> DeviceResult<ZNEResult> {
512 if scale_factors.len() != values.len() || scale_factors.is_empty() {
513 return Err(DeviceError::APIError(
514 "Invalid data for extrapolation".to_string(),
515 ));
516 }
517
518 match method {
519 ExtrapolationMethod::Linear => Self::linear_fit(scale_factors, values),
520 ExtrapolationMethod::Polynomial(order) => {
521 Self::polynomial_fit(scale_factors, values, order)
522 }
523 ExtrapolationMethod::Exponential => Self::exponential_fit(scale_factors, values),
524 ExtrapolationMethod::Richardson => {
525 Self::richardson_extrapolation(scale_factors, values)
526 }
527 ExtrapolationMethod::Adaptive => Self::adaptive_fit(scale_factors, values),
528 }
529 }
530
531 fn linear_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
533 let n = x.len() as f64;
534 let sum_x: f64 = x.iter().sum();
535 let sum_y: f64 = y.iter().sum();
536 let sum_xx: f64 = x.iter().map(|xi| xi * xi).sum();
537 let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
538
539 let slope = n.mul_add(sum_xy, -(sum_x * sum_y)) / n.mul_add(sum_xx, -(sum_x * sum_x));
540 let intercept = slope.mul_add(-sum_x, sum_y) / n;
541
542 let y_mean = sum_y / n;
544 let ss_tot: f64 = y.iter().map(|yi| (yi - y_mean).powi(2)).sum();
545 let ss_res: f64 = x
546 .iter()
547 .zip(y.iter())
548 .map(|(xi, yi)| (yi - (slope * xi + intercept)).powi(2))
549 .sum();
550 let r_squared = 1.0 - ss_res / ss_tot;
551
552 Ok(ZNEResult {
553 mitigated_value: intercept, error_estimate: None,
555 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
556 fit_params: vec![intercept, slope],
557 r_squared,
558 extrapolation_fn: format!("y = {intercept:.6} + {slope:.6}x"),
559 })
560 }
561
562 fn polynomial_fit(x: &[f64], y: &[f64], order: usize) -> DeviceResult<ZNEResult> {
564 let n = x.len();
565 if n == 0 {
566 return Err(DeviceError::APIError(
567 "No data points for polynomial fit".to_string(),
568 ));
569 }
570 if order == 0 {
571 let mean_y = y.iter().sum::<f64>() / n as f64;
573 let ss_tot: f64 = y.iter().map(|&yi| (yi - mean_y).powi(2)).sum();
575 let r_squared = if ss_tot < 1e-14 { 1.0 } else { 0.0 };
576 return Ok(ZNEResult {
577 mitigated_value: mean_y,
578 error_estimate: None,
579 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
580 fit_params: vec![mean_y],
581 r_squared,
582 extrapolation_fn: format!("y = {mean_y:.6}"),
583 });
584 }
585
586 let effective_order = order.min(n - 1);
588 let num_coeffs = effective_order + 1;
589
590 let mut vtv = vec![0.0_f64; num_coeffs * num_coeffs];
592 let mut vty = vec![0.0_f64; num_coeffs];
593
594 for i in 0..n {
595 let mut powers = vec![1.0_f64; num_coeffs];
597 for j in 1..num_coeffs {
598 powers[j] = powers[j - 1] * x[i];
599 }
600 for j in 0..num_coeffs {
601 vty[j] += powers[j] * y[i];
602 for k in 0..num_coeffs {
603 vtv[j * num_coeffs + k] += powers[j] * powers[k];
604 }
605 }
606 }
607
608 let coeffs = Self::gaussian_elimination(&vtv, &vty, num_coeffs);
610
611 let zero_noise_value = coeffs[0];
613
614 let y_mean = y.iter().sum::<f64>() / n as f64;
616 let ss_tot: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
617 let ss_res: f64 = x
618 .iter()
619 .zip(y.iter())
620 .map(|(&xi, &yi)| {
621 let y_pred = coeffs
622 .iter()
623 .enumerate()
624 .map(|(j, &c)| c * xi.powi(j as i32))
625 .sum::<f64>();
626 (yi - y_pred).powi(2)
627 })
628 .sum();
629 let r_squared = if ss_tot < 1e-14 {
630 1.0
631 } else {
632 1.0 - ss_res / ss_tot
633 };
634
635 let fn_desc = format!("polynomial order {effective_order}");
636 Ok(ZNEResult {
637 mitigated_value: zero_noise_value,
638 error_estimate: None,
639 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
640 fit_params: coeffs,
641 r_squared,
642 extrapolation_fn: fn_desc,
643 })
644 }
645
646 fn gaussian_elimination(a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
648 let cols = n + 1;
650 let mut mat = vec![0.0_f64; n * cols];
651 for i in 0..n {
652 for j in 0..n {
653 mat[i * cols + j] = a[i * n + j];
654 }
655 mat[i * cols + n] = b[i];
656 }
657
658 for col in 0..n {
660 let mut max_row = col;
662 for row in (col + 1)..n {
663 if mat[row * cols + col].abs() > mat[max_row * cols + col].abs() {
664 max_row = row;
665 }
666 }
667 for j in 0..cols {
669 mat.swap(col * cols + j, max_row * cols + j);
670 }
671
672 let pivot = mat[col * cols + col];
673 if pivot.abs() < 1e-14 {
674 continue;
676 }
677
678 for row in (col + 1)..n {
680 let factor = mat[row * cols + col] / pivot;
681 for j in col..cols {
682 let sub = factor * mat[col * cols + j];
683 mat[row * cols + j] -= sub;
684 }
685 }
686 }
687
688 let mut result = vec![0.0_f64; n];
690 for i in (0..n).rev() {
691 result[i] = mat[i * cols + n];
692 for j in (i + 1)..n {
693 result[i] -= mat[i * cols + j] * result[j];
694 }
695 let diag = mat[i * cols + i];
696 if diag.abs() > 1e-14 {
697 result[i] /= diag;
698 }
699 }
700 result
701 }
702
703 fn exponential_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
705 let log_y: Vec<f64> = y
707 .iter()
708 .map(|yi| {
709 if *yi > 0.0 {
710 Ok(yi.ln())
711 } else {
712 Err(DeviceError::APIError(
713 "Cannot fit exponential to non-positive values".to_string(),
714 ))
715 }
716 })
717 .collect::<DeviceResult<Vec<_>>>()?;
718
719 let linear_result = Self::linear_fit(x, &log_y)?;
721 let ln_a = linear_result.fit_params[0];
722 let b = linear_result.fit_params[1];
723 let a = ln_a.exp();
724
725 Ok(ZNEResult {
726 mitigated_value: a, error_estimate: None,
728 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
729 fit_params: vec![a, b],
730 r_squared: linear_result.r_squared,
731 extrapolation_fn: format!("y = {a:.6} * exp({b:.6}x)"),
732 })
733 }
734
735 fn richardson_extrapolation(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
737 if x.len() < 2 {
738 return Err(DeviceError::APIError(
739 "Need at least 2 points for Richardson extrapolation".to_string(),
740 ));
741 }
742
743 let mut paired: Vec<(f64, f64)> =
745 x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect();
746 paired.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
747
748 let mut richardson_table: Vec<Vec<f64>> = vec![vec![]; paired.len()];
750
751 for i in 0..paired.len() {
753 richardson_table[i].push(paired[i].1);
754 }
755
756 for j in 1..paired.len() {
758 for i in 0..(paired.len() - j) {
759 let x_i = paired[i].0;
760 let x_ij = paired[i + j].0;
761 let factor = x_ij / x_i;
762 let value = factor
763 .mul_add(richardson_table[i + 1][j - 1], -richardson_table[i][j - 1])
764 / (factor - 1.0);
765 richardson_table[i].push(value);
766 }
767 }
768
769 let mitigated = richardson_table[0].last().copied().unwrap_or(paired[0].1);
771
772 Ok(ZNEResult {
773 mitigated_value: mitigated,
774 error_estimate: None,
775 raw_data: paired,
776 fit_params: vec![mitigated],
777 r_squared: 0.95, extrapolation_fn: "Richardson extrapolation".to_string(),
779 })
780 }
781
782 fn adaptive_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
784 let models = vec![
785 ExtrapolationMethod::Linear,
786 ExtrapolationMethod::Polynomial(2),
787 ExtrapolationMethod::Exponential,
788 ];
789
790 let mut best_result = None;
791 let mut best_r2 = -1.0;
792
793 for model in models {
794 if let Ok(result) = Self::fit_and_extrapolate(x, y, model) {
795 if result.r_squared > best_r2 {
796 best_r2 = result.r_squared;
797 best_result = Some(result);
798 }
799 }
800 }
801
802 best_result.ok_or_else(|| DeviceError::APIError("Adaptive fitting failed".to_string()))
803 }
804
805 pub fn bootstrap_estimate(
807 scale_factors: &[f64],
808 values: &[f64],
809 method: ExtrapolationMethod,
810 n_samples: usize,
811 ) -> DeviceResult<f64> {
812 use scirs2_core::random::prelude::*;
813 let mut rng = thread_rng();
814 let n = scale_factors.len();
815 let mut bootstrap_values = Vec::new();
816
817 for _ in 0..n_samples {
818 let mut resampled_x = Vec::new();
820 let mut resampled_y = Vec::new();
821
822 for _ in 0..n {
823 let idx = rng.random_range(0..n);
824 resampled_x.push(scale_factors[idx]);
825 resampled_y.push(values[idx]);
826 }
827
828 if let Ok(result) = Self::fit_and_extrapolate(&resampled_x, &resampled_y, method) {
830 bootstrap_values.push(result.mitigated_value);
831 }
832 }
833
834 if bootstrap_values.is_empty() {
835 return Err(DeviceError::APIError(
836 "Bootstrap estimation failed".to_string(),
837 ));
838 }
839
840 let mean: f64 = bootstrap_values.iter().sum::<f64>() / bootstrap_values.len() as f64;
842 let variance: f64 = bootstrap_values
843 .iter()
844 .map(|v| (v - mean).powi(2))
845 .sum::<f64>()
846 / bootstrap_values.len() as f64;
847
848 Ok(variance.sqrt())
849 }
850}
851
852#[derive(Debug, Clone)]
854pub struct Observable {
855 pub pauli_string: Vec<(usize, String)>, pub coefficient: f64,
859}
860
861impl Observable {
862 pub fn z(qubit: usize) -> Self {
864 Self {
865 pauli_string: vec![(qubit, "Z".to_string())],
866 coefficient: 1.0,
867 }
868 }
869
870 pub fn zz(qubit1: usize, qubit2: usize) -> Self {
872 Self {
873 pauli_string: vec![(qubit1, "Z".to_string()), (qubit2, "Z".to_string())],
874 coefficient: 1.0,
875 }
876 }
877
878 pub fn expectation_value(&self, result: &CircuitResult) -> f64 {
880 let mut expectation = 0.0;
881 let total_shots = result.shots as f64;
882
883 for (bitstring, &count) in &result.counts {
884 let prob = count as f64 / total_shots;
885 let parity = self.calculate_parity(bitstring);
886 expectation += self.coefficient * parity * prob;
887 }
888
889 expectation
890 }
891
892 fn calculate_parity(&self, bitstring: &str) -> f64 {
894 let bits: Vec<char> = bitstring.chars().collect();
895 let mut parity = 1.0;
896
897 for (qubit, pauli) in &self.pauli_string {
898 if *qubit < bits.len() {
899 let bit = bits[*qubit];
900 match pauli.as_str() {
901 "Z" => {
902 if bit == '1' {
903 parity *= -1.0;
904 }
905 }
906 "X" | "Y" => {
907 }
910 _ => {} }
912 }
913 }
914
915 parity
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922
923 #[test]
924 fn test_circuit_folding() {
925 let mut circuit = Circuit::<2>::new();
926 circuit
927 .add_gate(quantrs2_core::gate::single::Hadamard {
928 target: quantrs2_core::qubit::QubitId(0),
929 })
930 .expect("Adding Hadamard gate should succeed");
931 circuit
932 .add_gate(quantrs2_core::gate::multi::CNOT {
933 control: quantrs2_core::qubit::QubitId(0),
934 target: quantrs2_core::qubit::QubitId(1),
935 })
936 .expect("Adding CNOT gate should succeed");
937
938 let folded = CircuitFolder::fold_global(&circuit, 3.0)
940 .expect("Global circuit folding should succeed");
941 assert_eq!(folded.num_gates(), 6);
946
947 let local_folded = CircuitFolder::fold_local(&circuit, 2.0, None)
949 .expect("Local circuit folding should succeed");
950 assert_eq!(local_folded.num_gates(), 2);
958
959 assert!(CircuitFolder::fold_global(&circuit, 0.5).is_err());
961 assert!(CircuitFolder::fold_local(&circuit, 0.5, None).is_err());
962 }
963
964 #[test]
965 fn test_linear_extrapolation() {
966 let x = vec![1.0, 2.0, 3.0, 4.0];
967 let y = vec![1.0, 1.5, 2.0, 2.5];
968
969 let result = ExtrapolationFitter::linear_fit(&x, &y).expect("Linear fit should succeed");
970 assert!((result.mitigated_value - 0.5).abs() < 0.01); assert!(result.r_squared > 0.99); }
973
974 #[test]
975 fn test_richardson_extrapolation() {
976 let x = vec![1.0, 1.5, 2.0, 3.0];
977 let y = vec![1.0, 1.25, 1.5, 2.0];
978
979 let result = ExtrapolationFitter::richardson_extrapolation(&x, &y)
980 .expect("Richardson extrapolation should succeed");
981 assert!(result.mitigated_value.is_finite());
984 assert_eq!(result.extrapolation_fn, "Richardson extrapolation");
985 }
986
987 #[test]
988 fn test_observable() {
989 let obs = Observable::z(0);
990
991 let mut counts = HashMap::new();
992 counts.insert("00".to_string(), 75);
993 counts.insert("10".to_string(), 25);
994
995 let result = CircuitResult {
996 counts,
997 shots: 100,
998 metadata: HashMap::new(),
999 };
1000
1001 let exp_val = obs.expectation_value(&result);
1002 assert!((exp_val - 0.5).abs() < 0.01); }
1004
1005 #[test]
1006 fn test_zne_config() {
1007 let config = ZNEConfig::default();
1008 assert_eq!(config.scale_factors, vec![1.0, 1.5, 2.0, 2.5, 3.0]);
1009 assert_eq!(config.scaling_method, NoiseScalingMethod::GlobalFolding);
1010 assert_eq!(config.extrapolation_method, ExtrapolationMethod::Richardson);
1011 }
1012
1013 #[test]
1014 fn test_polynomial_fit_linear() {
1015 let x = vec![1.0, 2.0, 3.0, 4.0];
1017 let y = vec![3.0, 5.0, 7.0, 9.0];
1018 let result =
1019 ExtrapolationFitter::fit_and_extrapolate(&x, &y, ExtrapolationMethod::Polynomial(1))
1020 .expect("polynomial order-1 fit should succeed");
1021 assert!(
1022 (result.mitigated_value - 1.0).abs() < 1e-6,
1023 "zero-noise intercept should be 1.0, got {}",
1024 result.mitigated_value
1025 );
1026 assert!(
1027 result.r_squared > 0.999,
1028 "R² should be ≈ 1.0 for perfect linear data, got {}",
1029 result.r_squared
1030 );
1031 }
1032
1033 #[test]
1034 fn test_polynomial_fit_quadratic() {
1035 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1037 let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
1038 let result =
1039 ExtrapolationFitter::fit_and_extrapolate(&x, &y, ExtrapolationMethod::Polynomial(2))
1040 .expect("polynomial order-2 fit should succeed");
1041 assert!(
1042 result.mitigated_value.abs() < 1e-4,
1043 "zero-noise extrapolation for x² should be ≈ 0, got {}",
1044 result.mitigated_value
1045 );
1046 assert!(
1047 result.r_squared > 0.999,
1048 "R² should be ≈ 1.0 for perfect quadratic data, got {}",
1049 result.r_squared
1050 );
1051 }
1052
1053 #[test]
1054 fn test_polynomial_fit_higher_order() {
1055 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1057 let y: Vec<f64> = x.iter().map(|&xi| xi * xi * xi).collect();
1058 let result =
1059 ExtrapolationFitter::fit_and_extrapolate(&x, &y, ExtrapolationMethod::Polynomial(3))
1060 .expect("polynomial order-3 fit should succeed");
1061 assert!(
1062 result.r_squared > 0.999,
1063 "R² for cubic fit on cubic data should be ≈ 1.0, got {}",
1064 result.r_squared
1065 );
1066 }
1067}