1use crate::{CircuitResult, DeviceError, DeviceResult};
7use quantrs2_circuit::prelude::*;
8use quantrs2_core::prelude::GateOp;
9use scirs2_core::ndarray::{Array1, Array2};
10use scirs2_core::random::thread_rng;
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum NoiseScalingMethod {
16 GlobalFolding,
18 LocalFolding,
20 PulseStretching,
22 DigitalRepetition,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ExtrapolationMethod {
29 Linear,
31 Polynomial(usize),
33 Exponential,
35 Richardson,
37 Adaptive,
39}
40
41#[derive(Debug, Clone)]
43pub struct ZNEConfig {
44 pub scale_factors: Vec<f64>,
46 pub scaling_method: NoiseScalingMethod,
48 pub extrapolation_method: ExtrapolationMethod,
50 pub bootstrap_samples: Option<usize>,
52 pub confidence_level: f64,
54}
55
56impl Default for ZNEConfig {
57 fn default() -> Self {
58 Self {
59 scale_factors: vec![1.0, 1.5, 2.0, 2.5, 3.0],
60 scaling_method: NoiseScalingMethod::GlobalFolding,
61 extrapolation_method: ExtrapolationMethod::Richardson,
62 bootstrap_samples: Some(100),
63 confidence_level: 0.95,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct ZNEResult {
71 pub mitigated_value: f64,
73 pub error_estimate: Option<f64>,
75 pub raw_data: Vec<(f64, f64)>, pub fit_params: Vec<f64>,
79 pub r_squared: f64,
81 pub extrapolation_fn: String,
83}
84
85pub struct ZNEExecutor<E> {
87 executor: E,
89 config: ZNEConfig,
91}
92
93impl<E> ZNEExecutor<E> {
94 pub const fn new(executor: E, config: ZNEConfig) -> Self {
96 Self { executor, config }
97 }
98
99 pub fn with_defaults(executor: E) -> Self {
101 Self::new(executor, ZNEConfig::default())
102 }
103}
104
105pub trait ZNECapable {
107 fn execute_scaled<const N: usize>(
109 &self,
110 circuit: &Circuit<N>,
111 scale_factor: f64,
112 shots: usize,
113 ) -> DeviceResult<CircuitResult>;
114
115 fn supports_scaling_method(&self, method: NoiseScalingMethod) -> bool;
117}
118
119pub struct CircuitFolder;
121
122impl CircuitFolder {
123 pub fn fold_global<const N: usize>(
125 circuit: &Circuit<N>,
126 scale_factor: f64,
127 ) -> DeviceResult<Circuit<N>> {
128 if scale_factor < 1.0 {
129 return Err(DeviceError::APIError(
130 "Scale factor must be >= 1.0".to_string(),
131 ));
132 }
133
134 if (scale_factor - 1.0).abs() < f64::EPSILON {
135 return Ok(circuit.clone());
136 }
137
138 let num_folds = ((scale_factor - 1.0) / 2.0).floor() as usize;
140 let partial_fold = (scale_factor - 1.0) % 2.0;
141
142 let mut folded_circuit = circuit.clone();
143
144 for _ in 0..num_folds {
146 folded_circuit = Self::apply_full_fold(&folded_circuit)?;
147 }
148
149 if partial_fold > f64::EPSILON {
151 folded_circuit = Self::apply_partial_fold(&folded_circuit, partial_fold)?;
152 }
153
154 Ok(folded_circuit)
155 }
156
157 pub fn fold_local<const N: usize>(
159 circuit: &Circuit<N>,
160 scale_factor: f64,
161 gate_weights: Option<Vec<f64>>,
162 ) -> DeviceResult<Circuit<N>> {
163 if scale_factor < 1.0 {
164 return Err(DeviceError::APIError(
165 "Scale factor must be >= 1.0".to_string(),
166 ));
167 }
168
169 let num_gates = circuit.num_gates();
170 let weights = gate_weights.unwrap_or_else(|| vec![1.0; num_gates]);
171
172 if weights.len() != num_gates {
173 return Err(DeviceError::APIError(
174 "Gate weights length mismatch".to_string(),
175 ));
176 }
177
178 let total_weight: f64 = weights.iter().sum();
180 let normalized_weights: Vec<f64> = weights.iter().map(|w| w / total_weight).collect();
181
182 let extra_noise = scale_factor - 1.0;
184 let fold_amounts: Vec<f64> = normalized_weights
185 .iter()
186 .map(|w| 1.0 + extra_noise * w)
187 .collect();
188
189 let gates = circuit.gates();
191 let mut folded_circuit = Circuit::<N>::new();
192
193 for (idx, gate) in gates.iter().enumerate() {
194 let fold_factor = fold_amounts[idx];
195
196 if (fold_factor - 1.0).abs() < f64::EPSILON {
197 folded_circuit
199 .add_gate_arc(gate.clone())
200 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
201 } else {
202 let num_folds = ((fold_factor - 1.0) / 2.0).floor() as usize;
204 let partial = (fold_factor - 1.0) % 2.0;
205
206 folded_circuit
208 .add_gate_arc(gate.clone())
209 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
210
211 let inverse = Self::create_inverse_gate(gate.as_ref())?;
213 for _ in 0..num_folds {
214 folded_circuit.add_gate_arc(inverse.clone()).map_err(|e| {
215 DeviceError::APIError(format!("Failed to add inverse gate: {e:?}"))
216 })?;
217 folded_circuit
218 .add_gate_arc(gate.clone())
219 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
220 }
221
222 if partial > 0.5 {
224 folded_circuit.add_gate_arc(inverse).map_err(|e| {
226 DeviceError::APIError(format!("Failed to add inverse gate: {e:?}"))
227 })?;
228 folded_circuit
229 .add_gate_arc(gate.clone())
230 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
231 }
232 }
233 }
234
235 Ok(folded_circuit)
236 }
237
238 fn apply_full_fold<const N: usize>(circuit: &Circuit<N>) -> DeviceResult<Circuit<N>> {
240 let gates = circuit.gates();
241 let mut folded_circuit = Circuit::<N>::with_capacity(gates.len() * 3);
242
243 for gate in gates {
245 folded_circuit
247 .add_gate_arc(gate.clone())
248 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
249
250 let inverse = Self::create_inverse_gate(gate.as_ref())?;
252 folded_circuit
253 .add_gate_arc(inverse)
254 .map_err(|e| DeviceError::APIError(format!("Failed to add inverse gate: {e:?}")))?;
255
256 folded_circuit
258 .add_gate_arc(gate.clone())
259 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
260 }
261
262 Ok(folded_circuit)
263 }
264
265 fn apply_partial_fold<const N: usize>(
267 circuit: &Circuit<N>,
268 fraction: f64,
269 ) -> DeviceResult<Circuit<N>> {
270 let gates = circuit.gates();
271 let num_gates_to_fold = (gates.len() as f64 * fraction / 2.0).ceil() as usize;
272
273 let mut folded_circuit = Circuit::<N>::with_capacity(gates.len() + num_gates_to_fold * 2);
274
275 for (idx, gate) in gates.iter().enumerate() {
277 if idx < num_gates_to_fold {
278 folded_circuit
280 .add_gate_arc(gate.clone())
281 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
282
283 let inverse = Self::create_inverse_gate(gate.as_ref())?;
284 folded_circuit.add_gate_arc(inverse).map_err(|e| {
285 DeviceError::APIError(format!("Failed to add inverse gate: {e:?}"))
286 })?;
287
288 folded_circuit
289 .add_gate_arc(gate.clone())
290 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
291 } else {
292 folded_circuit
294 .add_gate_arc(gate.clone())
295 .map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
296 }
297 }
298
299 Ok(folded_circuit)
300 }
301
302 fn create_inverse_gate(
304 gate: &dyn GateOp,
305 ) -> DeviceResult<std::sync::Arc<dyn GateOp + Send + Sync>> {
306 use quantrs2_core::gate::{multi::*, single::*};
307 use std::sync::Arc;
308
309 match gate.name() {
311 "X" => {
312 let target = gate.qubits()[0];
313 Ok(Arc::new(PauliX { target }))
314 }
315 "Y" => {
316 let target = gate.qubits()[0];
317 Ok(Arc::new(PauliY { target }))
318 }
319 "Z" => {
320 let target = gate.qubits()[0];
321 Ok(Arc::new(PauliZ { target }))
322 }
323 "H" => {
324 let target = gate.qubits()[0];
325 Ok(Arc::new(Hadamard { target }))
326 }
327 "CNOT" => {
328 let qubits = gate.qubits();
329 Ok(Arc::new(CNOT {
330 control: qubits[0],
331 target: qubits[1],
332 }))
333 }
334 "CZ" => {
335 let qubits = gate.qubits();
336 Ok(Arc::new(CZ {
337 control: qubits[0],
338 target: qubits[1],
339 }))
340 }
341 "CY" => {
342 let qubits = gate.qubits();
343 Ok(Arc::new(CY {
344 control: qubits[0],
345 target: qubits[1],
346 }))
347 }
348 "SWAP" => {
349 let qubits = gate.qubits();
350 Ok(Arc::new(SWAP {
351 qubit1: qubits[0],
352 qubit2: qubits[1],
353 }))
354 }
355 "Fredkin" => {
356 let qubits = gate.qubits();
357 Ok(Arc::new(Fredkin {
358 control: qubits[0],
359 target1: qubits[1],
360 target2: qubits[2],
361 }))
362 }
363 "Toffoli" => {
364 let qubits = gate.qubits();
365 Ok(Arc::new(Toffoli {
366 control1: qubits[0],
367 control2: qubits[1],
368 target: qubits[2],
369 }))
370 }
371
372 "S" => {
374 let target = gate.qubits()[0];
375 Ok(Arc::new(PhaseDagger { target }))
376 }
377 "Sdg" => {
378 let target = gate.qubits()[0];
379 Ok(Arc::new(Phase { target }))
380 }
381 "T" => {
382 let target = gate.qubits()[0];
383 Ok(Arc::new(TDagger { target }))
384 }
385 "Tdg" => {
386 let target = gate.qubits()[0];
387 Ok(Arc::new(T { target }))
388 }
389 "SqrtX" => {
390 let target = gate.qubits()[0];
391 Ok(Arc::new(SqrtXDagger { target }))
392 }
393 "SqrtXDagger" => {
394 let target = gate.qubits()[0];
395 Ok(Arc::new(SqrtX { target }))
396 }
397
398 "RX" => {
400 if let Some(rx) = gate.as_any().downcast_ref::<RotationX>() {
401 Ok(Arc::new(RotationX {
402 target: rx.target,
403 theta: -rx.theta,
404 }))
405 } else {
406 Err(DeviceError::APIError(
407 "Failed to downcast RX gate".to_string(),
408 ))
409 }
410 }
411 "RY" => {
412 if let Some(ry) = gate.as_any().downcast_ref::<RotationY>() {
413 Ok(Arc::new(RotationY {
414 target: ry.target,
415 theta: -ry.theta,
416 }))
417 } else {
418 Err(DeviceError::APIError(
419 "Failed to downcast RY gate".to_string(),
420 ))
421 }
422 }
423 "RZ" => {
424 if let Some(rz) = gate.as_any().downcast_ref::<RotationZ>() {
425 Ok(Arc::new(RotationZ {
426 target: rz.target,
427 theta: -rz.theta,
428 }))
429 } else {
430 Err(DeviceError::APIError(
431 "Failed to downcast RZ gate".to_string(),
432 ))
433 }
434 }
435
436 "CRX" => {
438 if let Some(crx) = gate.as_any().downcast_ref::<CRX>() {
439 Ok(Arc::new(CRX {
440 control: crx.control,
441 target: crx.target,
442 theta: -crx.theta,
443 }))
444 } else {
445 Err(DeviceError::APIError(
446 "Failed to downcast CRX gate".to_string(),
447 ))
448 }
449 }
450 "CRY" => {
451 if let Some(cry) = gate.as_any().downcast_ref::<CRY>() {
452 Ok(Arc::new(CRY {
453 control: cry.control,
454 target: cry.target,
455 theta: -cry.theta,
456 }))
457 } else {
458 Err(DeviceError::APIError(
459 "Failed to downcast CRY gate".to_string(),
460 ))
461 }
462 }
463 "CRZ" => {
464 if let Some(crz) = gate.as_any().downcast_ref::<CRZ>() {
465 Ok(Arc::new(CRZ {
466 control: crz.control,
467 target: crz.target,
468 theta: -crz.theta,
469 }))
470 } else {
471 Err(DeviceError::APIError(
472 "Failed to downcast CRZ gate".to_string(),
473 ))
474 }
475 }
476
477 "CH" => {
479 let qubits = gate.qubits();
480 Ok(Arc::new(CH {
481 control: qubits[0],
482 target: qubits[1],
483 }))
484 }
485
486 "CS" => {
488 let qubits = gate.qubits();
489 Ok(Arc::new(CS {
490 control: qubits[0],
491 target: qubits[1],
492 }))
493 }
494
495 _ => Err(DeviceError::APIError(format!(
496 "Cannot create inverse for unsupported gate: {}",
497 gate.name()
498 ))),
499 }
500 }
501}
502
503pub struct ExtrapolationFitter;
505
506impl ExtrapolationFitter {
507 pub fn fit_and_extrapolate(
509 scale_factors: &[f64],
510 values: &[f64],
511 method: ExtrapolationMethod,
512 ) -> DeviceResult<ZNEResult> {
513 if scale_factors.len() != values.len() || scale_factors.is_empty() {
514 return Err(DeviceError::APIError(
515 "Invalid data for extrapolation".to_string(),
516 ));
517 }
518
519 match method {
520 ExtrapolationMethod::Linear => Self::linear_fit(scale_factors, values),
521 ExtrapolationMethod::Polynomial(order) => {
522 Self::polynomial_fit(scale_factors, values, order)
523 }
524 ExtrapolationMethod::Exponential => Self::exponential_fit(scale_factors, values),
525 ExtrapolationMethod::Richardson => {
526 Self::richardson_extrapolation(scale_factors, values)
527 }
528 ExtrapolationMethod::Adaptive => Self::adaptive_fit(scale_factors, values),
529 }
530 }
531
532 fn linear_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
534 let n = x.len() as f64;
535 let sum_x: f64 = x.iter().sum();
536 let sum_y: f64 = y.iter().sum();
537 let sum_xx: f64 = x.iter().map(|xi| xi * xi).sum();
538 let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
539
540 let slope = n.mul_add(sum_xy, -(sum_x * sum_y)) / n.mul_add(sum_xx, -(sum_x * sum_x));
541 let intercept = slope.mul_add(-sum_x, sum_y) / n;
542
543 let y_mean = sum_y / n;
545 let ss_tot: f64 = y.iter().map(|yi| (yi - y_mean).powi(2)).sum();
546 let ss_res: f64 = x
547 .iter()
548 .zip(y.iter())
549 .map(|(xi, yi)| (yi - (slope * xi + intercept)).powi(2))
550 .sum();
551 let r_squared = 1.0 - ss_res / ss_tot;
552
553 Ok(ZNEResult {
554 mitigated_value: intercept, error_estimate: None,
556 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
557 fit_params: vec![intercept, slope],
558 r_squared,
559 extrapolation_fn: format!("y = {intercept:.6} + {slope:.6}x"),
560 })
561 }
562
563 fn polynomial_fit(x: &[f64], y: &[f64], order: usize) -> DeviceResult<ZNEResult> {
565 let n = x.len();
566 if order >= n {
567 return Err(DeviceError::APIError(
568 "Polynomial order too high for data".to_string(),
569 ));
570 }
571
572 let mut a = Array2::<f64>::zeros((n, order + 1));
574 for i in 0..n {
575 for j in 0..=order {
576 a[[i, j]] = x[i].powi(j as i32);
577 }
578 }
579
580 let y_vec = Array1::from_vec(y.to_vec());
582
583 if order == 2 {
585 let sum_x: f64 = x.iter().sum();
587 let sum_x2: f64 = x.iter().map(|xi| xi * xi).sum();
588 let sum_x3: f64 = x.iter().map(|xi| xi * xi * xi).sum();
589 let sum_x4: f64 = x.iter().map(|xi| xi * xi * xi * xi).sum();
590 let sum_y: f64 = y.iter().sum();
591 let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
592 let sum_x2y: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * xi * yi).sum();
593
594 let det = sum_x2.mul_add(
596 sum_x.mul_add(sum_x3, -(sum_x2 * sum_x2)),
597 (n as f64).mul_add(
598 sum_x2.mul_add(sum_x4, -(sum_x3 * sum_x3)),
599 -(sum_x * sum_x.mul_add(sum_x4, -(sum_x2 * sum_x3))),
600 ),
601 );
602
603 let a = sum_x2y.mul_add(
604 sum_x.mul_add(sum_x3, -(sum_x2 * sum_x2)),
605 sum_y.mul_add(
606 sum_x2.mul_add(sum_x4, -(sum_x3 * sum_x3)),
607 -(sum_xy * sum_x.mul_add(sum_x4, -(sum_x2 * sum_x3))),
608 ),
609 ) / det;
610
611 return Ok(ZNEResult {
612 mitigated_value: a,
613 error_estimate: None,
614 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
615 fit_params: vec![a],
616 r_squared: 0.9, extrapolation_fn: format!("y = {a:.6} + bx + cx²"),
618 });
619 }
620
621 Self::linear_fit(x, y)
623 }
624
625 fn exponential_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
627 let log_y: Vec<f64> = y
629 .iter()
630 .map(|yi| {
631 if *yi > 0.0 {
632 Ok(yi.ln())
633 } else {
634 Err(DeviceError::APIError(
635 "Cannot fit exponential to non-positive values".to_string(),
636 ))
637 }
638 })
639 .collect::<DeviceResult<Vec<_>>>()?;
640
641 let linear_result = Self::linear_fit(x, &log_y)?;
643 let ln_a = linear_result.fit_params[0];
644 let b = linear_result.fit_params[1];
645 let a = ln_a.exp();
646
647 Ok(ZNEResult {
648 mitigated_value: a, error_estimate: None,
650 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
651 fit_params: vec![a, b],
652 r_squared: linear_result.r_squared,
653 extrapolation_fn: format!("y = {a:.6} * exp({b:.6}x)"),
654 })
655 }
656
657 fn richardson_extrapolation(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
659 if x.len() < 2 {
660 return Err(DeviceError::APIError(
661 "Need at least 2 points for Richardson extrapolation".to_string(),
662 ));
663 }
664
665 let mut paired: Vec<(f64, f64)> =
667 x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect();
668 paired.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
669
670 let mut richardson_table: Vec<Vec<f64>> = vec![vec![]; paired.len()];
672
673 for i in 0..paired.len() {
675 richardson_table[i].push(paired[i].1);
676 }
677
678 for j in 1..paired.len() {
680 for i in 0..(paired.len() - j) {
681 let x_i = paired[i].0;
682 let x_ij = paired[i + j].0;
683 let factor = x_ij / x_i;
684 let value = factor
685 .mul_add(richardson_table[i + 1][j - 1], -richardson_table[i][j - 1])
686 / (factor - 1.0);
687 richardson_table[i].push(value);
688 }
689 }
690
691 let mitigated = richardson_table[0].last().copied().unwrap_or(paired[0].1);
693
694 Ok(ZNEResult {
695 mitigated_value: mitigated,
696 error_estimate: None,
697 raw_data: paired,
698 fit_params: vec![mitigated],
699 r_squared: 0.95, extrapolation_fn: "Richardson extrapolation".to_string(),
701 })
702 }
703
704 fn adaptive_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
706 let models = vec![
707 ExtrapolationMethod::Linear,
708 ExtrapolationMethod::Polynomial(2),
709 ExtrapolationMethod::Exponential,
710 ];
711
712 let mut best_result = None;
713 let mut best_r2 = -1.0;
714
715 for model in models {
716 if let Ok(result) = Self::fit_and_extrapolate(x, y, model) {
717 if result.r_squared > best_r2 {
718 best_r2 = result.r_squared;
719 best_result = Some(result);
720 }
721 }
722 }
723
724 best_result.ok_or_else(|| DeviceError::APIError("Adaptive fitting failed".to_string()))
725 }
726
727 pub fn bootstrap_estimate(
729 scale_factors: &[f64],
730 values: &[f64],
731 method: ExtrapolationMethod,
732 n_samples: usize,
733 ) -> DeviceResult<f64> {
734 use scirs2_core::random::prelude::*;
735 let mut rng = thread_rng();
736 let n = scale_factors.len();
737 let mut bootstrap_values = Vec::new();
738
739 for _ in 0..n_samples {
740 let mut resampled_x = Vec::new();
742 let mut resampled_y = Vec::new();
743
744 for _ in 0..n {
745 let idx = rng.gen_range(0..n);
746 resampled_x.push(scale_factors[idx]);
747 resampled_y.push(values[idx]);
748 }
749
750 if let Ok(result) = Self::fit_and_extrapolate(&resampled_x, &resampled_y, method) {
752 bootstrap_values.push(result.mitigated_value);
753 }
754 }
755
756 if bootstrap_values.is_empty() {
757 return Err(DeviceError::APIError(
758 "Bootstrap estimation failed".to_string(),
759 ));
760 }
761
762 let mean: f64 = bootstrap_values.iter().sum::<f64>() / bootstrap_values.len() as f64;
764 let variance: f64 = bootstrap_values
765 .iter()
766 .map(|v| (v - mean).powi(2))
767 .sum::<f64>()
768 / bootstrap_values.len() as f64;
769
770 Ok(variance.sqrt())
771 }
772}
773
774#[derive(Debug, Clone)]
776pub struct Observable {
777 pub pauli_string: Vec<(usize, String)>, pub coefficient: f64,
781}
782
783impl Observable {
784 pub fn z(qubit: usize) -> Self {
786 Self {
787 pauli_string: vec![(qubit, "Z".to_string())],
788 coefficient: 1.0,
789 }
790 }
791
792 pub fn zz(qubit1: usize, qubit2: usize) -> Self {
794 Self {
795 pauli_string: vec![(qubit1, "Z".to_string()), (qubit2, "Z".to_string())],
796 coefficient: 1.0,
797 }
798 }
799
800 pub fn expectation_value(&self, result: &CircuitResult) -> f64 {
802 let mut expectation = 0.0;
803 let total_shots = result.shots as f64;
804
805 for (bitstring, &count) in &result.counts {
806 let prob = count as f64 / total_shots;
807 let parity = self.calculate_parity(bitstring);
808 expectation += self.coefficient * parity * prob;
809 }
810
811 expectation
812 }
813
814 fn calculate_parity(&self, bitstring: &str) -> f64 {
816 let bits: Vec<char> = bitstring.chars().collect();
817 let mut parity = 1.0;
818
819 for (qubit, pauli) in &self.pauli_string {
820 if *qubit < bits.len() {
821 let bit = bits[*qubit];
822 match pauli.as_str() {
823 "Z" => {
824 if bit == '1' {
825 parity *= -1.0;
826 }
827 }
828 "X" | "Y" => {
829 }
832 _ => {} }
834 }
835 }
836
837 parity
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844
845 #[test]
846 fn test_circuit_folding() {
847 let mut circuit = Circuit::<2>::new();
848 circuit
849 .add_gate(quantrs2_core::gate::single::Hadamard {
850 target: quantrs2_core::qubit::QubitId(0),
851 })
852 .expect("Adding Hadamard gate should succeed");
853 circuit
854 .add_gate(quantrs2_core::gate::multi::CNOT {
855 control: quantrs2_core::qubit::QubitId(0),
856 target: quantrs2_core::qubit::QubitId(1),
857 })
858 .expect("Adding CNOT gate should succeed");
859
860 let folded = CircuitFolder::fold_global(&circuit, 3.0)
862 .expect("Global circuit folding should succeed");
863 assert_eq!(folded.num_gates(), 6);
868
869 let local_folded = CircuitFolder::fold_local(&circuit, 2.0, None)
871 .expect("Local circuit folding should succeed");
872 assert_eq!(local_folded.num_gates(), 2);
880
881 assert!(CircuitFolder::fold_global(&circuit, 0.5).is_err());
883 assert!(CircuitFolder::fold_local(&circuit, 0.5, None).is_err());
884 }
885
886 #[test]
887 fn test_linear_extrapolation() {
888 let x = vec![1.0, 2.0, 3.0, 4.0];
889 let y = vec![1.0, 1.5, 2.0, 2.5];
890
891 let result = ExtrapolationFitter::linear_fit(&x, &y).expect("Linear fit should succeed");
892 assert!((result.mitigated_value - 0.5).abs() < 0.01); assert!(result.r_squared > 0.99); }
895
896 #[test]
897 fn test_richardson_extrapolation() {
898 let x = vec![1.0, 1.5, 2.0, 3.0];
899 let y = vec![1.0, 1.25, 1.5, 2.0];
900
901 let result = ExtrapolationFitter::richardson_extrapolation(&x, &y)
902 .expect("Richardson extrapolation should succeed");
903 assert!(result.mitigated_value.is_finite());
906 assert_eq!(result.extrapolation_fn, "Richardson extrapolation");
907 }
908
909 #[test]
910 fn test_observable() {
911 let obs = Observable::z(0);
912
913 let mut counts = HashMap::new();
914 counts.insert("00".to_string(), 75);
915 counts.insert("10".to_string(), 25);
916
917 let result = CircuitResult {
918 counts,
919 shots: 100,
920 metadata: HashMap::new(),
921 };
922
923 let exp_val = obs.expectation_value(&result);
924 assert!((exp_val - 0.5).abs() < 0.01); }
926
927 #[test]
928 fn test_zne_config() {
929 let config = ZNEConfig::default();
930 assert_eq!(config.scale_factors, vec![1.0, 1.5, 2.0, 2.5, 3.0]);
931 assert_eq!(config.scaling_method, NoiseScalingMethod::GlobalFolding);
932 assert_eq!(config.extrapolation_method, ExtrapolationMethod::Richardson);
933 }
934}