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