use crate::circuit::QuantumCircuit;
use crate::gate::Gate;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ZneConfig {
pub noise_factors: Vec<f64>,
pub extrapolation: ExtrapolationMethod,
}
#[derive(Debug, Clone)]
pub enum ExtrapolationMethod {
Linear,
Polynomial(usize),
Richardson,
}
pub fn fold_circuit(circuit: &QuantumCircuit, factor: f64) -> QuantumCircuit {
assert!(factor >= 1.0, "noise factor must be >= 1.0");
let gates = circuit.gates();
let mut folded = QuantumCircuit::new(circuit.num_qubits());
let unitary_indices: Vec<usize> = gates
.iter()
.enumerate()
.filter(|(_, g)| !g.is_non_unitary())
.map(|(i, _)| i)
.collect();
let n_unitary = unitary_indices.len();
let target_unitary_slots = (n_unitary as f64 * factor).round() as usize;
let num_folds = if target_unitary_slots > n_unitary {
(target_unitary_slots - n_unitary) / 2
} else {
0
};
let full_rounds = num_folds / n_unitary.max(1);
let extra_folds = num_folds % n_unitary.max(1);
let mut unitary_counter: usize = 0;
for gate in gates.iter() {
if gate.is_non_unitary() {
folded.add_gate(gate.clone());
continue;
}
let rounds = full_rounds + if unitary_counter < extra_folds { 1 } else { 0 };
unitary_counter += 1;
folded.add_gate(gate.clone());
for _ in 0..rounds {
let dag = gate_dagger(gate);
folded.add_gate(dag);
folded.add_gate(gate.clone());
}
}
folded
}
fn gate_dagger(gate: &Gate) -> Gate {
match gate {
Gate::H(q) => Gate::H(*q),
Gate::X(q) => Gate::X(*q),
Gate::Y(q) => Gate::Y(*q),
Gate::Z(q) => Gate::Z(*q),
Gate::CNOT(c, t) => Gate::CNOT(*c, *t),
Gate::CZ(q1, q2) => Gate::CZ(*q1, *q2),
Gate::SWAP(q1, q2) => Gate::SWAP(*q1, *q2),
Gate::S(q) => Gate::Sdg(*q),
Gate::Sdg(q) => Gate::S(*q),
Gate::T(q) => Gate::Tdg(*q),
Gate::Tdg(q) => Gate::T(*q),
Gate::Rx(q, theta) => Gate::Rx(*q, -theta),
Gate::Ry(q, theta) => Gate::Ry(*q, -theta),
Gate::Rz(q, theta) => Gate::Rz(*q, -theta),
Gate::Phase(q, theta) => Gate::Phase(*q, -theta),
Gate::Rzz(q1, q2, theta) => Gate::Rzz(*q1, *q2, -theta),
Gate::Unitary1Q(q, m) => {
let dag = [
[m[0][0].conj(), m[1][0].conj()],
[m[0][1].conj(), m[1][1].conj()],
];
Gate::Unitary1Q(*q, dag)
}
Gate::Measure(q) => Gate::Measure(*q),
Gate::Reset(q) => Gate::Reset(*q),
Gate::Barrier => Gate::Barrier,
}
}
pub fn richardson_extrapolate(noise_factors: &[f64], values: &[f64]) -> f64 {
assert_eq!(
noise_factors.len(),
values.len(),
"noise_factors and values must have the same length"
);
let n = noise_factors.len();
assert!(n > 0, "need at least one data point");
let mut result = 0.0;
for i in 0..n {
let mut weight = 1.0;
for j in 0..n {
if j != i {
weight *= -noise_factors[j] / (noise_factors[i] - noise_factors[j]);
}
}
result += values[i] * weight;
}
result
}
pub fn polynomial_extrapolate(noise_factors: &[f64], values: &[f64], degree: usize) -> f64 {
assert_eq!(
noise_factors.len(),
values.len(),
"noise_factors and values must have the same length"
);
let n = noise_factors.len();
let p = degree + 1; assert!(n >= p, "need at least degree+1 data points for a degree-{degree} polynomial");
let mut ata = vec![vec![0.0_f64; p]; p];
let mut aty = vec![0.0_f64; p];
for i in 0..n {
let x = noise_factors[i];
let y = values[i];
let max_power = 2 * degree;
let mut x_powers = Vec::with_capacity(max_power + 1);
x_powers.push(1.0);
for k in 1..=max_power {
x_powers.push(x_powers[k - 1] * x);
}
for j in 0..p {
aty[j] += y * x_powers[j];
for k in 0..p {
ata[j][k] += x_powers[j + k];
}
}
}
let coeffs = solve_linear_system(&mut ata, &mut aty);
coeffs[0]
}
pub fn linear_extrapolate(noise_factors: &[f64], values: &[f64]) -> f64 {
polynomial_extrapolate(noise_factors, values, 1)
}
fn solve_linear_system(a: &mut Vec<Vec<f64>>, b: &mut Vec<f64>) -> Vec<f64> {
let n = b.len();
assert!(n > 0);
for col in 0..n {
let mut max_row = col;
let mut max_val = a[col][col].abs();
for row in (col + 1)..n {
let v = a[row][col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_row != col {
a.swap(col, max_row);
b.swap(col, max_row);
}
let pivot = a[col][col];
assert!(
pivot.abs() > 1e-15,
"singular or near-singular matrix in least-squares solve"
);
for row in (col + 1)..n {
let factor = a[row][col] / pivot;
for k in col..n {
a[row][k] -= factor * a[col][k];
}
b[row] -= factor * b[col];
}
}
let mut x = vec![0.0; n];
for col in (0..n).rev() {
let mut sum = b[col];
for k in (col + 1)..n {
sum -= a[col][k] * x[k];
}
x[col] = sum / a[col][col];
}
x
}
#[derive(Debug, Clone)]
pub struct MeasurementCorrector {
num_qubits: usize,
calibration_matrix: Vec<Vec<f64>>,
}
impl MeasurementCorrector {
pub fn new(readout_errors: &[(f64, f64)]) -> Self {
let num_qubits = readout_errors.len();
let dim = 1usize << num_qubits;
let qubit_matrices: Vec<[[f64; 2]; 2]> = readout_errors
.iter()
.map(|&(p01, p10)| {
[
[1.0 - p01, p10],
[p01, 1.0 - p10],
]
})
.collect();
let mut cal = vec![vec![0.0; dim]; dim];
for row in 0..dim {
for col in 0..dim {
let mut val = 1.0;
for q in 0..num_qubits {
let row_bit = (row >> q) & 1;
let col_bit = (col >> q) & 1;
val *= qubit_matrices[q][row_bit][col_bit];
}
cal[row][col] = val;
}
}
Self {
num_qubits,
calibration_matrix: cal,
}
}
pub fn correct_counts(
&self,
counts: &HashMap<Vec<bool>, usize>,
) -> HashMap<Vec<bool>, f64> {
let dim = 1usize << self.num_qubits;
let total_shots: usize = counts.values().sum();
let total_f64 = total_shots as f64;
let mut prob_vec = vec![0.0; dim];
for (bits, &count) in counts {
let idx = bits_to_index(bits, self.num_qubits);
prob_vec[idx] = count as f64 / total_f64;
}
let corrected_probs = if self.num_qubits <= 12 {
let inv = invert_matrix(&self.calibration_matrix);
mat_vec_mul(&inv, &prob_vec)
} else {
self.tensor_product_correct(&prob_vec)
};
let mut result = HashMap::new();
for idx in 0..dim {
let corrected_count = corrected_probs[idx] * total_f64;
if corrected_count.abs() > 1e-10 {
let bits = index_to_bits(idx, self.num_qubits);
result.insert(bits, corrected_count);
}
}
result
}
pub fn calibration_matrix(&self) -> &Vec<Vec<f64>> {
&self.calibration_matrix
}
fn tensor_product_correct(&self, prob_vec: &[f64]) -> Vec<f64> {
let dim = 1usize << self.num_qubits;
let mut result = prob_vec.to_vec();
for q in 0..self.num_qubits {
let qubit_mat = self.extract_qubit_matrix(q);
let inv = invert_2x2(&qubit_mat);
let mut new_result = vec![0.0; dim];
let stride = 1usize << q;
for block_start in (0..dim).step_by(stride * 2) {
for offset in 0..stride {
let i0 = block_start + offset;
let i1 = i0 + stride;
new_result[i0] = inv[0][0] * result[i0] + inv[0][1] * result[i1];
new_result[i1] = inv[1][0] * result[i0] + inv[1][1] * result[i1];
}
}
result = new_result;
}
result
}
fn extract_qubit_matrix(&self, qubit: usize) -> [[f64; 2]; 2] {
let i0 = 0;
let i1 = 1usize << qubit;
[
[self.calibration_matrix[i0][i0], self.calibration_matrix[i0][i1]],
[self.calibration_matrix[i1][i0], self.calibration_matrix[i1][i1]],
]
}
}
fn bits_to_index(bits: &[bool], num_qubits: usize) -> usize {
let mut idx = 0usize;
for q in 0..num_qubits {
if q < bits.len() && bits[q] {
idx |= 1 << q;
}
}
idx
}
fn index_to_bits(idx: usize, num_qubits: usize) -> Vec<bool> {
(0..num_qubits).map(|q| (idx >> q) & 1 == 1).collect()
}
fn invert_2x2(m: &[[f64; 2]; 2]) -> [[f64; 2]; 2] {
let det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
assert!(det.abs() > 1e-15, "singular 2x2 matrix");
let inv_det = 1.0 / det;
[
[m[1][1] * inv_det, -m[0][1] * inv_det],
[-m[1][0] * inv_det, m[0][0] * inv_det],
]
}
fn invert_matrix(mat: &[Vec<f64>]) -> Vec<Vec<f64>> {
let n = mat.len();
let mut aug: Vec<Vec<f64>> = mat
.iter()
.enumerate()
.map(|(i, row)| {
let mut aug_row = row.clone();
aug_row.resize(2 * n, 0.0);
aug_row[n + i] = 1.0;
aug_row
})
.collect();
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[col][col].abs();
for row in (col + 1)..n {
let v = aug[row][col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
aug.swap(col, max_row);
let pivot = aug[col][col];
assert!(
pivot.abs() > 1e-15,
"singular matrix in calibration inversion"
);
let inv_pivot = 1.0 / pivot;
for k in 0..(2 * n) {
aug[col][k] *= inv_pivot;
}
for row in 0..n {
if row == col {
continue;
}
let factor = aug[row][col];
for k in 0..(2 * n) {
aug[row][k] -= factor * aug[col][k];
}
}
}
aug.iter()
.map(|row| row[n..].to_vec())
.collect()
}
fn mat_vec_mul(mat: &[Vec<f64>], vec: &[f64]) -> Vec<f64> {
mat.iter()
.map(|row| row.iter().zip(vec.iter()).map(|(a, b)| a * b).sum())
.collect()
}
#[derive(Debug, Clone)]
pub struct CdrConfig {
pub num_training_circuits: usize,
pub seed: u64,
}
pub fn generate_training_circuits(
circuit: &QuantumCircuit,
config: &CdrConfig,
) -> Vec<QuantumCircuit> {
let mut circuits = Vec::with_capacity(config.num_training_circuits);
let mut rng_state = config.seed;
let lcg_next = |state: &mut u64| -> u64 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*state
};
let clifford_1q = |q: u32, choice: u64| -> Gate {
match choice % 6 {
0 => Gate::H(q),
1 => Gate::X(q),
2 => Gate::Y(q),
3 => Gate::Z(q),
4 => Gate::S(q),
_ => Gate::Sdg(q),
}
};
let clifford_2q = |q1: u32, q2: u32, choice: u64| -> Gate {
match choice % 3 {
0 => Gate::CNOT(q1, q2),
1 => Gate::CZ(q1, q2),
_ => Gate::SWAP(q1, q2),
}
};
for _ in 0..config.num_training_circuits {
let mut training = QuantumCircuit::new(circuit.num_qubits());
for gate in circuit.gates() {
let replacement = match gate {
Gate::T(q) | Gate::Tdg(q) => {
let r = lcg_next(&mut rng_state);
clifford_1q(*q, r)
}
Gate::Rx(q, _) | Gate::Ry(q, _) | Gate::Rz(q, _) | Gate::Phase(q, _) => {
let r = lcg_next(&mut rng_state);
clifford_1q(*q, r)
}
Gate::Unitary1Q(q, _) => {
let r = lcg_next(&mut rng_state);
clifford_1q(*q, r)
}
Gate::Rzz(q1, q2, _) => {
let r = lcg_next(&mut rng_state);
clifford_2q(*q1, *q2, r)
}
other => other.clone(),
};
training.add_gate(replacement);
}
circuits.push(training);
}
circuits
}
pub fn cdr_correct(noisy_values: &[f64], ideal_values: &[f64], target_noisy: f64) -> f64 {
assert_eq!(
noisy_values.len(),
ideal_values.len(),
"noisy_values and ideal_values must have the same length"
);
let n = noisy_values.len();
assert!(n >= 2, "need at least 2 training points for CDR");
let sum_x: f64 = noisy_values.iter().sum();
let sum_y: f64 = ideal_values.iter().sum();
let sum_xy: f64 = noisy_values.iter().zip(ideal_values.iter()).map(|(x, y)| x * y).sum();
let sum_x2: f64 = noisy_values.iter().map(|x| x * x).sum();
let n_f64 = n as f64;
let denom = n_f64 * sum_x2 - sum_x * sum_x;
if denom.abs() < 1e-15 {
return sum_y / n_f64;
}
let a = (n_f64 * sum_xy - sum_x * sum_y) / denom;
let b = (sum_y - a * sum_x) / n_f64;
a * target_noisy + b
}
pub fn expectation_from_counts(counts: &HashMap<Vec<bool>, usize>, qubit: u32) -> f64 {
let mut total_shots: usize = 0;
let mut z_sum: f64 = 0.0;
for (bits, &count) in counts {
total_shots += count;
let bit_val = bits.get(qubit as usize).copied().unwrap_or(false);
let z_eigenvalue = if bit_val { -1.0 } else { 1.0 };
z_sum += z_eigenvalue * count as f64;
}
if total_shots == 0 {
return 0.0;
}
z_sum / total_shots as f64
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Complex;
#[test]
fn test_richardson_recovers_polynomial() {
let noise_factors = vec![1.0, 2.0, 3.0];
let values: Vec<f64> = noise_factors
.iter()
.map(|&x| 3.0 * x * x - 2.0 * x + 5.0)
.collect();
let result = richardson_extrapolate(&noise_factors, &values);
assert!(
(result - 5.0).abs() < 1e-10,
"Richardson should recover f(0) = 5.0, got {result}"
);
}
#[test]
fn test_richardson_linear_data() {
let noise_factors = vec![1.0, 2.0];
let values = vec![9.0, 11.0];
let result = richardson_extrapolate(&noise_factors, &values);
assert!(
(result - 7.0).abs() < 1e-10,
"Richardson on linear data: expected 7.0, got {result}"
);
}
#[test]
fn test_richardson_cubic() {
let noise_factors = vec![1.0, 1.5, 2.0, 3.0];
let values: Vec<f64> = noise_factors
.iter()
.map(|&x| x * x * x - x + 1.0)
.collect();
let result = richardson_extrapolate(&noise_factors, &values);
assert!(
(result - 1.0).abs() < 1e-9,
"Richardson on cubic data: expected 1.0, got {result}"
);
}
#[test]
fn test_linear_extrapolation_exact() {
let noise_factors = vec![1.0, 2.0, 3.0];
let values: Vec<f64> = noise_factors.iter().map(|&x| 3.0 * x + 2.0).collect();
let result = linear_extrapolate(&noise_factors, &values);
assert!(
(result - 2.0).abs() < 1e-10,
"Linear extrapolation: expected 2.0, got {result}"
);
}
#[test]
fn test_linear_extrapolation_two_points() {
let noise_factors = vec![1.0, 3.0];
let values = vec![5.0, 11.0]; let result = linear_extrapolate(&noise_factors, &values);
assert!(
(result - 2.0).abs() < 1e-10,
"Linear extrapolation with 2 points: expected 2.0, got {result}"
);
}
#[test]
fn test_polynomial_extrapolation_quadratic() {
let noise_factors = vec![1.0, 2.0, 3.0];
let values: Vec<f64> = noise_factors.iter().map(|&x| x * x + 1.0).collect();
let result = polynomial_extrapolate(&noise_factors, &values, 2);
assert!(
(result - 1.0).abs() < 1e-10,
"Polynomial (degree 2): expected 1.0, got {result}"
);
}
#[test]
fn test_fold_circuit_factor_1() {
let mut circuit = QuantumCircuit::new(2);
circuit.h(0);
circuit.cnot(0, 1);
circuit.measure(0);
circuit.measure(1);
let folded = fold_circuit(&circuit, 1.0);
assert_eq!(
folded.gates().len(),
circuit.gates().len(),
"fold factor=1 should produce the same number of gates"
);
}
#[test]
fn test_fold_circuit_factor_3() {
let mut circuit = QuantumCircuit::new(2);
circuit.h(0);
circuit.cnot(0, 1);
let folded = fold_circuit(&circuit, 3.0);
let unitary_count = folded.gates().iter().filter(|g| !g.is_non_unitary()).count();
assert_eq!(
unitary_count, 6,
"fold factor=3 on 2-gate circuit: expected 6 unitary gates, got {unitary_count}"
);
}
#[test]
fn test_fold_circuit_factor_3_preserves_measurements() {
let mut circuit = QuantumCircuit::new(1);
circuit.h(0);
circuit.measure(0);
let folded = fold_circuit(&circuit, 3.0);
let measure_count = folded
.gates()
.iter()
.filter(|g| matches!(g, Gate::Measure(_)))
.count();
assert_eq!(
measure_count, 1,
"measurements should not be folded"
);
let unitary_count = folded.gates().iter().filter(|g| !g.is_non_unitary()).count();
assert_eq!(
unitary_count, 3,
"1 H gate folded at factor 3 => 3 unitary gates"
);
}
#[test]
fn test_fold_circuit_fractional_factor() {
let mut circuit = QuantumCircuit::new(2);
circuit.h(0);
circuit.x(1);
circuit.cnot(0, 1);
circuit.z(0);
let folded = fold_circuit(&circuit, 1.5);
let unitary_count = folded.gates().iter().filter(|g| !g.is_non_unitary()).count();
assert_eq!(
unitary_count, 6,
"fold factor=1.5 on 4-gate circuit: expected 6 unitary gates, got {unitary_count}"
);
}
#[test]
fn test_measurement_corrector_zero_error_is_identity() {
let corrector = MeasurementCorrector::new(&[(0.0, 0.0), (0.0, 0.0)]);
let cal = corrector.calibration_matrix();
let dim = 4; for i in 0..dim {
for j in 0..dim {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(cal[i][j] - expected).abs() < 1e-12,
"cal[{i}][{j}] = {}, expected {expected}",
cal[i][j]
);
}
}
}
#[test]
fn test_measurement_corrector_single_qubit() {
let corrector = MeasurementCorrector::new(&[(0.1, 0.05)]);
let cal = corrector.calibration_matrix();
assert!((cal[0][0] - 0.9).abs() < 1e-12);
assert!((cal[0][1] - 0.05).abs() < 1e-12);
assert!((cal[1][0] - 0.1).abs() < 1e-12);
assert!((cal[1][1] - 0.95).abs() < 1e-12);
}
#[test]
fn test_measurement_corrector_correction_identity() {
let corrector = MeasurementCorrector::new(&[(0.0, 0.0)]);
let mut counts = HashMap::new();
counts.insert(vec![false], 600);
counts.insert(vec![true], 400);
let corrected = corrector.correct_counts(&counts);
let c0 = corrected.get(&vec![false]).copied().unwrap_or(0.0);
let c1 = corrected.get(&vec![true]).copied().unwrap_or(0.0);
assert!(
(c0 - 600.0).abs() < 1e-6,
"expected 600.0 for |0>, got {c0}"
);
assert!(
(c1 - 400.0).abs() < 1e-6,
"expected 400.0 for |1>, got {c1}"
);
}
#[test]
fn test_measurement_corrector_nontrivial_correction() {
let corrector = MeasurementCorrector::new(&[(0.1, 0.05)]);
let mut counts = HashMap::new();
counts.insert(vec![false], 550);
counts.insert(vec![true], 450);
let corrected = corrector.correct_counts(&counts);
let c0 = corrected.get(&vec![false]).copied().unwrap_or(0.0);
let c1 = corrected.get(&vec![true]).copied().unwrap_or(0.0);
assert!(
(c0 + c1 - 1000.0).abs() < 1.0,
"total corrected counts should sum to ~1000"
);
assert!(
(c0 - 550.0).abs() > 1.0 || (c1 - 450.0).abs() > 1.0,
"correction should change the counts"
);
}
#[test]
fn test_cdr_correct_known_linear() {
let noisy_values = vec![1.0, 2.0, 3.0, 4.0];
let ideal_values: Vec<f64> = noisy_values.iter().map(|&x| 2.0 * x - 1.0).collect();
let result = cdr_correct(&noisy_values, &ideal_values, 3.0);
assert!(
(result - 5.0).abs() < 1e-10,
"CDR correction: expected 5.0, got {result}"
);
}
#[test]
fn test_cdr_correct_identity_model() {
let noisy_values = vec![1.0, 2.0, 3.0];
let ideal_values = vec![1.0, 2.0, 3.0];
let result = cdr_correct(&noisy_values, &ideal_values, 5.0);
assert!(
(result - 5.0).abs() < 1e-10,
"CDR identity model: expected 5.0, got {result}"
);
}
#[test]
fn test_cdr_correct_offset() {
let noisy_values = vec![0.0, 1.0, 2.0];
let ideal_values = vec![0.5, 1.5, 2.5];
let result = cdr_correct(&noisy_values, &ideal_values, 3.0);
assert!(
(result - 3.5).abs() < 1e-10,
"CDR offset model: expected 3.5, got {result}"
);
}
#[test]
fn test_generate_training_circuits_count() {
let mut circuit = QuantumCircuit::new(2);
circuit.h(0);
circuit.t(0);
circuit.cnot(0, 1);
circuit.rx(1, 0.5);
let config = CdrConfig {
num_training_circuits: 10,
seed: 42,
};
let training = generate_training_circuits(&circuit, &config);
assert_eq!(training.len(), 10);
}
#[test]
fn test_generate_training_circuits_preserves_clifford_gates() {
let mut circuit = QuantumCircuit::new(2);
circuit.h(0);
circuit.cnot(0, 1);
circuit.x(1);
let config = CdrConfig {
num_training_circuits: 5,
seed: 0,
};
let training = generate_training_circuits(&circuit, &config);
for tc in &training {
assert_eq!(
tc.gates().len(),
circuit.gates().len(),
"training circuit should have same gate count"
);
}
}
#[test]
fn test_generate_training_circuits_replaces_non_clifford() {
let mut circuit = QuantumCircuit::new(1);
circuit.t(0);
let config = CdrConfig {
num_training_circuits: 20,
seed: 123,
};
let training = generate_training_circuits(&circuit, &config);
for tc in &training {
for gate in tc.gates() {
assert!(
!matches!(gate, Gate::T(_)),
"training circuit should not contain T gate"
);
}
}
}
#[test]
fn test_generate_training_circuits_deterministic() {
let mut circuit = QuantumCircuit::new(1);
circuit.rx(0, 1.0);
circuit.t(0);
let config = CdrConfig {
num_training_circuits: 5,
seed: 42,
};
let training1 = generate_training_circuits(&circuit, &config);
let training2 = generate_training_circuits(&circuit, &config);
assert_eq!(training1.len(), training2.len());
for (t1, t2) in training1.iter().zip(training2.iter()) {
assert_eq!(t1.gates().len(), t2.gates().len());
}
}
#[test]
fn test_expectation_all_zero() {
let mut counts = HashMap::new();
counts.insert(vec![false], 1000);
let exp = expectation_from_counts(&counts, 0);
assert!(
(exp - 1.0).abs() < 1e-12,
"all |0>: expected <Z> = 1.0, got {exp}"
);
}
#[test]
fn test_expectation_all_one() {
let mut counts = HashMap::new();
counts.insert(vec![true], 500);
let exp = expectation_from_counts(&counts, 0);
assert!(
(exp - (-1.0)).abs() < 1e-12,
"all |1>: expected <Z> = -1.0, got {exp}"
);
}
#[test]
fn test_expectation_equal_split() {
let mut counts = HashMap::new();
counts.insert(vec![false], 500);
counts.insert(vec![true], 500);
let exp = expectation_from_counts(&counts, 0);
assert!(
exp.abs() < 1e-12,
"equal split: expected <Z> = 0.0, got {exp}"
);
}
#[test]
fn test_expectation_multi_qubit() {
let mut counts = HashMap::new();
counts.insert(vec![false, false], 300);
counts.insert(vec![true, false], 200);
counts.insert(vec![false, true], 100);
counts.insert(vec![true, true], 400);
let exp0 = expectation_from_counts(&counts, 0);
let exp1 = expectation_from_counts(&counts, 1);
assert!(
(exp0 - (-0.2)).abs() < 1e-12,
"qubit 0: expected -0.2, got {exp0}"
);
assert!(
exp1.abs() < 1e-12,
"qubit 1: expected 0.0, got {exp1}"
);
}
#[test]
fn test_expectation_empty_counts() {
let counts: HashMap<Vec<bool>, usize> = HashMap::new();
let exp = expectation_from_counts(&counts, 0);
assert!(
exp.abs() < 1e-12,
"empty counts should give 0.0, got {exp}"
);
}
#[test]
fn test_gate_dagger_self_inverse() {
let gates = vec![Gate::H(0), Gate::X(0), Gate::Y(0), Gate::Z(0)];
for gate in &gates {
let dag = gate_dagger(gate);
if let (Some(m_orig), Some(m_dag)) = (gate.matrix_1q(), dag.matrix_1q()) {
for i in 0..2 {
for j in 0..2 {
let diff = (m_orig[i][j] - m_dag[i][j]).norm();
assert!(
diff < 1e-12,
"gate_dagger of self-inverse gate should match: diff = {diff}"
);
}
}
}
}
}
#[test]
fn test_gate_dagger_s_sdg() {
let s_dag = gate_dagger(&Gate::S(0));
let sdg = Gate::Sdg(0);
let m1 = s_dag.matrix_1q().unwrap();
let m2 = sdg.matrix_1q().unwrap();
for i in 0..2 {
for j in 0..2 {
let diff = (m1[i][j] - m2[i][j]).norm();
assert!(diff < 1e-12, "S dagger should equal Sdg");
}
}
}
#[test]
fn test_gate_dagger_rotation_inverse() {
let theta = 1.23;
let rx = Gate::Rx(0, theta);
let rx_dag = gate_dagger(&rx);
let m = rx.matrix_1q().unwrap();
let m_dag = rx_dag.matrix_1q().unwrap();
let product = mat_mul_2x2(&m, &m_dag);
for i in 0..2 {
for j in 0..2 {
let expected = if i == j {
Complex::ONE
} else {
Complex::ZERO
};
let diff = (product[i][j] - expected).norm();
assert!(
diff < 1e-12,
"Rx * Rx^dag should be identity at [{i}][{j}]: diff = {diff}"
);
}
}
}
fn mat_mul_2x2(
a: &[[Complex; 2]; 2],
b: &[[Complex; 2]; 2],
) -> [[Complex; 2]; 2] {
let mut result = [[Complex::ZERO; 2]; 2];
for i in 0..2 {
for j in 0..2 {
for k in 0..2 {
result[i][j] = result[i][j] + a[i][k] * b[k][j];
}
}
}
result
}
}