use crate::{CircuitResult, DeviceError, DeviceResult};
use quantrs2_circuit::prelude::*;
use quantrs2_core::prelude::GateOp;
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::thread_rng;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoiseScalingMethod {
GlobalFolding,
LocalFolding,
PulseStretching,
DigitalRepetition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtrapolationMethod {
Linear,
Polynomial(usize),
Exponential,
Richardson,
Adaptive,
}
#[derive(Debug, Clone)]
pub struct ZNEConfig {
pub scale_factors: Vec<f64>,
pub scaling_method: NoiseScalingMethod,
pub extrapolation_method: ExtrapolationMethod,
pub bootstrap_samples: Option<usize>,
pub confidence_level: f64,
}
impl Default for ZNEConfig {
fn default() -> Self {
Self {
scale_factors: vec![1.0, 1.5, 2.0, 2.5, 3.0],
scaling_method: NoiseScalingMethod::GlobalFolding,
extrapolation_method: ExtrapolationMethod::Richardson,
bootstrap_samples: Some(100),
confidence_level: 0.95,
}
}
}
#[derive(Debug, Clone)]
pub struct ZNEResult {
pub mitigated_value: f64,
pub error_estimate: Option<f64>,
pub raw_data: Vec<(f64, f64)>, pub fit_params: Vec<f64>,
pub r_squared: f64,
pub extrapolation_fn: String,
}
pub struct ZNEExecutor<E> {
executor: E,
config: ZNEConfig,
}
impl<E> ZNEExecutor<E> {
pub const fn new(executor: E, config: ZNEConfig) -> Self {
Self { executor, config }
}
pub fn with_defaults(executor: E) -> Self {
Self::new(executor, ZNEConfig::default())
}
}
pub trait ZNECapable {
fn execute_scaled<const N: usize>(
&self,
circuit: &Circuit<N>,
scale_factor: f64,
shots: usize,
) -> DeviceResult<CircuitResult>;
fn supports_scaling_method(&self, method: NoiseScalingMethod) -> bool;
}
pub struct CircuitFolder;
impl CircuitFolder {
pub fn fold_global<const N: usize>(
circuit: &Circuit<N>,
scale_factor: f64,
) -> DeviceResult<Circuit<N>> {
if scale_factor < 1.0 {
return Err(DeviceError::APIError(
"Scale factor must be >= 1.0".to_string(),
));
}
if (scale_factor - 1.0).abs() < f64::EPSILON {
return Ok(circuit.clone());
}
let num_folds = ((scale_factor - 1.0) / 2.0).floor() as usize;
let partial_fold = (scale_factor - 1.0) % 2.0;
let mut folded_circuit = circuit.clone();
for _ in 0..num_folds {
folded_circuit = Self::apply_full_fold(&folded_circuit)?;
}
if partial_fold > f64::EPSILON {
folded_circuit = Self::apply_partial_fold(&folded_circuit, partial_fold)?;
}
Ok(folded_circuit)
}
pub fn fold_local<const N: usize>(
circuit: &Circuit<N>,
scale_factor: f64,
gate_weights: Option<Vec<f64>>,
) -> DeviceResult<Circuit<N>> {
if scale_factor < 1.0 {
return Err(DeviceError::APIError(
"Scale factor must be >= 1.0".to_string(),
));
}
let num_gates = circuit.num_gates();
let weights = gate_weights.unwrap_or_else(|| vec![1.0; num_gates]);
if weights.len() != num_gates {
return Err(DeviceError::APIError(
"Gate weights length mismatch".to_string(),
));
}
let total_weight: f64 = weights.iter().sum();
let normalized_weights: Vec<f64> = weights.iter().map(|w| w / total_weight).collect();
let extra_noise = scale_factor - 1.0;
let fold_amounts: Vec<f64> = normalized_weights
.iter()
.map(|w| 1.0 + extra_noise * w)
.collect();
let gates = circuit.gates();
let mut folded_circuit = Circuit::<N>::new();
for (idx, gate) in gates.iter().enumerate() {
let fold_factor = fold_amounts[idx];
if (fold_factor - 1.0).abs() < f64::EPSILON {
folded_circuit
.add_gate_arc(gate.clone())
.map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
} else {
let num_folds = ((fold_factor - 1.0) / 2.0).floor() as usize;
let partial = (fold_factor - 1.0) % 2.0;
folded_circuit
.add_gate_arc(gate.clone())
.map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
let inverse = Self::create_inverse_gate(gate.as_ref())?;
for _ in 0..num_folds {
folded_circuit.add_gate_arc(inverse.clone()).map_err(|e| {
DeviceError::APIError(format!("Failed to add inverse gate: {e:?}"))
})?;
folded_circuit
.add_gate_arc(gate.clone())
.map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
}
if partial > 0.5 {
folded_circuit.add_gate_arc(inverse).map_err(|e| {
DeviceError::APIError(format!("Failed to add inverse gate: {e:?}"))
})?;
folded_circuit
.add_gate_arc(gate.clone())
.map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
}
}
}
Ok(folded_circuit)
}
fn apply_full_fold<const N: usize>(circuit: &Circuit<N>) -> DeviceResult<Circuit<N>> {
let gates = circuit.gates();
let mut folded_circuit = Circuit::<N>::with_capacity(gates.len() * 3);
for gate in gates {
folded_circuit
.add_gate_arc(gate.clone())
.map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
let inverse = Self::create_inverse_gate(gate.as_ref())?;
folded_circuit
.add_gate_arc(inverse)
.map_err(|e| DeviceError::APIError(format!("Failed to add inverse gate: {e:?}")))?;
folded_circuit
.add_gate_arc(gate.clone())
.map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
}
Ok(folded_circuit)
}
fn apply_partial_fold<const N: usize>(
circuit: &Circuit<N>,
fraction: f64,
) -> DeviceResult<Circuit<N>> {
let gates = circuit.gates();
let num_gates_to_fold = (gates.len() as f64 * fraction / 2.0).ceil() as usize;
let mut folded_circuit = Circuit::<N>::with_capacity(gates.len() + num_gates_to_fold * 2);
for (idx, gate) in gates.iter().enumerate() {
if idx < num_gates_to_fold {
folded_circuit
.add_gate_arc(gate.clone())
.map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
let inverse = Self::create_inverse_gate(gate.as_ref())?;
folded_circuit.add_gate_arc(inverse).map_err(|e| {
DeviceError::APIError(format!("Failed to add inverse gate: {e:?}"))
})?;
folded_circuit
.add_gate_arc(gate.clone())
.map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
} else {
folded_circuit
.add_gate_arc(gate.clone())
.map_err(|e| DeviceError::APIError(format!("Failed to add gate: {e:?}")))?;
}
}
Ok(folded_circuit)
}
fn create_inverse_gate(
gate: &dyn GateOp,
) -> DeviceResult<std::sync::Arc<dyn GateOp + Send + Sync>> {
use quantrs2_core::gate::{multi::*, single::*};
use std::sync::Arc;
match gate.name() {
"X" => {
let target = gate.qubits()[0];
Ok(Arc::new(PauliX { target }))
}
"Y" => {
let target = gate.qubits()[0];
Ok(Arc::new(PauliY { target }))
}
"Z" => {
let target = gate.qubits()[0];
Ok(Arc::new(PauliZ { target }))
}
"H" => {
let target = gate.qubits()[0];
Ok(Arc::new(Hadamard { target }))
}
"CNOT" => {
let qubits = gate.qubits();
Ok(Arc::new(CNOT {
control: qubits[0],
target: qubits[1],
}))
}
"CZ" => {
let qubits = gate.qubits();
Ok(Arc::new(CZ {
control: qubits[0],
target: qubits[1],
}))
}
"CY" => {
let qubits = gate.qubits();
Ok(Arc::new(CY {
control: qubits[0],
target: qubits[1],
}))
}
"SWAP" => {
let qubits = gate.qubits();
Ok(Arc::new(SWAP {
qubit1: qubits[0],
qubit2: qubits[1],
}))
}
"Fredkin" => {
let qubits = gate.qubits();
Ok(Arc::new(Fredkin {
control: qubits[0],
target1: qubits[1],
target2: qubits[2],
}))
}
"Toffoli" => {
let qubits = gate.qubits();
Ok(Arc::new(Toffoli {
control1: qubits[0],
control2: qubits[1],
target: qubits[2],
}))
}
"S" => {
let target = gate.qubits()[0];
Ok(Arc::new(PhaseDagger { target }))
}
"Sdg" => {
let target = gate.qubits()[0];
Ok(Arc::new(Phase { target }))
}
"T" => {
let target = gate.qubits()[0];
Ok(Arc::new(TDagger { target }))
}
"Tdg" => {
let target = gate.qubits()[0];
Ok(Arc::new(T { target }))
}
"SqrtX" => {
let target = gate.qubits()[0];
Ok(Arc::new(SqrtXDagger { target }))
}
"SqrtXDagger" => {
let target = gate.qubits()[0];
Ok(Arc::new(SqrtX { target }))
}
"RX" => {
if let Some(rx) = gate.as_any().downcast_ref::<RotationX>() {
Ok(Arc::new(RotationX {
target: rx.target,
theta: -rx.theta,
}))
} else {
Err(DeviceError::APIError(
"Failed to downcast RX gate".to_string(),
))
}
}
"RY" => {
if let Some(ry) = gate.as_any().downcast_ref::<RotationY>() {
Ok(Arc::new(RotationY {
target: ry.target,
theta: -ry.theta,
}))
} else {
Err(DeviceError::APIError(
"Failed to downcast RY gate".to_string(),
))
}
}
"RZ" => {
if let Some(rz) = gate.as_any().downcast_ref::<RotationZ>() {
Ok(Arc::new(RotationZ {
target: rz.target,
theta: -rz.theta,
}))
} else {
Err(DeviceError::APIError(
"Failed to downcast RZ gate".to_string(),
))
}
}
"CRX" => {
if let Some(crx) = gate.as_any().downcast_ref::<CRX>() {
Ok(Arc::new(CRX {
control: crx.control,
target: crx.target,
theta: -crx.theta,
}))
} else {
Err(DeviceError::APIError(
"Failed to downcast CRX gate".to_string(),
))
}
}
"CRY" => {
if let Some(cry) = gate.as_any().downcast_ref::<CRY>() {
Ok(Arc::new(CRY {
control: cry.control,
target: cry.target,
theta: -cry.theta,
}))
} else {
Err(DeviceError::APIError(
"Failed to downcast CRY gate".to_string(),
))
}
}
"CRZ" => {
if let Some(crz) = gate.as_any().downcast_ref::<CRZ>() {
Ok(Arc::new(CRZ {
control: crz.control,
target: crz.target,
theta: -crz.theta,
}))
} else {
Err(DeviceError::APIError(
"Failed to downcast CRZ gate".to_string(),
))
}
}
"CH" => {
let qubits = gate.qubits();
Ok(Arc::new(CH {
control: qubits[0],
target: qubits[1],
}))
}
"CS" => {
let qubits = gate.qubits();
Ok(Arc::new(CS {
control: qubits[0],
target: qubits[1],
}))
}
_ => Err(DeviceError::APIError(format!(
"Cannot create inverse for unsupported gate: {}",
gate.name()
))),
}
}
}
pub struct ExtrapolationFitter;
impl ExtrapolationFitter {
pub fn fit_and_extrapolate(
scale_factors: &[f64],
values: &[f64],
method: ExtrapolationMethod,
) -> DeviceResult<ZNEResult> {
if scale_factors.len() != values.len() || scale_factors.is_empty() {
return Err(DeviceError::APIError(
"Invalid data for extrapolation".to_string(),
));
}
match method {
ExtrapolationMethod::Linear => Self::linear_fit(scale_factors, values),
ExtrapolationMethod::Polynomial(order) => {
Self::polynomial_fit(scale_factors, values, order)
}
ExtrapolationMethod::Exponential => Self::exponential_fit(scale_factors, values),
ExtrapolationMethod::Richardson => {
Self::richardson_extrapolation(scale_factors, values)
}
ExtrapolationMethod::Adaptive => Self::adaptive_fit(scale_factors, values),
}
}
fn linear_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
let n = x.len() as f64;
let sum_x: f64 = x.iter().sum();
let sum_y: f64 = y.iter().sum();
let sum_xx: f64 = x.iter().map(|xi| xi * xi).sum();
let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
let slope = n.mul_add(sum_xy, -(sum_x * sum_y)) / n.mul_add(sum_xx, -(sum_x * sum_x));
let intercept = slope.mul_add(-sum_x, sum_y) / n;
let y_mean = sum_y / n;
let ss_tot: f64 = y.iter().map(|yi| (yi - y_mean).powi(2)).sum();
let ss_res: f64 = x
.iter()
.zip(y.iter())
.map(|(xi, yi)| (yi - (slope * xi + intercept)).powi(2))
.sum();
let r_squared = 1.0 - ss_res / ss_tot;
Ok(ZNEResult {
mitigated_value: intercept, error_estimate: None,
raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
fit_params: vec![intercept, slope],
r_squared,
extrapolation_fn: format!("y = {intercept:.6} + {slope:.6}x"),
})
}
fn polynomial_fit(x: &[f64], y: &[f64], order: usize) -> DeviceResult<ZNEResult> {
let n = x.len();
if order >= n {
return Err(DeviceError::APIError(
"Polynomial order too high for data".to_string(),
));
}
let mut a = Array2::<f64>::zeros((n, order + 1));
for i in 0..n {
for j in 0..=order {
a[[i, j]] = x[i].powi(j as i32);
}
}
let y_vec = Array1::from_vec(y.to_vec());
if order == 2 {
let sum_x: f64 = x.iter().sum();
let sum_x2: f64 = x.iter().map(|xi| xi * xi).sum();
let sum_x3: f64 = x.iter().map(|xi| xi * xi * xi).sum();
let sum_x4: f64 = x.iter().map(|xi| xi * xi * xi * xi).sum();
let sum_y: f64 = y.iter().sum();
let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
let sum_x2y: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * xi * yi).sum();
let det = sum_x2.mul_add(
sum_x.mul_add(sum_x3, -(sum_x2 * sum_x2)),
(n as f64).mul_add(
sum_x2.mul_add(sum_x4, -(sum_x3 * sum_x3)),
-(sum_x * sum_x.mul_add(sum_x4, -(sum_x2 * sum_x3))),
),
);
let a = sum_x2y.mul_add(
sum_x.mul_add(sum_x3, -(sum_x2 * sum_x2)),
sum_y.mul_add(
sum_x2.mul_add(sum_x4, -(sum_x3 * sum_x3)),
-(sum_xy * sum_x.mul_add(sum_x4, -(sum_x2 * sum_x3))),
),
) / det;
return Ok(ZNEResult {
mitigated_value: a,
error_estimate: None,
raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
fit_params: vec![a],
r_squared: 0.9, extrapolation_fn: format!("y = {a:.6} + bx + cx²"),
});
}
Self::linear_fit(x, y)
}
fn exponential_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
let log_y: Vec<f64> = y
.iter()
.map(|yi| {
if *yi > 0.0 {
Ok(yi.ln())
} else {
Err(DeviceError::APIError(
"Cannot fit exponential to non-positive values".to_string(),
))
}
})
.collect::<DeviceResult<Vec<_>>>()?;
let linear_result = Self::linear_fit(x, &log_y)?;
let ln_a = linear_result.fit_params[0];
let b = linear_result.fit_params[1];
let a = ln_a.exp();
Ok(ZNEResult {
mitigated_value: a, error_estimate: None,
raw_data: x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect(),
fit_params: vec![a, b],
r_squared: linear_result.r_squared,
extrapolation_fn: format!("y = {a:.6} * exp({b:.6}x)"),
})
}
fn richardson_extrapolation(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
if x.len() < 2 {
return Err(DeviceError::APIError(
"Need at least 2 points for Richardson extrapolation".to_string(),
));
}
let mut paired: Vec<(f64, f64)> =
x.iter().zip(y.iter()).map(|(&xi, &yi)| (xi, yi)).collect();
paired.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut richardson_table: Vec<Vec<f64>> = vec![vec![]; paired.len()];
for i in 0..paired.len() {
richardson_table[i].push(paired[i].1);
}
for j in 1..paired.len() {
for i in 0..(paired.len() - j) {
let x_i = paired[i].0;
let x_ij = paired[i + j].0;
let factor = x_ij / x_i;
let value = factor
.mul_add(richardson_table[i + 1][j - 1], -richardson_table[i][j - 1])
/ (factor - 1.0);
richardson_table[i].push(value);
}
}
let mitigated = richardson_table[0].last().copied().unwrap_or(paired[0].1);
Ok(ZNEResult {
mitigated_value: mitigated,
error_estimate: None,
raw_data: paired,
fit_params: vec![mitigated],
r_squared: 0.95, extrapolation_fn: "Richardson extrapolation".to_string(),
})
}
fn adaptive_fit(x: &[f64], y: &[f64]) -> DeviceResult<ZNEResult> {
let models = vec![
ExtrapolationMethod::Linear,
ExtrapolationMethod::Polynomial(2),
ExtrapolationMethod::Exponential,
];
let mut best_result = None;
let mut best_r2 = -1.0;
for model in models {
if let Ok(result) = Self::fit_and_extrapolate(x, y, model) {
if result.r_squared > best_r2 {
best_r2 = result.r_squared;
best_result = Some(result);
}
}
}
best_result.ok_or_else(|| DeviceError::APIError("Adaptive fitting failed".to_string()))
}
pub fn bootstrap_estimate(
scale_factors: &[f64],
values: &[f64],
method: ExtrapolationMethod,
n_samples: usize,
) -> DeviceResult<f64> {
use scirs2_core::random::prelude::*;
let mut rng = thread_rng();
let n = scale_factors.len();
let mut bootstrap_values = Vec::new();
for _ in 0..n_samples {
let mut resampled_x = Vec::new();
let mut resampled_y = Vec::new();
for _ in 0..n {
let idx = rng.random_range(0..n);
resampled_x.push(scale_factors[idx]);
resampled_y.push(values[idx]);
}
if let Ok(result) = Self::fit_and_extrapolate(&resampled_x, &resampled_y, method) {
bootstrap_values.push(result.mitigated_value);
}
}
if bootstrap_values.is_empty() {
return Err(DeviceError::APIError(
"Bootstrap estimation failed".to_string(),
));
}
let mean: f64 = bootstrap_values.iter().sum::<f64>() / bootstrap_values.len() as f64;
let variance: f64 = bootstrap_values
.iter()
.map(|v| (v - mean).powi(2))
.sum::<f64>()
/ bootstrap_values.len() as f64;
Ok(variance.sqrt())
}
}
#[derive(Debug, Clone)]
pub struct Observable {
pub pauli_string: Vec<(usize, String)>, pub coefficient: f64,
}
impl Observable {
pub fn z(qubit: usize) -> Self {
Self {
pauli_string: vec![(qubit, "Z".to_string())],
coefficient: 1.0,
}
}
pub fn zz(qubit1: usize, qubit2: usize) -> Self {
Self {
pauli_string: vec![(qubit1, "Z".to_string()), (qubit2, "Z".to_string())],
coefficient: 1.0,
}
}
pub fn expectation_value(&self, result: &CircuitResult) -> f64 {
let mut expectation = 0.0;
let total_shots = result.shots as f64;
for (bitstring, &count) in &result.counts {
let prob = count as f64 / total_shots;
let parity = self.calculate_parity(bitstring);
expectation += self.coefficient * parity * prob;
}
expectation
}
fn calculate_parity(&self, bitstring: &str) -> f64 {
let bits: Vec<char> = bitstring.chars().collect();
let mut parity = 1.0;
for (qubit, pauli) in &self.pauli_string {
if *qubit < bits.len() {
let bit = bits[*qubit];
match pauli.as_str() {
"Z" => {
if bit == '1' {
parity *= -1.0;
}
}
"X" | "Y" => {
}
_ => {} }
}
}
parity
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circuit_folding() {
let mut circuit = Circuit::<2>::new();
circuit
.add_gate(quantrs2_core::gate::single::Hadamard {
target: quantrs2_core::qubit::QubitId(0),
})
.expect("Adding Hadamard gate should succeed");
circuit
.add_gate(quantrs2_core::gate::multi::CNOT {
control: quantrs2_core::qubit::QubitId(0),
target: quantrs2_core::qubit::QubitId(1),
})
.expect("Adding CNOT gate should succeed");
let folded = CircuitFolder::fold_global(&circuit, 3.0)
.expect("Global circuit folding should succeed");
assert_eq!(folded.num_gates(), 6);
let local_folded = CircuitFolder::fold_local(&circuit, 2.0, None)
.expect("Local circuit folding should succeed");
assert_eq!(local_folded.num_gates(), 2);
assert!(CircuitFolder::fold_global(&circuit, 0.5).is_err());
assert!(CircuitFolder::fold_local(&circuit, 0.5, None).is_err());
}
#[test]
fn test_linear_extrapolation() {
let x = vec![1.0, 2.0, 3.0, 4.0];
let y = vec![1.0, 1.5, 2.0, 2.5];
let result = ExtrapolationFitter::linear_fit(&x, &y).expect("Linear fit should succeed");
assert!((result.mitigated_value - 0.5).abs() < 0.01); assert!(result.r_squared > 0.99); }
#[test]
fn test_richardson_extrapolation() {
let x = vec![1.0, 1.5, 2.0, 3.0];
let y = vec![1.0, 1.25, 1.5, 2.0];
let result = ExtrapolationFitter::richardson_extrapolation(&x, &y)
.expect("Richardson extrapolation should succeed");
assert!(result.mitigated_value.is_finite());
assert_eq!(result.extrapolation_fn, "Richardson extrapolation");
}
#[test]
fn test_observable() {
let obs = Observable::z(0);
let mut counts = HashMap::new();
counts.insert("00".to_string(), 75);
counts.insert("10".to_string(), 25);
let result = CircuitResult {
counts,
shots: 100,
metadata: HashMap::new(),
};
let exp_val = obs.expectation_value(&result);
assert!((exp_val - 0.5).abs() < 0.01); }
#[test]
fn test_zne_config() {
let config = ZNEConfig::default();
assert_eq!(config.scale_factors, vec![1.0, 1.5, 2.0, 2.5, 3.0]);
assert_eq!(config.scaling_method, NoiseScalingMethod::GlobalFolding);
assert_eq!(config.extrapolation_method, ExtrapolationMethod::Richardson);
}
}