quantrs2_sim/
quantum_volume.rs

1//! Quantum Volume calculation and benchmarking protocol.
2//!
3//! This module implements the quantum volume protocol for benchmarking quantum
4//! computers, including random circuit generation, ideal simulation, heavy output
5//! probability calculation, and quantum volume determination.
6
7use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
8use scirs2_core::parallel_ops::{IndexedParallelIterator, ParallelIterator};
9use scirs2_core::random::prelude::*;
10use scirs2_core::random::{ChaCha8Rng, Rng as RngTrait, SeedableRng}; // Rename to avoid conflict
11use scirs2_core::Complex64;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15use crate::error::{Result, SimulatorError};
16use crate::scirs2_integration::SciRS2Backend;
17use crate::shot_sampling::{QuantumSampler, SamplingConfig};
18use crate::statevector::StateVectorSimulator;
19
20/// Quantum Volume test result
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct QuantumVolumeResult {
23    /// Quantum volume achieved
24    pub quantum_volume: u64,
25    /// Width tested (number of qubits)
26    pub width: usize,
27    /// Depth tested
28    pub depth: usize,
29    /// Number of circuits tested
30    pub num_circuits: usize,
31    /// Success probability threshold
32    pub threshold: f64,
33    /// Actual success probability achieved
34    pub success_probability: f64,
35    /// Heavy output probabilities for each circuit
36    pub heavy_output_probs: Vec<f64>,
37    /// Statistical confidence
38    pub confidence: f64,
39    /// Passed the quantum volume test
40    pub passed: bool,
41    /// Execution statistics
42    pub stats: QVStats,
43}
44
45/// Statistics for quantum volume calculation
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct QVStats {
48    /// Total execution time (seconds)
49    pub total_time_s: f64,
50    /// Classical simulation time (seconds)
51    pub classical_sim_time_s: f64,
52    /// Circuit generation time (seconds)
53    pub circuit_gen_time_s: f64,
54    /// Heavy output calculation time (seconds)
55    pub heavy_output_time_s: f64,
56    /// Memory usage (bytes)
57    pub memory_usage_bytes: usize,
58    /// Number of gates in total
59    pub total_gates: usize,
60    /// Average circuit fidelity
61    pub avg_fidelity: f64,
62}
63
64/// Single quantum volume circuit
65#[derive(Debug, Clone)]
66pub struct QVCircuit {
67    /// Circuit width (number of qubits)
68    pub width: usize,
69    /// Circuit depth
70    pub depth: usize,
71    /// Random SU(4) gates and their locations
72    pub gates: Vec<QVGate>,
73    /// Permutation applied at the end
74    pub permutation: Vec<usize>,
75    /// Ideal output amplitudes
76    pub ideal_amplitudes: Array1<Complex64>,
77    /// Heavy output threshold
78    pub heavy_threshold: f64,
79    /// Heavy outputs (above threshold)
80    pub heavy_outputs: Vec<usize>,
81}
82
83/// Quantum volume gate (random SU(4))
84#[derive(Debug, Clone)]
85pub struct QVGate {
86    /// Target qubits (always 2 for SU(4))
87    pub qubits: [usize; 2],
88    /// Gate matrix (4x4 unitary)
89    pub matrix: Array2<Complex64>,
90    /// Layer index
91    pub layer: usize,
92}
93
94/// Quantum Volume calculator
95pub struct QuantumVolumeCalculator {
96    /// Random number generator
97    rng: ChaCha8Rng,
98    /// `SciRS2` backend for optimization
99    backend: Option<SciRS2Backend>,
100    /// QV protocol parameters
101    params: QVParams,
102}
103
104/// Parameters for quantum volume protocol
105#[derive(Debug, Clone)]
106pub struct QVParams {
107    /// Number of circuits to test per width
108    pub circuits_per_width: usize,
109    /// Heavy output probability threshold (default: 2/3)
110    pub heavy_output_threshold: f64,
111    /// Statistical confidence level
112    pub confidence_level: f64,
113    /// Maximum width to test
114    pub max_width: usize,
115    /// Random seed for reproducibility
116    pub seed: Option<u64>,
117    /// Use parallel execution
118    pub parallel: bool,
119}
120
121impl Default for QVParams {
122    fn default() -> Self {
123        Self {
124            circuits_per_width: 100,
125            heavy_output_threshold: 2.0 / 3.0,
126            confidence_level: 0.95,
127            max_width: 8,
128            seed: None,
129            parallel: true,
130        }
131    }
132}
133
134impl QuantumVolumeCalculator {
135    /// Create new quantum volume calculator
136    #[must_use]
137    pub fn new(params: QVParams) -> Self {
138        let rng = if let Some(seed) = params.seed {
139            ChaCha8Rng::seed_from_u64(seed)
140        } else {
141            ChaCha8Rng::from_rng(&mut thread_rng())
142        };
143
144        Self {
145            rng,
146            backend: None,
147            params,
148        }
149    }
150
151    /// Initialize with `SciRS2` backend
152    pub fn with_scirs2_backend(mut self) -> Result<Self> {
153        self.backend = Some(SciRS2Backend::new());
154        Ok(self)
155    }
156
157    /// Calculate quantum volume for a given width
158    pub fn calculate_quantum_volume(&mut self, width: usize) -> Result<QuantumVolumeResult> {
159        let start_time = std::time::Instant::now();
160        let depth = width; // Square circuits
161
162        let mut stats = QVStats::default();
163        let mut heavy_output_probs = Vec::with_capacity(self.params.circuits_per_width);
164
165        // Generate and test circuits
166        for circuit_idx in 0..self.params.circuits_per_width {
167            let circuit_start = std::time::Instant::now();
168
169            // Generate random quantum volume circuit
170            let circuit = self.generate_qv_circuit(width, depth)?;
171            stats.circuit_gen_time_s += circuit_start.elapsed().as_secs_f64();
172
173            // Calculate heavy output probability
174            let heavy_prob_start = std::time::Instant::now();
175            let heavy_prob = self.calculate_heavy_output_probability(&circuit)?;
176            stats.heavy_output_time_s += heavy_prob_start.elapsed().as_secs_f64();
177
178            heavy_output_probs.push(heavy_prob);
179            stats.total_gates += circuit.gates.len();
180
181            // Progress tracking
182            if circuit_idx % 10 == 0 && circuit_idx > 0 {
183                println!(
184                    "Processed {}/{} circuits for width {}",
185                    circuit_idx, self.params.circuits_per_width, width
186                );
187            }
188        }
189
190        // Calculate success probability
191        let success_count = heavy_output_probs
192            .iter()
193            .filter(|&&prob| prob >= self.params.heavy_output_threshold)
194            .count();
195        let success_probability = success_count as f64 / heavy_output_probs.len() as f64;
196
197        // Statistical test
198        let required_success_prob = self.get_required_success_probability(width)?;
199        let passed = success_probability >= required_success_prob;
200
201        // Calculate quantum volume
202        let quantum_volume = if passed { 1 << width } else { 0 };
203
204        // Confidence calculation
205        let confidence = self.calculate_confidence(&heavy_output_probs, required_success_prob)?;
206
207        stats.total_time_s = start_time.elapsed().as_secs_f64();
208        stats.memory_usage_bytes = self.estimate_memory_usage(width);
209        stats.avg_fidelity =
210            heavy_output_probs.iter().sum::<f64>() / heavy_output_probs.len() as f64;
211
212        Ok(QuantumVolumeResult {
213            quantum_volume,
214            width,
215            depth,
216            num_circuits: self.params.circuits_per_width,
217            threshold: required_success_prob,
218            success_probability,
219            heavy_output_probs,
220            confidence,
221            passed,
222            stats,
223        })
224    }
225
226    /// Generate random quantum volume circuit
227    fn generate_qv_circuit(&mut self, width: usize, depth: usize) -> Result<QVCircuit> {
228        if width < 2 {
229            return Err(SimulatorError::InvalidInput(
230                "Quantum volume requires at least 2 qubits".to_string(),
231            ));
232        }
233
234        let mut gates = Vec::new();
235        let mut available_qubits: Vec<usize> = (0..width).collect();
236
237        // Generate random SU(4) gates for each layer
238        for layer in 0..depth {
239            // Randomly partition qubits into pairs
240            available_qubits.shuffle(&mut self.rng);
241
242            // Add SU(4) gates for each pair
243            for pair_idx in 0..(width / 2) {
244                let qubit1 = available_qubits[2 * pair_idx];
245                let qubit2 = available_qubits[2 * pair_idx + 1];
246
247                let gate = QVGate {
248                    qubits: [qubit1, qubit2],
249                    matrix: self.generate_random_su4()?,
250                    layer,
251                };
252
253                gates.push(gate);
254            }
255        }
256
257        // Generate random permutation
258        let mut permutation: Vec<usize> = (0..width).collect();
259        permutation.shuffle(&mut self.rng);
260
261        // Simulate ideal circuit to get amplitudes
262        let ideal_amplitudes = self.simulate_ideal_circuit(width, &gates, &permutation)?;
263
264        // Calculate heavy output threshold and heavy outputs
265        let heavy_threshold = self.calculate_heavy_threshold(&ideal_amplitudes);
266        let heavy_outputs = self.find_heavy_outputs(&ideal_amplitudes, heavy_threshold);
267
268        Ok(QVCircuit {
269            width,
270            depth,
271            gates,
272            permutation,
273            ideal_amplitudes,
274            heavy_threshold,
275            heavy_outputs,
276        })
277    }
278
279    /// Generate random SU(4) matrix
280    fn generate_random_su4(&mut self) -> Result<Array2<Complex64>> {
281        // Generate random 4x4 unitary matrix
282        // Using Hurwitz parametrization for SU(4)
283
284        // Generate 15 random parameters (SU(4) has 15 real parameters)
285        let mut params = Vec::with_capacity(15);
286        for _ in 0..15 {
287            params.push(self.rng.gen::<f64>() * 2.0 * std::f64::consts::PI);
288        }
289
290        // Construct SU(4) matrix using parametrization
291        // This is a simplified construction - full implementation would use
292        // proper SU(4) parametrization
293        let mut matrix = Array2::zeros((4, 4));
294
295        // Simplified random unitary construction
296        for i in 0..4 {
297            for j in 0..4 {
298                let real = self.rng.gen::<f64>() - 0.5;
299                let imag = self.rng.gen::<f64>() - 0.5;
300                matrix[[i, j]] = Complex64::new(real, imag);
301            }
302        }
303
304        // Gram-Schmidt orthogonalization to make it unitary
305        self.gram_schmidt_orthogonalize(&mut matrix)?;
306
307        Ok(matrix)
308    }
309
310    /// Gram-Schmidt orthogonalization for making matrix unitary
311    fn gram_schmidt_orthogonalize(&self, matrix: &mut Array2<Complex64>) -> Result<()> {
312        let n = matrix.nrows();
313
314        // Orthogonalize columns
315        for j in 0..n {
316            // Normalize column j
317            let mut norm = 0.0;
318            for i in 0..n {
319                norm += matrix[[i, j]].norm_sqr();
320            }
321            norm = norm.sqrt();
322
323            if norm > 1e-12 {
324                for i in 0..n {
325                    matrix[[i, j]] /= norm;
326                }
327            }
328
329            // Orthogonalize remaining columns against column j
330            for k in (j + 1)..n {
331                let mut dot_product = Complex64::new(0.0, 0.0);
332                for i in 0..n {
333                    dot_product += matrix[[i, j]].conj() * matrix[[i, k]];
334                }
335
336                let column_j_values: Vec<Complex64> = (0..n).map(|i| matrix[[i, j]]).collect();
337                for i in 0..n {
338                    matrix[[i, k]] -= dot_product * column_j_values[i];
339                }
340            }
341        }
342
343        Ok(())
344    }
345
346    /// Simulate ideal quantum volume circuit
347    fn simulate_ideal_circuit(
348        &self,
349        width: usize,
350        gates: &[QVGate],
351        permutation: &[usize],
352    ) -> Result<Array1<Complex64>> {
353        let dim = 1 << width;
354        let mut state = Array1::zeros(dim);
355        state[0] = Complex64::new(1.0, 0.0); // |0...0⟩
356
357        // Apply gates layer by layer
358        for layer in 0..=gates.iter().map(|g| g.layer).max().unwrap_or(0) {
359            let layer_gates: Vec<&QVGate> = gates.iter().filter(|g| g.layer == layer).collect();
360
361            // Apply all gates in this layer (they commute since they act on disjoint qubits)
362            for gate in layer_gates {
363                self.apply_two_qubit_gate(
364                    &mut state,
365                    width,
366                    &gate.matrix,
367                    gate.qubits[0],
368                    gate.qubits[1],
369                )?;
370            }
371        }
372
373        // Apply final permutation
374        state = self.apply_permutation(&state, width, permutation)?;
375
376        Ok(state)
377    }
378
379    /// Apply two-qubit gate to state vector
380    fn apply_two_qubit_gate(
381        &self,
382        state: &mut Array1<Complex64>,
383        width: usize,
384        gate_matrix: &Array2<Complex64>,
385        qubit1: usize,
386        qubit2: usize,
387    ) -> Result<()> {
388        let dim = 1 << width;
389        let mut new_state = Array1::zeros(dim);
390
391        for basis_state in 0..dim {
392            let bit1 = (basis_state >> (width - 1 - qubit1)) & 1;
393            let bit2 = (basis_state >> (width - 1 - qubit2)) & 1;
394            let two_qubit_state = (bit1 << 1) | bit2;
395
396            for target_two_qubit in 0..4 {
397                let amplitude = gate_matrix[[target_two_qubit, two_qubit_state]];
398
399                if amplitude.norm() > 1e-12 {
400                    let target_bit1 = (target_two_qubit >> 1) & 1;
401                    let target_bit2 = target_two_qubit & 1;
402
403                    let mut target_basis = basis_state;
404
405                    // Update qubit1
406                    if target_bit1 == 1 {
407                        target_basis |= 1 << (width - 1 - qubit1);
408                    } else {
409                        target_basis &= !(1 << (width - 1 - qubit1));
410                    }
411
412                    // Update qubit2
413                    if target_bit2 == 1 {
414                        target_basis |= 1 << (width - 1 - qubit2);
415                    } else {
416                        target_basis &= !(1 << (width - 1 - qubit2));
417                    }
418
419                    new_state[target_basis] += amplitude * state[basis_state];
420                }
421            }
422        }
423
424        *state = new_state;
425        Ok(())
426    }
427
428    /// Apply permutation to state vector
429    fn apply_permutation(
430        &self,
431        state: &Array1<Complex64>,
432        width: usize,
433        permutation: &[usize],
434    ) -> Result<Array1<Complex64>> {
435        let dim = 1 << width;
436        let mut new_state = Array1::zeros(dim);
437
438        for basis_state in 0..dim {
439            let mut permuted_state = 0;
440
441            for (new_pos, &old_pos) in permutation.iter().enumerate() {
442                let bit = (basis_state >> (width - 1 - old_pos)) & 1;
443                permuted_state |= bit << (width - 1 - new_pos);
444            }
445
446            new_state[permuted_state] = state[basis_state];
447        }
448
449        Ok(new_state)
450    }
451
452    /// Calculate heavy output threshold (median probability)
453    fn calculate_heavy_threshold(&self, amplitudes: &Array1<Complex64>) -> f64 {
454        let mut probabilities: Vec<f64> = amplitudes
455            .iter()
456            .map(scirs2_core::Complex::norm_sqr)
457            .collect();
458        probabilities.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
459
460        let median_idx = probabilities.len() / 2;
461        probabilities[median_idx]
462    }
463
464    /// Find heavy outputs (above threshold)
465    fn find_heavy_outputs(&self, amplitudes: &Array1<Complex64>, threshold: f64) -> Vec<usize> {
466        amplitudes
467            .iter()
468            .enumerate()
469            .filter_map(|(idx, amp)| {
470                if amp.norm_sqr() > threshold {
471                    Some(idx)
472                } else {
473                    None
474                }
475            })
476            .collect()
477    }
478
479    /// Calculate heavy output probability for a circuit
480    fn calculate_heavy_output_probability(&self, circuit: &QVCircuit) -> Result<f64> {
481        // Sum probabilities of all heavy outputs
482        let heavy_prob: f64 = circuit
483            .heavy_outputs
484            .iter()
485            .map(|&idx| circuit.ideal_amplitudes[idx].norm_sqr())
486            .sum();
487
488        Ok(heavy_prob)
489    }
490
491    /// Get required success probability for statistical test
492    fn get_required_success_probability(&self, width: usize) -> Result<f64> {
493        // For quantum volume, we typically require 2/3 probability with high confidence
494        // The exact threshold depends on the number of circuits and confidence level
495
496        let baseline_threshold = 2.0 / 3.0;
497
498        // Adjust for statistical fluctuations based on number of circuits
499        let n = self.params.circuits_per_width as f64;
500        let confidence = self.params.confidence_level;
501
502        // Use normal approximation for large n
503        let z_score = match confidence {
504            x if x >= 0.99 => 2.576,
505            x if x >= 0.95 => 1.96,
506            x if x >= 0.90 => 1.645,
507            _ => 1.96,
508        };
509
510        let standard_error = (baseline_threshold * (1.0 - baseline_threshold) / n).sqrt();
511        let adjusted_threshold = baseline_threshold - z_score * standard_error;
512
513        Ok(adjusted_threshold.max(0.5)) // At least better than random
514    }
515
516    /// Calculate confidence in the result
517    fn calculate_confidence(&self, heavy_probs: &[f64], threshold: f64) -> Result<f64> {
518        let n = heavy_probs.len() as f64;
519        let success_count = heavy_probs.iter().filter(|&&p| p >= threshold).count() as f64;
520        let success_rate = success_count / n;
521
522        // Binomial confidence interval
523        let p = success_rate;
524        let standard_error = (p * (1.0 - p) / n).sqrt();
525
526        // Return confidence that we're above threshold
527        if success_rate > threshold {
528            let z_score = (success_rate - threshold) / standard_error;
529            Ok(self.normal_cdf(z_score))
530        } else {
531            Ok(0.0)
532        }
533    }
534
535    /// Normal CDF approximation
536    fn normal_cdf(&self, x: f64) -> f64 {
537        0.5 * (1.0 + self.erf(x / 2.0_f64.sqrt()))
538    }
539
540    /// Error function approximation
541    fn erf(&self, x: f64) -> f64 {
542        // Abramowitz and Stegun approximation
543        let a1 = 0.254_829_592;
544        let a2 = -0.284_496_736;
545        let a3 = 1.421_413_741;
546        let a4 = -1.453_152_027;
547        let a5 = 1.061_405_429;
548        let p = 0.327_591_1;
549
550        let sign = if x < 0.0 { -1.0 } else { 1.0 };
551        let x = x.abs();
552
553        let t = 1.0 / (1.0 + p * x);
554        let y = ((a5 * t + a4).mul_add(t, a3).mul_add(t, a2).mul_add(t, a1) * t)
555            .mul_add(-(-x * x).exp(), 1.0);
556
557        sign * y
558    }
559
560    /// Estimate memory usage
561    const fn estimate_memory_usage(&self, width: usize) -> usize {
562        let state_vector_size = (1 << width) * std::mem::size_of::<Complex64>();
563        let circuit_storage = 1000 * std::mem::size_of::<QVGate>(); // Rough estimate
564        state_vector_size + circuit_storage
565    }
566
567    /// Find maximum quantum volume by testing increasing widths
568    pub fn find_maximum_quantum_volume(&mut self) -> Result<Vec<QuantumVolumeResult>> {
569        let mut results = Vec::new();
570
571        for width in 2..=self.params.max_width {
572            println!("Testing quantum volume for width {width}");
573
574            let result = self.calculate_quantum_volume(width)?;
575
576            println!(
577                "Width {}: QV = {}, Success Prob = {:.3}, Passed = {}",
578                width, result.quantum_volume, result.success_probability, result.passed
579            );
580
581            results.push(result.clone());
582
583            // If we fail, we've found the maximum quantum volume
584            if !result.passed {
585                break;
586            }
587        }
588
589        Ok(results)
590    }
591}
592
593/// Benchmark quantum volume calculation
594pub fn benchmark_quantum_volume(max_width: usize) -> Result<Vec<QuantumVolumeResult>> {
595    let params = QVParams {
596        circuits_per_width: 20, // Reduced for benchmarking
597        max_width,
598        ..Default::default()
599    };
600
601    let mut calculator = QuantumVolumeCalculator::new(params);
602    calculator.find_maximum_quantum_volume()
603}
604
605/// Calculate quantum volume for specific parameters
606pub fn calculate_quantum_volume_with_params(
607    width: usize,
608    circuits: usize,
609    seed: Option<u64>,
610) -> Result<QuantumVolumeResult> {
611    let params = QVParams {
612        circuits_per_width: circuits,
613        max_width: width,
614        seed,
615        ..Default::default()
616    };
617
618    let mut calculator = QuantumVolumeCalculator::new(params);
619    calculator.calculate_quantum_volume(width)
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625
626    #[test]
627    fn test_qv_calculator_creation() {
628        let params = QVParams::default();
629        let calculator = QuantumVolumeCalculator::new(params);
630        assert!(calculator.params.circuits_per_width > 0);
631    }
632
633    #[test]
634    fn test_random_su4_generation() {
635        let params = QVParams::default();
636        let mut calculator = QuantumVolumeCalculator::new(params);
637
638        let matrix = calculator
639            .generate_random_su4()
640            .expect("Failed to generate random SU(4) matrix");
641        assert_eq!(matrix.shape(), [4, 4]);
642
643        // Check that it's approximately unitary (U†U ≈ I)
644        let matrix_dagger = matrix.t().mapv(|x| x.conj());
645        let product = matrix_dagger.dot(&matrix);
646
647        // Check diagonal elements are close to 1
648        for i in 0..4 {
649            assert!((product[[i, i]].re - 1.0).abs() < 1e-1);
650            assert!(product[[i, i]].im.abs() < 1e-1);
651        }
652    }
653
654    #[test]
655    fn test_qv_circuit_generation() {
656        let params = QVParams::default();
657        let mut calculator = QuantumVolumeCalculator::new(params);
658
659        let circuit = calculator
660            .generate_qv_circuit(4, 4)
661            .expect("Failed to generate QV circuit");
662        assert_eq!(circuit.width, 4);
663        assert_eq!(circuit.depth, 4);
664        assert!(!circuit.gates.is_empty());
665        assert_eq!(circuit.permutation.len(), 4);
666    }
667
668    #[test]
669    fn test_heavy_output_calculation() {
670        let amplitudes = Array1::from_vec(vec![
671            Complex64::new(0.6, 0.0),  // High probability
672            Complex64::new(0.3, 0.0),  // Medium probability
673            Complex64::new(0.1, 0.0),  // Low probability
674            Complex64::new(0.05, 0.0), // Very low probability
675        ]);
676
677        let params = QVParams::default();
678        let calculator = QuantumVolumeCalculator::new(params);
679
680        let threshold = calculator.calculate_heavy_threshold(&amplitudes);
681        let heavy_outputs = calculator.find_heavy_outputs(&amplitudes, threshold);
682
683        assert!(threshold > 0.0);
684        assert!(!heavy_outputs.is_empty());
685    }
686
687    #[test]
688    fn test_small_quantum_volume() {
689        let result = calculate_quantum_volume_with_params(3, 10, Some(42))
690            .expect("Failed to calculate quantum volume");
691
692        assert_eq!(result.width, 3);
693        assert_eq!(result.depth, 3);
694        assert_eq!(result.num_circuits, 10);
695        assert!(!result.heavy_output_probs.is_empty());
696    }
697
698    #[test]
699    fn test_permutation_application() {
700        let params = QVParams::default();
701        let calculator = QuantumVolumeCalculator::new(params);
702
703        let state = Array1::from_vec(vec![
704            Complex64::new(1.0, 0.0),
705            Complex64::new(0.0, 0.0),
706            Complex64::new(0.0, 0.0),
707            Complex64::new(0.0, 0.0),
708        ]);
709
710        let permutation = vec![1, 0]; // Swap qubits
711        let permuted = calculator
712            .apply_permutation(&state, 2, &permutation)
713            .expect("Failed to apply permutation");
714
715        // |00⟩ should become |00⟩ (no change for this state)
716        assert!((permuted[0].re - 1.0).abs() < 1e-10);
717    }
718}