use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
use scirs2_core::parallel_ops::{IndexedParallelIterator, ParallelIterator};
use scirs2_core::random::prelude::*;
use scirs2_core::random::{ChaCha8Rng, RngExt as RngTrait, SeedableRng}; use scirs2_core::Complex64;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::error::{Result, SimulatorError};
use crate::scirs2_integration::SciRS2Backend;
use crate::shot_sampling::{QuantumSampler, SamplingConfig};
use crate::statevector::StateVectorSimulator;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuantumVolumeResult {
pub quantum_volume: u64,
pub width: usize,
pub depth: usize,
pub num_circuits: usize,
pub threshold: f64,
pub success_probability: f64,
pub heavy_output_probs: Vec<f64>,
pub confidence: f64,
pub passed: bool,
pub stats: QVStats,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct QVStats {
pub total_time_s: f64,
pub classical_sim_time_s: f64,
pub circuit_gen_time_s: f64,
pub heavy_output_time_s: f64,
pub memory_usage_bytes: usize,
pub total_gates: usize,
pub avg_fidelity: f64,
}
#[derive(Debug, Clone)]
pub struct QVCircuit {
pub width: usize,
pub depth: usize,
pub gates: Vec<QVGate>,
pub permutation: Vec<usize>,
pub ideal_amplitudes: Array1<Complex64>,
pub heavy_threshold: f64,
pub heavy_outputs: Vec<usize>,
}
#[derive(Debug, Clone)]
pub struct QVGate {
pub qubits: [usize; 2],
pub matrix: Array2<Complex64>,
pub layer: usize,
}
pub struct QuantumVolumeCalculator {
rng: ChaCha8Rng,
backend: Option<SciRS2Backend>,
params: QVParams,
}
#[derive(Debug, Clone)]
pub struct QVParams {
pub circuits_per_width: usize,
pub heavy_output_threshold: f64,
pub confidence_level: f64,
pub max_width: usize,
pub seed: Option<u64>,
pub parallel: bool,
}
impl Default for QVParams {
fn default() -> Self {
Self {
circuits_per_width: 100,
heavy_output_threshold: 2.0 / 3.0,
confidence_level: 0.95,
max_width: 8,
seed: None,
parallel: true,
}
}
}
impl QuantumVolumeCalculator {
#[must_use]
pub fn new(params: QVParams) -> Self {
let rng = if let Some(seed) = params.seed {
ChaCha8Rng::seed_from_u64(seed)
} else {
ChaCha8Rng::from_rng(&mut thread_rng())
};
Self {
rng,
backend: None,
params,
}
}
pub fn with_scirs2_backend(mut self) -> Result<Self> {
self.backend = Some(SciRS2Backend::new());
Ok(self)
}
pub fn calculate_quantum_volume(&mut self, width: usize) -> Result<QuantumVolumeResult> {
let start_time = std::time::Instant::now();
let depth = width;
let mut stats = QVStats::default();
let mut heavy_output_probs = Vec::with_capacity(self.params.circuits_per_width);
for circuit_idx in 0..self.params.circuits_per_width {
let circuit_start = std::time::Instant::now();
let circuit = self.generate_qv_circuit(width, depth)?;
stats.circuit_gen_time_s += circuit_start.elapsed().as_secs_f64();
let heavy_prob_start = std::time::Instant::now();
let heavy_prob = self.calculate_heavy_output_probability(&circuit)?;
stats.heavy_output_time_s += heavy_prob_start.elapsed().as_secs_f64();
heavy_output_probs.push(heavy_prob);
stats.total_gates += circuit.gates.len();
if circuit_idx % 10 == 0 && circuit_idx > 0 {
println!(
"Processed {}/{} circuits for width {}",
circuit_idx, self.params.circuits_per_width, width
);
}
}
let success_count = heavy_output_probs
.iter()
.filter(|&&prob| prob >= self.params.heavy_output_threshold)
.count();
let success_probability = success_count as f64 / heavy_output_probs.len() as f64;
let required_success_prob = self.get_required_success_probability(width)?;
let passed = success_probability >= required_success_prob;
let quantum_volume = if passed { 1 << width } else { 0 };
let confidence = self.calculate_confidence(&heavy_output_probs, required_success_prob)?;
stats.total_time_s = start_time.elapsed().as_secs_f64();
stats.memory_usage_bytes = self.estimate_memory_usage(width);
stats.avg_fidelity =
heavy_output_probs.iter().sum::<f64>() / heavy_output_probs.len() as f64;
Ok(QuantumVolumeResult {
quantum_volume,
width,
depth,
num_circuits: self.params.circuits_per_width,
threshold: required_success_prob,
success_probability,
heavy_output_probs,
confidence,
passed,
stats,
})
}
fn generate_qv_circuit(&mut self, width: usize, depth: usize) -> Result<QVCircuit> {
if width < 2 {
return Err(SimulatorError::InvalidInput(
"Quantum volume requires at least 2 qubits".to_string(),
));
}
let mut gates = Vec::new();
let mut available_qubits: Vec<usize> = (0..width).collect();
for layer in 0..depth {
available_qubits.shuffle(&mut self.rng);
for pair_idx in 0..(width / 2) {
let qubit1 = available_qubits[2 * pair_idx];
let qubit2 = available_qubits[2 * pair_idx + 1];
let gate = QVGate {
qubits: [qubit1, qubit2],
matrix: self.generate_random_su4()?,
layer,
};
gates.push(gate);
}
}
let mut permutation: Vec<usize> = (0..width).collect();
permutation.shuffle(&mut self.rng);
let ideal_amplitudes = self.simulate_ideal_circuit(width, &gates, &permutation)?;
let heavy_threshold = self.calculate_heavy_threshold(&ideal_amplitudes);
let heavy_outputs = self.find_heavy_outputs(&ideal_amplitudes, heavy_threshold);
Ok(QVCircuit {
width,
depth,
gates,
permutation,
ideal_amplitudes,
heavy_threshold,
heavy_outputs,
})
}
fn generate_random_su4(&mut self) -> Result<Array2<Complex64>> {
let mut params = Vec::with_capacity(15);
for _ in 0..15 {
params.push(self.rng.random::<f64>() * 2.0 * std::f64::consts::PI);
}
let mut matrix = Array2::zeros((4, 4));
for i in 0..4 {
for j in 0..4 {
let real = self.rng.random::<f64>() - 0.5;
let imag = self.rng.random::<f64>() - 0.5;
matrix[[i, j]] = Complex64::new(real, imag);
}
}
self.gram_schmidt_orthogonalize(&mut matrix)?;
Ok(matrix)
}
fn gram_schmidt_orthogonalize(&self, matrix: &mut Array2<Complex64>) -> Result<()> {
let n = matrix.nrows();
for j in 0..n {
let mut norm = 0.0;
for i in 0..n {
norm += matrix[[i, j]].norm_sqr();
}
norm = norm.sqrt();
if norm > 1e-12 {
for i in 0..n {
matrix[[i, j]] /= norm;
}
}
for k in (j + 1)..n {
let mut dot_product = Complex64::new(0.0, 0.0);
for i in 0..n {
dot_product += matrix[[i, j]].conj() * matrix[[i, k]];
}
let column_j_values: Vec<Complex64> = (0..n).map(|i| matrix[[i, j]]).collect();
for i in 0..n {
matrix[[i, k]] -= dot_product * column_j_values[i];
}
}
}
Ok(())
}
fn simulate_ideal_circuit(
&self,
width: usize,
gates: &[QVGate],
permutation: &[usize],
) -> Result<Array1<Complex64>> {
let dim = 1 << width;
let mut state = Array1::zeros(dim);
state[0] = Complex64::new(1.0, 0.0);
for layer in 0..=gates.iter().map(|g| g.layer).max().unwrap_or(0) {
let layer_gates: Vec<&QVGate> = gates.iter().filter(|g| g.layer == layer).collect();
for gate in layer_gates {
self.apply_two_qubit_gate(
&mut state,
width,
&gate.matrix,
gate.qubits[0],
gate.qubits[1],
)?;
}
}
state = self.apply_permutation(&state, width, permutation)?;
Ok(state)
}
fn apply_two_qubit_gate(
&self,
state: &mut Array1<Complex64>,
width: usize,
gate_matrix: &Array2<Complex64>,
qubit1: usize,
qubit2: usize,
) -> Result<()> {
let dim = 1 << width;
let mut new_state = Array1::zeros(dim);
for basis_state in 0..dim {
let bit1 = (basis_state >> (width - 1 - qubit1)) & 1;
let bit2 = (basis_state >> (width - 1 - qubit2)) & 1;
let two_qubit_state = (bit1 << 1) | bit2;
for target_two_qubit in 0..4 {
let amplitude = gate_matrix[[target_two_qubit, two_qubit_state]];
if amplitude.norm() > 1e-12 {
let target_bit1 = (target_two_qubit >> 1) & 1;
let target_bit2 = target_two_qubit & 1;
let mut target_basis = basis_state;
if target_bit1 == 1 {
target_basis |= 1 << (width - 1 - qubit1);
} else {
target_basis &= !(1 << (width - 1 - qubit1));
}
if target_bit2 == 1 {
target_basis |= 1 << (width - 1 - qubit2);
} else {
target_basis &= !(1 << (width - 1 - qubit2));
}
new_state[target_basis] += amplitude * state[basis_state];
}
}
}
*state = new_state;
Ok(())
}
fn apply_permutation(
&self,
state: &Array1<Complex64>,
width: usize,
permutation: &[usize],
) -> Result<Array1<Complex64>> {
let dim = 1 << width;
let mut new_state = Array1::zeros(dim);
for basis_state in 0..dim {
let mut permuted_state = 0;
for (new_pos, &old_pos) in permutation.iter().enumerate() {
let bit = (basis_state >> (width - 1 - old_pos)) & 1;
permuted_state |= bit << (width - 1 - new_pos);
}
new_state[permuted_state] = state[basis_state];
}
Ok(new_state)
}
fn calculate_heavy_threshold(&self, amplitudes: &Array1<Complex64>) -> f64 {
let mut probabilities: Vec<f64> = amplitudes
.iter()
.map(scirs2_core::Complex::norm_sqr)
.collect();
probabilities.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median_idx = probabilities.len() / 2;
probabilities[median_idx]
}
fn find_heavy_outputs(&self, amplitudes: &Array1<Complex64>, threshold: f64) -> Vec<usize> {
amplitudes
.iter()
.enumerate()
.filter_map(|(idx, amp)| {
if amp.norm_sqr() > threshold {
Some(idx)
} else {
None
}
})
.collect()
}
fn calculate_heavy_output_probability(&self, circuit: &QVCircuit) -> Result<f64> {
let heavy_prob: f64 = circuit
.heavy_outputs
.iter()
.map(|&idx| circuit.ideal_amplitudes[idx].norm_sqr())
.sum();
Ok(heavy_prob)
}
fn get_required_success_probability(&self, width: usize) -> Result<f64> {
let baseline_threshold = 2.0 / 3.0;
let n = self.params.circuits_per_width as f64;
let confidence = self.params.confidence_level;
let z_score = match confidence {
x if x >= 0.99 => 2.576,
x if x >= 0.95 => 1.96,
x if x >= 0.90 => 1.645,
_ => 1.96,
};
let standard_error = (baseline_threshold * (1.0 - baseline_threshold) / n).sqrt();
let adjusted_threshold = baseline_threshold - z_score * standard_error;
Ok(adjusted_threshold.max(0.5)) }
fn calculate_confidence(&self, heavy_probs: &[f64], threshold: f64) -> Result<f64> {
let n = heavy_probs.len() as f64;
let success_count = heavy_probs.iter().filter(|&&p| p >= threshold).count() as f64;
let success_rate = success_count / n;
let p = success_rate;
let standard_error = (p * (1.0 - p) / n).sqrt();
if success_rate > threshold {
let z_score = (success_rate - threshold) / standard_error;
Ok(self.normal_cdf(z_score))
} else {
Ok(0.0)
}
}
fn normal_cdf(&self, x: f64) -> f64 {
0.5 * (1.0 + self.erf(x / 2.0_f64.sqrt()))
}
fn erf(&self, x: f64) -> f64 {
let a1 = 0.254_829_592;
let a2 = -0.284_496_736;
let a3 = 1.421_413_741;
let a4 = -1.453_152_027;
let a5 = 1.061_405_429;
let p = 0.327_591_1;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs();
let t = 1.0 / (1.0 + p * x);
let y = ((a5 * t + a4).mul_add(t, a3).mul_add(t, a2).mul_add(t, a1) * t)
.mul_add(-(-x * x).exp(), 1.0);
sign * y
}
const fn estimate_memory_usage(&self, width: usize) -> usize {
let state_vector_size = (1 << width) * std::mem::size_of::<Complex64>();
let circuit_storage = 1000 * std::mem::size_of::<QVGate>(); state_vector_size + circuit_storage
}
pub fn find_maximum_quantum_volume(&mut self) -> Result<Vec<QuantumVolumeResult>> {
let mut results = Vec::new();
for width in 2..=self.params.max_width {
println!("Testing quantum volume for width {width}");
let result = self.calculate_quantum_volume(width)?;
println!(
"Width {}: QV = {}, Success Prob = {:.3}, Passed = {}",
width, result.quantum_volume, result.success_probability, result.passed
);
results.push(result.clone());
if !result.passed {
break;
}
}
Ok(results)
}
}
pub fn benchmark_quantum_volume(max_width: usize) -> Result<Vec<QuantumVolumeResult>> {
let params = QVParams {
circuits_per_width: 20, max_width,
..Default::default()
};
let mut calculator = QuantumVolumeCalculator::new(params);
calculator.find_maximum_quantum_volume()
}
pub fn calculate_quantum_volume_with_params(
width: usize,
circuits: usize,
seed: Option<u64>,
) -> Result<QuantumVolumeResult> {
let params = QVParams {
circuits_per_width: circuits,
max_width: width,
seed,
..Default::default()
};
let mut calculator = QuantumVolumeCalculator::new(params);
calculator.calculate_quantum_volume(width)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_qv_calculator_creation() {
let params = QVParams::default();
let calculator = QuantumVolumeCalculator::new(params);
assert!(calculator.params.circuits_per_width > 0);
}
#[test]
fn test_random_su4_generation() {
let params = QVParams::default();
let mut calculator = QuantumVolumeCalculator::new(params);
let matrix = calculator
.generate_random_su4()
.expect("Failed to generate random SU(4) matrix");
assert_eq!(matrix.shape(), [4, 4]);
let matrix_dagger = matrix.t().mapv(|x| x.conj());
let product = matrix_dagger.dot(&matrix);
for i in 0..4 {
assert!((product[[i, i]].re - 1.0).abs() < 1e-1);
assert!(product[[i, i]].im.abs() < 1e-1);
}
}
#[test]
fn test_qv_circuit_generation() {
let params = QVParams::default();
let mut calculator = QuantumVolumeCalculator::new(params);
let circuit = calculator
.generate_qv_circuit(4, 4)
.expect("Failed to generate QV circuit");
assert_eq!(circuit.width, 4);
assert_eq!(circuit.depth, 4);
assert!(!circuit.gates.is_empty());
assert_eq!(circuit.permutation.len(), 4);
}
#[test]
fn test_heavy_output_calculation() {
let amplitudes = Array1::from_vec(vec![
Complex64::new(0.6, 0.0), Complex64::new(0.3, 0.0), Complex64::new(0.1, 0.0), Complex64::new(0.05, 0.0), ]);
let params = QVParams::default();
let calculator = QuantumVolumeCalculator::new(params);
let threshold = calculator.calculate_heavy_threshold(&litudes);
let heavy_outputs = calculator.find_heavy_outputs(&litudes, threshold);
assert!(threshold > 0.0);
assert!(!heavy_outputs.is_empty());
}
#[test]
fn test_small_quantum_volume() {
let result = calculate_quantum_volume_with_params(3, 10, Some(42))
.expect("Failed to calculate quantum volume");
assert_eq!(result.width, 3);
assert_eq!(result.depth, 3);
assert_eq!(result.num_circuits, 10);
assert!(!result.heavy_output_probs.is_empty());
}
#[test]
fn test_permutation_application() {
let params = QVParams::default();
let calculator = QuantumVolumeCalculator::new(params);
let state = Array1::from_vec(vec![
Complex64::new(1.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
]);
let permutation = vec![1, 0]; let permuted = calculator
.apply_permutation(&state, 2, &permutation)
.expect("Failed to apply permutation");
assert!((permuted[0].re - 1.0).abs() < 1e-10);
}
}