quantrs2_core/
quantum_volume_tomography.rs

1//! Quantum Volume and Process Tomography
2//!
3//! This module implements quantum benchmarking and characterization protocols
4//! for evaluating quantum computer performance.
5//!
6//! ## Quantum Volume
7//! Quantum Volume (QV) is a holistic metric that captures the overall performance
8//! of a quantum computer, taking into account:
9//! - Number of qubits
10//! - Gate fidelity
11//! - Qubit connectivity
12//! - Error rates
13//! - Measurement quality
14//!
15//! ## Quantum Process Tomography
16//! QPT completely characterizes a quantum operation by reconstructing its
17//! process matrix (chi matrix) or Choi representation.
18
19use crate::{
20    error::{QuantRS2Error, QuantRS2Result},
21    gate::GateOp,
22    qubit::QubitId,
23};
24use scirs2_core::ndarray::{Array1, Array2, Array3, Array4};
25use scirs2_core::random::prelude::*;
26use scirs2_core::Complex64;
27use std::collections::HashMap;
28
29/// Quantum Volume Protocol
30///
31/// Measures the largest random square circuit (n×n) that can be executed
32/// reliably on a quantum computer.
33pub struct QuantumVolume {
34    /// Maximum number of qubits to test
35    pub max_qubits: usize,
36    /// Number of random circuits per qubit count
37    pub num_circuits: usize,
38    /// Number of shots per circuit
39    pub num_shots: usize,
40    /// Success threshold (heavy output probability)
41    pub success_threshold: f64,
42    /// Random number generator
43    rng: ThreadRng,
44}
45
46impl QuantumVolume {
47    /// Create a new quantum volume protocol
48    pub fn new(max_qubits: usize, num_circuits: usize, num_shots: usize) -> Self {
49        Self {
50            max_qubits,
51            num_circuits,
52            num_shots,
53            success_threshold: 2.0 / 3.0, // Standard QV threshold
54            rng: thread_rng(),
55        }
56    }
57
58    /// Run quantum volume protocol
59    ///
60    /// Returns the achieved quantum volume (largest successful n)
61    pub fn run<F>(&mut self, mut circuit_executor: F) -> QuantRS2Result<QuantumVolumeResult>
62    where
63        F: FnMut(&[Box<dyn GateOp>], usize) -> Vec<usize>, // Returns measured bitstrings
64    {
65        let mut results = HashMap::new();
66        let mut quantum_volume = 1;
67
68        for n_qubits in 1..=self.max_qubits {
69            let success_rate = self.test_quantum_volume(n_qubits, &mut circuit_executor)?;
70
71            results.insert(n_qubits, success_rate);
72
73            // Check if QV is achieved for this qubit count
74            if success_rate >= self.success_threshold {
75                quantum_volume = 1 << n_qubits; // 2^n
76            } else {
77                break; // Stop at first failure
78            }
79        }
80
81        Ok(QuantumVolumeResult {
82            quantum_volume,
83            success_rates: results,
84            max_qubits_tested: self.max_qubits,
85        })
86    }
87
88    /// Test quantum volume for a specific number of qubits
89    fn test_quantum_volume<F>(
90        &self,
91        n_qubits: usize,
92        circuit_executor: &mut F,
93    ) -> QuantRS2Result<f64>
94    where
95        F: FnMut(&[Box<dyn GateOp>], usize) -> Vec<usize>,
96    {
97        let mut successful_circuits = 0;
98
99        for _ in 0..self.num_circuits {
100            // Generate random model circuit
101            let (circuit, heavy_outputs) = self.generate_random_circuit(n_qubits)?;
102
103            // Execute circuit and collect measurements
104            let measurements = circuit_executor(&circuit, self.num_shots);
105
106            // Calculate heavy output probability
107            let hop = self.calculate_heavy_output_probability(&measurements, &heavy_outputs);
108
109            // Check if circuit passed (HOP > 2/3)
110            if hop > 2.0 / 3.0 {
111                successful_circuits += 1;
112            }
113        }
114
115        let success_rate = successful_circuits as f64 / self.num_circuits as f64;
116        Ok(success_rate)
117    }
118
119    /// Generate a random model circuit for quantum volume
120    ///
121    /// Returns the circuit and the set of heavy outputs (outputs with above-median probability)
122    fn generate_random_circuit(
123        &self,
124        n_qubits: usize,
125    ) -> QuantRS2Result<(Vec<Box<dyn GateOp>>, Vec<usize>)> {
126        // For quantum volume, we use depth = n_qubits
127        let depth = n_qubits;
128
129        // Placeholder: generate random SU(4) gates
130        // In a real implementation, this would generate random 2-qubit unitaries
131        let circuit = vec![];
132
133        // Simulate ideal circuit to find heavy outputs
134        let heavy_outputs = self.find_heavy_outputs(n_qubits, &circuit)?;
135
136        Ok((circuit, heavy_outputs))
137    }
138
139    /// Find heavy outputs (outputs with above-median probability)
140    fn find_heavy_outputs(
141        &self,
142        n_qubits: usize,
143        _circuit: &[Box<dyn GateOp>],
144    ) -> QuantRS2Result<Vec<usize>> {
145        // Simulate the circuit classically to find heavy outputs
146        // This is a simplified placeholder
147
148        let num_states = 1 << n_qubits;
149        let median_prob = 1.0 / (num_states as f64);
150
151        // In reality, we would:
152        // 1. Simulate the circuit
153        // 2. Calculate all outcome probabilities
154        // 3. Find those above median
155
156        // For now, return a placeholder (first half of bitstrings)
157        Ok((0..num_states / 2).collect())
158    }
159
160    /// Calculate heavy output probability
161    fn calculate_heavy_output_probability(
162        &self,
163        measurements: &[usize],
164        heavy_outputs: &[usize],
165    ) -> f64 {
166        let heavy_count = measurements
167            .iter()
168            .filter(|&&bitstring| heavy_outputs.contains(&bitstring))
169            .count();
170
171        heavy_count as f64 / measurements.len() as f64
172    }
173}
174
175/// Result of quantum volume protocol
176#[derive(Debug, Clone)]
177pub struct QuantumVolumeResult {
178    /// Achieved quantum volume (2^n)
179    pub quantum_volume: usize,
180    /// Success rates for each qubit count tested
181    pub success_rates: HashMap<usize, f64>,
182    /// Maximum number of qubits tested
183    pub max_qubits_tested: usize,
184}
185
186impl QuantumVolumeResult {
187    /// Get the number of qubits achieved
188    pub fn num_qubits_achieved(&self) -> usize {
189        (self.quantum_volume as f64).log2() as usize
190    }
191
192    /// Check if quantum volume was achieved for n qubits
193    pub fn is_qv_achieved(&self, n_qubits: usize) -> bool {
194        self.success_rates
195            .get(&n_qubits)
196            .is_some_and(|&rate| rate >= 2.0 / 3.0)
197    }
198}
199
200/// Quantum Process Tomography Protocol
201///
202/// Completely characterizes a quantum operation by measuring its action
203/// on a complete set of input states.
204pub struct QuantumProcessTomography {
205    /// Number of qubits in the process
206    pub num_qubits: usize,
207    /// Basis for state preparation (typically Pauli basis)
208    pub preparation_basis: Vec<String>,
209    /// Basis for measurement (typically Pauli basis)
210    pub measurement_basis: Vec<String>,
211}
212
213impl QuantumProcessTomography {
214    /// Create a new QPT protocol
215    pub fn new(num_qubits: usize) -> Self {
216        // Generate Pauli basis for preparation and measurement
217        let basis = Self::generate_pauli_basis(num_qubits);
218
219        Self {
220            num_qubits,
221            preparation_basis: basis.clone(),
222            measurement_basis: basis,
223        }
224    }
225
226    /// Generate Pauli basis strings for n qubits
227    fn generate_pauli_basis(n_qubits: usize) -> Vec<String> {
228        let paulis = ['I', 'X', 'Y', 'Z'];
229        let basis_size = 4_usize.pow(n_qubits as u32);
230
231        let mut basis = Vec::with_capacity(basis_size);
232
233        for i in 0..basis_size {
234            let mut pauli_string = String::with_capacity(n_qubits);
235            let mut idx = i;
236
237            for _ in 0..n_qubits {
238                pauli_string.push(paulis[idx % 4]);
239                idx /= 4;
240            }
241
242            basis.push(pauli_string);
243        }
244
245        basis
246    }
247
248    /// Run quantum process tomography
249    ///
250    /// Returns the reconstructed process matrix (chi matrix)
251    pub fn run<F>(&self, mut apply_process: F) -> QuantRS2Result<ProcessMatrix>
252    where
253        F: FnMut(&str, &str) -> Complex64, // (prep_basis, meas_basis) -> expectation value
254    {
255        let dim = 1 << self.num_qubits;
256        let basis_size = self.preparation_basis.len();
257
258        // Allocate chi matrix
259        let mut chi_matrix = Array2::zeros((basis_size, basis_size));
260
261        // Perform tomography: measure E[P_out | P_in] for all Pauli pairs
262        for (i, prep) in self.preparation_basis.iter().enumerate() {
263            for (j, meas) in self.measurement_basis.iter().enumerate() {
264                let expectation = apply_process(prep, meas);
265                chi_matrix[[i, j]] = expectation;
266            }
267        }
268
269        // Post-process to enforce physicality (positive semidefinite, trace-preserving)
270        let chi_matrix = self.enforce_physicality(chi_matrix)?;
271
272        Ok(ProcessMatrix {
273            chi_matrix,
274            num_qubits: self.num_qubits,
275            basis_labels: self.preparation_basis.clone(),
276        })
277    }
278
279    /// Enforce physicality constraints on the process matrix
280    fn enforce_physicality(&self, chi: Array2<Complex64>) -> QuantRS2Result<Array2<Complex64>> {
281        // Simplified physicality enforcement
282        // In practice, this would use:
283        // 1. Maximum likelihood estimation
284        // 2. Projection onto physical process matrices
285        // 3. Constrained optimization
286
287        // For now, just normalize
288        let trace: Complex64 = chi.diag().iter().sum();
289        let normalized = if trace.norm() > 1e-10 {
290            &chi / trace
291        } else {
292            chi
293        };
294
295        Ok(normalized)
296    }
297
298    /// Compute process fidelity between two process matrices
299    pub fn process_fidelity(chi1: &Array2<Complex64>, chi2: &Array2<Complex64>) -> f64 {
300        // F_proc = Tr(chi1^† chi2)
301        let product = chi1.t().mapv(|x| x.conj()).dot(chi2);
302        let trace: Complex64 = product.diag().iter().sum();
303        trace.norm()
304    }
305
306    /// Compute average gate fidelity from process matrix
307    pub fn average_gate_fidelity(
308        &self,
309        chi: &Array2<Complex64>,
310        ideal_chi: &Array2<Complex64>,
311    ) -> f64 {
312        let dim = 1 << self.num_qubits;
313        let d = dim as f64;
314
315        // F_avg = (d * F_proc + 1) / (d + 1)
316        let f_proc = Self::process_fidelity(chi, ideal_chi);
317        (d * f_proc + 1.0) / (d + 1.0)
318    }
319}
320
321/// Reconstructed process matrix from QPT
322#[derive(Debug, Clone)]
323pub struct ProcessMatrix {
324    /// Chi matrix in Pauli basis
325    pub chi_matrix: Array2<Complex64>,
326    /// Number of qubits
327    pub num_qubits: usize,
328    /// Basis labels
329    pub basis_labels: Vec<String>,
330}
331
332impl ProcessMatrix {
333    /// Get the process matrix element for specific Pauli operators
334    pub fn get_element(&self, prep_pauli: &str, meas_pauli: &str) -> Option<Complex64> {
335        let i = self.basis_labels.iter().position(|s| s == prep_pauli)?;
336        let j = self.basis_labels.iter().position(|s| s == meas_pauli)?;
337        Some(self.chi_matrix[[i, j]])
338    }
339
340    /// Check if the process is trace-preserving
341    pub fn is_trace_preserving(&self, tolerance: f64) -> bool {
342        let trace: Complex64 = self.chi_matrix.diag().iter().sum();
343        (trace - Complex64::new(1.0, 0.0)).norm() < tolerance
344    }
345
346    /// Check if the process is completely positive
347    pub fn is_completely_positive(&self, tolerance: f64) -> bool {
348        // Simplified check: chi should be positive semidefinite
349        // In practice, would compute eigenvalues
350
351        // For now, check diagonal elements are non-negative
352        self.chi_matrix.diag().iter().all(|&x| x.re >= -tolerance)
353    }
354
355    /// Compute the diamond norm distance to another process
356    pub fn diamond_distance(&self, other: &Self) -> QuantRS2Result<f64> {
357        if self.num_qubits != other.num_qubits {
358            return Err(QuantRS2Error::InvalidInput(
359                "Process matrices must have same dimension".to_string(),
360            ));
361        }
362
363        // Simplified diamond distance computation
364        // Full implementation requires semidefinite programming
365
366        // Approximate using Frobenius norm
367        let diff = &self.chi_matrix - &other.chi_matrix;
368        let frobenius_norm = diff.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
369
370        Ok(frobenius_norm)
371    }
372}
373
374/// Gate Set Tomography (GST)
375///
376/// More comprehensive than QPT, GST characterizes an entire gate set
377/// including state preparation and measurement errors.
378pub struct GateSetTomography {
379    /// Number of qubits
380    pub num_qubits: usize,
381    /// Gate set to characterize
382    pub gate_set: Vec<String>,
383    /// Maximum sequence length
384    pub max_length: usize,
385}
386
387impl GateSetTomography {
388    /// Create a new GST protocol
389    pub const fn new(num_qubits: usize, gate_set: Vec<String>, max_length: usize) -> Self {
390        Self {
391            num_qubits,
392            gate_set,
393            max_length,
394        }
395    }
396
397    /// Run gate set tomography
398    ///
399    /// This is a placeholder for the full GST algorithm
400    pub fn run<F>(&self, mut execute_sequence: F) -> QuantRS2Result<GateSetModel>
401    where
402        F: FnMut(&[&str]) -> f64, // Gate sequence -> measurement probability
403    {
404        // GST consists of three types of sequences:
405        // 1. Germ sequences (repeated short sequences)
406        // 2. Fiducial sequences (state prep and measurement)
407        // 3. Amplification sequences (repeated germs)
408
409        let germs = self.generate_germs();
410        let fiducials = self.generate_fiducials();
411
412        // Collect data from all sequences
413        let mut data = HashMap::new();
414
415        for prep_fiducial in &fiducials {
416            for germ in &germs {
417                for meas_fiducial in &fiducials {
418                    // Build amplified sequence
419                    for power in 1..=self.max_length {
420                        let mut sequence = Vec::new();
421
422                        // Prep fiducial
423                        sequence.extend_from_slice(prep_fiducial);
424
425                        // Repeated germ
426                        for _ in 0..power {
427                            sequence.extend_from_slice(germ);
428                        }
429
430                        // Measurement fiducial
431                        sequence.extend_from_slice(meas_fiducial);
432
433                        // Execute and collect data
434                        let probability = execute_sequence(&sequence);
435                        data.insert(sequence.clone(), probability);
436                    }
437                }
438            }
439        }
440
441        // Fit model to data using maximum likelihood estimation
442        let model = self.fit_model(&data)?;
443
444        Ok(model)
445    }
446
447    /// Generate germ sequences
448    fn generate_germs(&self) -> Vec<Vec<&str>> {
449        // Standard germs for single qubit: I, X, Y, XY, XYX
450        // This is a simplified set
451        vec![vec!["I"], vec!["X"], vec!["Y"], vec!["X", "Y"]]
452    }
453
454    /// Generate fiducial sequences
455    fn generate_fiducials(&self) -> Vec<Vec<&str>> {
456        // Standard fiducials for single qubit
457        vec![
458            vec!["I"],
459            vec!["X"],
460            vec!["Y"],
461            vec!["X", "X"], // -I
462        ]
463    }
464
465    /// Fit GST model to data
466    fn fit_model(&self, _data: &HashMap<Vec<&str>, f64>) -> QuantRS2Result<GateSetModel> {
467        // Placeholder: maximum likelihood estimation
468        // Real implementation would use iterative optimization
469
470        Ok(GateSetModel {
471            num_qubits: self.num_qubits,
472            gate_errors: HashMap::new(),
473            spam_errors: vec![],
474        })
475    }
476}
477
478/// GST model describing errors in gates and measurements
479#[derive(Debug, Clone)]
480pub struct GateSetModel {
481    /// Number of qubits
482    pub num_qubits: usize,
483    /// Error models for each gate
484    pub gate_errors: HashMap<String, Array2<Complex64>>,
485    /// State preparation and measurement (SPAM) errors
486    pub spam_errors: Vec<f64>,
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    #[test]
494    fn test_quantum_volume_result() {
495        let mut result = QuantumVolumeResult {
496            quantum_volume: 16,
497            success_rates: HashMap::new(),
498            max_qubits_tested: 5,
499        };
500
501        result.success_rates.insert(1, 0.95);
502        result.success_rates.insert(2, 0.85);
503        result.success_rates.insert(3, 0.75);
504        result.success_rates.insert(4, 0.70);
505
506        assert_eq!(result.num_qubits_achieved(), 4);
507        assert!(result.is_qv_achieved(1));
508        assert!(result.is_qv_achieved(2));
509        assert!(result.is_qv_achieved(3));
510        assert!(result.is_qv_achieved(4));
511
512        println!("Quantum Volume: {}", result.quantum_volume);
513    }
514
515    #[test]
516    fn test_pauli_basis_generation() {
517        let basis = QuantumProcessTomography::generate_pauli_basis(1);
518        assert_eq!(basis.len(), 4);
519        assert!(basis.contains(&"I".to_string()));
520        assert!(basis.contains(&"X".to_string()));
521        assert!(basis.contains(&"Y".to_string()));
522        assert!(basis.contains(&"Z".to_string()));
523
524        let basis_2q = QuantumProcessTomography::generate_pauli_basis(2);
525        assert_eq!(basis_2q.len(), 16);
526    }
527
528    #[test]
529    fn test_process_matrix() {
530        let qpt = QuantumProcessTomography::new(1);
531
532        // Mock process: identity
533        let mock_process = |_prep: &str, meas: &str| {
534            if meas == "I" {
535                Complex64::new(1.0, 0.0)
536            } else {
537                Complex64::new(0.0, 0.0)
538            }
539        };
540
541        let result = qpt
542            .run(mock_process)
543            .expect("QPT run should succeed with mock process");
544
545        assert_eq!(result.num_qubits, 1);
546        assert!(result.is_trace_preserving(1e-6));
547        println!("Process matrix shape: {:?}", result.chi_matrix.dim());
548    }
549
550    #[test]
551    fn test_process_fidelity() {
552        let dim = 4;
553        let identity = Array2::eye(dim);
554        let noisy = &identity * Complex64::new(0.95, 0.0);
555
556        let fidelity = QuantumProcessTomography::process_fidelity(&identity, &noisy);
557
558        // Fidelity is the trace of the product, which for scaled identity is just the scaling factor times dim
559        // So for 0.95 * I with dim=4, we expect fidelity = 0.95 * 4 = 3.8
560        println!("Process fidelity: {}", fidelity);
561
562        // The fidelity should be proportional to the scaling
563        assert!(fidelity > 0.0 && fidelity <= dim as f64);
564    }
565
566    #[test]
567    fn test_gst_initialization() {
568        let gate_set = vec!["I".to_string(), "X".to_string(), "H".to_string()];
569        let gst = GateSetTomography::new(1, gate_set, 10);
570
571        assert_eq!(gst.num_qubits, 1);
572        assert_eq!(gst.max_length, 10);
573
574        let germs = gst.generate_germs();
575        assert!(!germs.is_empty());
576
577        let fiducials = gst.generate_fiducials();
578        assert!(!fiducials.is_empty());
579    }
580}