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 Ok(circuit.clone())
192 }
193
194 fn apply_full_fold<const N: usize>(circuit: &Circuit<N>) -> DeviceResult<Circuit<N>> {
196 Ok(circuit.clone())
198 }
199
200 fn apply_partial_fold<const N: usize>(
202 circuit: &Circuit<N>,
203 fraction: f64,
204 ) -> DeviceResult<Circuit<N>> {
205 Ok(circuit.clone())
209 }
210
211 fn invert_gate(gate: &Box<dyn GateOp>) -> DeviceResult<Box<dyn GateOp>> {
213 match gate.name() {
216 "X" | "Y" | "Z" | "H" | "CNOT" | "CZ" | "SWAP" => Ok(gate.clone()), "S" => Ok(gate.clone()), "T" => Ok(gate.clone()), "RX" | "RY" | "RZ" => Ok(gate.clone()), _ => Err(DeviceError::APIError(format!(
221 "Cannot invert gate: {}",
222 gate.name()
223 ))),
224 }
225 }
226}
227
228pub struct ExtrapolationFitter;
230
231impl ExtrapolationFitter {
232 pub fn fit_and_extrapolate(
234 scale_factors: &[f64],
235 values: &[f64],
236 method: ExtrapolationMethod,
237 ) -> DeviceResult<ZNEResult> {
238 if scale_factors.len() != values.len() || scale_factors.is_empty() {
239 return Err(DeviceError::APIError(
240 "Invalid data for extrapolation".to_string(),
241 ));
242 }
243
244 match method {
245 ExtrapolationMethod::Linear => Self::linear_fit(scale_factors, values),
246 ExtrapolationMethod::Polynomial(order) => {
247 Self::polynomial_fit(scale_factors, values, order)
248 }
249 ExtrapolationMethod::Exponential => Self::exponential_fit(scale_factors, values),
250 ExtrapolationMethod::Richardson => {
251 Self::richardson_extrapolation(scale_factors, values)
252 }
253 ExtrapolationMethod::Adaptive => Self::adaptive_fit(scale_factors, values),
254 }
255 }
256
257 fn linear_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
259 let n = x.len() as f64;
260 let sum_x: f64 = x.iter().sum();
261 let sum_y: f64 = y.iter().sum();
262 let sum_xx: f64 = x.iter().map(|xi| xi * xi).sum();
263 let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
264
265 let slope = n.mul_add(sum_xy, -(sum_x * sum_y)) / n.mul_add(sum_xx, -(sum_x * sum_x));
266 let intercept = slope.mul_add(-sum_x, sum_y) / n;
267
268 let y_mean = sum_y / n;
270 let ss_tot: f64 = y.iter().map(|yi| (yi - y_mean).powi(2)).sum();
271 let ss_res: f64 = x
272 .iter()
273 .zip(y.iter())
274 .map(|(xi, yi)| (yi - (slope * xi + intercept)).powi(2))
275 .sum();
276 let r_squared = 1.0 - ss_res / ss_tot;
277
278 Ok(ZNEResult {
279 mitigated_value: intercept, error_estimate: None,
281 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
282 fit_params: vec![intercept, slope],
283 r_squared,
284 extrapolation_fn: format!("y = {intercept:.6} + {slope:.6}x"),
285 })
286 }
287
288 fn polynomial_fit(x: &[f64], y: &[f64], order: usize) -> DeviceResult<ZNEResult> {
290 let n = x.len();
291 if order >= n {
292 return Err(DeviceError::APIError(
293 "Polynomial order too high for data".to_string(),
294 ));
295 }
296
297 let mut a = Array2::<f64>::zeros((n, order + 1));
299 for i in 0..n {
300 for j in 0..=order {
301 a[[i, j]] = x[i].powi(j as i32);
302 }
303 }
304
305 let y_vec = Array1::from_vec(y.to_vec());
307
308 if order == 2 {
310 let sum_x: f64 = x.iter().sum();
312 let sum_x2: f64 = x.iter().map(|xi| xi * xi).sum();
313 let sum_x3: f64 = x.iter().map(|xi| xi * xi * xi).sum();
314 let sum_x4: f64 = x.iter().map(|xi| xi * xi * xi * xi).sum();
315 let sum_y: f64 = y.iter().sum();
316 let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
317 let sum_x2y: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * xi * yi).sum();
318
319 let det = sum_x2.mul_add(
321 sum_x.mul_add(sum_x3, -(sum_x2 * sum_x2)),
322 (n as f64).mul_add(
323 sum_x2.mul_add(sum_x4, -(sum_x3 * sum_x3)),
324 -(sum_x * sum_x.mul_add(sum_x4, -(sum_x2 * sum_x3))),
325 ),
326 );
327
328 let a = sum_x2y.mul_add(
329 sum_x.mul_add(sum_x3, -(sum_x2 * sum_x2)),
330 sum_y.mul_add(
331 sum_x2.mul_add(sum_x4, -(sum_x3 * sum_x3)),
332 -(sum_xy * sum_x.mul_add(sum_x4, -(sum_x2 * sum_x3))),
333 ),
334 ) / det;
335
336 return Ok(ZNEResult {
337 mitigated_value: a,
338 error_estimate: None,
339 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
340 fit_params: vec![a],
341 r_squared: 0.9, extrapolation_fn: format!("y = {a:.6} + bx + cx²"),
343 });
344 }
345
346 Self::linear_fit(x, y)
348 }
349
350 fn exponential_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
352 let log_y: Vec<f64> = y
354 .iter()
355 .map(|yi| {
356 if *yi > 0.0 {
357 Ok(yi.ln())
358 } else {
359 Err(DeviceError::APIError(
360 "Cannot fit exponential to non-positive values".to_string(),
361 ))
362 }
363 })
364 .collect::<DeviceResult<Vec<_>>>()?;
365
366 let linear_result = Self::linear_fit(x, &log_y)?;
368 let ln_a = linear_result.fit_params[0];
369 let b = linear_result.fit_params[1];
370 let a = ln_a.exp();
371
372 Ok(ZNEResult {
373 mitigated_value: a, error_estimate: None,
375 raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
376 fit_params: vec![a, b],
377 r_squared: linear_result.r_squared,
378 extrapolation_fn: format!("y = {a:.6} * exp({b:.6}x)"),
379 })
380 }
381
382 fn richardson_extrapolation(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
384 if x.len() < 2 {
385 return Err(DeviceError::APIError(
386 "Need at least 2 points for Richardson extrapolation".to_string(),
387 ));
388 }
389
390 let mut paired: Vec<(f64, f64)> =
392 x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect();
393 paired.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
394
395 let mut richardson_table: Vec<Vec<f64>> = vec![vec![]; paired.len()];
397
398 for i in 0..paired.len() {
400 richardson_table[i].push(paired[i].1);
401 }
402
403 for j in 1..paired.len() {
405 for i in 0..(paired.len() - j) {
406 let x_i = paired[i].0;
407 let x_ij = paired[i + j].0;
408 let factor = x_ij / x_i;
409 let value = factor
410 .mul_add(richardson_table[i + 1][j - 1], -richardson_table[i][j - 1])
411 / (factor - 1.0);
412 richardson_table[i].push(value);
413 }
414 }
415
416 let mitigated = richardson_table[0].last().copied().unwrap_or(paired[0].1);
418
419 Ok(ZNEResult {
420 mitigated_value: mitigated,
421 error_estimate: None,
422 raw_data: paired,
423 fit_params: vec![mitigated],
424 r_squared: 0.95, extrapolation_fn: "Richardson extrapolation".to_string(),
426 })
427 }
428
429 fn adaptive_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
431 let models = vec![
432 ExtrapolationMethod::Linear,
433 ExtrapolationMethod::Polynomial(2),
434 ExtrapolationMethod::Exponential,
435 ];
436
437 let mut best_result = None;
438 let mut best_r2 = -1.0;
439
440 for model in models {
441 if let Ok(result) = Self::fit_and_extrapolate(x, y, model) {
442 if result.r_squared > best_r2 {
443 best_r2 = result.r_squared;
444 best_result = Some(result);
445 }
446 }
447 }
448
449 best_result.ok_or_else(|| DeviceError::APIError("Adaptive fitting failed".to_string()))
450 }
451
452 pub fn bootstrap_estimate(
454 scale_factors: &[f64],
455 values: &[f64],
456 method: ExtrapolationMethod,
457 n_samples: usize,
458 ) -> DeviceResult<f64> {
459 use scirs2_core::random::prelude::*;
460 let mut rng = thread_rng();
461 let n = scale_factors.len();
462 let mut bootstrap_values = Vec::new();
463
464 for _ in 0..n_samples {
465 let mut resampled_x = Vec::new();
467 let mut resampled_y = Vec::new();
468
469 for _ in 0..n {
470 let idx = rng.gen_range(0..n);
471 resampled_x.push(scale_factors[idx]);
472 resampled_y.push(values[idx]);
473 }
474
475 if let Ok(result) = Self::fit_and_extrapolate(&resampled_x, &resampled_y, method) {
477 bootstrap_values.push(result.mitigated_value);
478 }
479 }
480
481 if bootstrap_values.is_empty() {
482 return Err(DeviceError::APIError(
483 "Bootstrap estimation failed".to_string(),
484 ));
485 }
486
487 let mean: f64 = bootstrap_values.iter().sum::<f64>() / bootstrap_values.len() as f64;
489 let variance: f64 = bootstrap_values
490 .iter()
491 .map(|v| (v - mean).powi(2))
492 .sum::<f64>()
493 / bootstrap_values.len() as f64;
494
495 Ok(variance.sqrt())
496 }
497}
498
499#[derive(Debug, Clone)]
501pub struct Observable {
502 pub pauli_string: Vec<(usize, String)>, pub coefficient: f64,
506}
507
508impl Observable {
509 pub fn z(qubit: usize) -> Self {
511 Self {
512 pauli_string: vec![(qubit, "Z".to_string())],
513 coefficient: 1.0,
514 }
515 }
516
517 pub fn zz(qubit1: usize, qubit2: usize) -> Self {
519 Self {
520 pauli_string: vec![(qubit1, "Z".to_string()), (qubit2, "Z".to_string())],
521 coefficient: 1.0,
522 }
523 }
524
525 pub fn expectation_value(&self, result: &CircuitResult) -> f64 {
527 let mut expectation = 0.0;
528 let total_shots = result.shots as f64;
529
530 for (bitstring, &count) in &result.counts {
531 let prob = count as f64 / total_shots;
532 let parity = self.calculate_parity(bitstring);
533 expectation += self.coefficient * parity * prob;
534 }
535
536 expectation
537 }
538
539 fn calculate_parity(&self, bitstring: &str) -> f64 {
541 let bits: Vec<char> = bitstring.chars().collect();
542 let mut parity = 1.0;
543
544 for (qubit, pauli) in &self.pauli_string {
545 if *qubit < bits.len() {
546 let bit = bits[*qubit];
547 match pauli.as_str() {
548 "Z" => {
549 if bit == '1' {
550 parity *= -1.0;
551 }
552 }
553 "X" | "Y" => {
554 }
557 _ => {} }
559 }
560 }
561
562 parity
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[test]
571 fn test_circuit_folding() {
572 let mut circuit = Circuit::<2>::new();
573 circuit
574 .add_gate(quantrs2_core::gate::single::Hadamard {
575 target: quantrs2_core::qubit::QubitId(0),
576 })
577 .expect("Adding Hadamard gate should succeed");
578 circuit
579 .add_gate(quantrs2_core::gate::multi::CNOT {
580 control: quantrs2_core::qubit::QubitId(0),
581 target: quantrs2_core::qubit::QubitId(1),
582 })
583 .expect("Adding CNOT gate should succeed");
584
585 let folded = CircuitFolder::fold_global(&circuit, 3.0)
587 .expect("Global circuit folding should succeed");
588 assert_eq!(folded.num_gates(), 2); let local_folded = CircuitFolder::fold_local(&circuit, 2.0, None)
594 .expect("Local circuit folding should succeed");
595 assert_eq!(local_folded.num_gates(), 2); assert!(CircuitFolder::fold_global(&circuit, 0.5).is_err());
600 assert!(CircuitFolder::fold_local(&circuit, 0.5, None).is_err());
601 }
602
603 #[test]
604 fn test_linear_extrapolation() {
605 let x = vec![1.0, 2.0, 3.0, 4.0];
606 let y = vec![1.0, 1.5, 2.0, 2.5];
607
608 let result = ExtrapolationFitter::linear_fit(&x, &y).expect("Linear fit should succeed");
609 assert!((result.mitigated_value - 0.5).abs() < 0.01); assert!(result.r_squared > 0.99); }
612
613 #[test]
614 fn test_richardson_extrapolation() {
615 let x = vec![1.0, 1.5, 2.0, 3.0];
616 let y = vec![1.0, 1.25, 1.5, 2.0];
617
618 let result = ExtrapolationFitter::richardson_extrapolation(&x, &y)
619 .expect("Richardson extrapolation should succeed");
620 assert!(result.mitigated_value.is_finite());
623 assert_eq!(result.extrapolation_fn, "Richardson extrapolation");
624 }
625
626 #[test]
627 fn test_observable() {
628 let obs = Observable::z(0);
629
630 let mut counts = HashMap::new();
631 counts.insert("00".to_string(), 75);
632 counts.insert("10".to_string(), 25);
633
634 let result = CircuitResult {
635 counts,
636 shots: 100,
637 metadata: HashMap::new(),
638 };
639
640 let exp_val = obs.expectation_value(&result);
641 assert!((exp_val - 0.5).abs() < 0.01); }
643
644 #[test]
645 fn test_zne_config() {
646 let config = ZNEConfig::default();
647 assert_eq!(config.scale_factors, vec![1.0, 1.5, 2.0, 2.5, 3.0]);
648 assert_eq!(config.scaling_method, NoiseScalingMethod::GlobalFolding);
649 assert_eq!(config.extrapolation_method, ExtrapolationMethod::Richardson);
650 }
651}