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            .map(|&rate| rate >= 2.0 / 3.0)
197            .unwrap_or(false)
198    }
199}
200
201/// Quantum Process Tomography Protocol
202///
203/// Completely characterizes a quantum operation by measuring its action
204/// on a complete set of input states.
205pub struct QuantumProcessTomography {
206    /// Number of qubits in the process
207    pub num_qubits: usize,
208    /// Basis for state preparation (typically Pauli basis)
209    pub preparation_basis: Vec<String>,
210    /// Basis for measurement (typically Pauli basis)
211    pub measurement_basis: Vec<String>,
212}
213
214impl QuantumProcessTomography {
215    /// Create a new QPT protocol
216    pub fn new(num_qubits: usize) -> Self {
217        // Generate Pauli basis for preparation and measurement
218        let basis = Self::generate_pauli_basis(num_qubits);
219
220        Self {
221            num_qubits,
222            preparation_basis: basis.clone(),
223            measurement_basis: basis,
224        }
225    }
226
227    /// Generate Pauli basis strings for n qubits
228    fn generate_pauli_basis(n_qubits: usize) -> Vec<String> {
229        let paulis = ['I', 'X', 'Y', 'Z'];
230        let basis_size = 4_usize.pow(n_qubits as u32);
231
232        let mut basis = Vec::with_capacity(basis_size);
233
234        for i in 0..basis_size {
235            let mut pauli_string = String::with_capacity(n_qubits);
236            let mut idx = i;
237
238            for _ in 0..n_qubits {
239                pauli_string.push(paulis[idx % 4]);
240                idx /= 4;
241            }
242
243            basis.push(pauli_string);
244        }
245
246        basis
247    }
248
249    /// Run quantum process tomography
250    ///
251    /// Returns the reconstructed process matrix (chi matrix)
252    pub fn run<F>(&self, mut apply_process: F) -> QuantRS2Result<ProcessMatrix>
253    where
254        F: FnMut(&str, &str) -> Complex64, // (prep_basis, meas_basis) -> expectation value
255    {
256        let dim = 1 << self.num_qubits;
257        let basis_size = self.preparation_basis.len();
258
259        // Allocate chi matrix
260        let mut chi_matrix = Array2::zeros((basis_size, basis_size));
261
262        // Perform tomography: measure E[P_out | P_in] for all Pauli pairs
263        for (i, prep) in self.preparation_basis.iter().enumerate() {
264            for (j, meas) in self.measurement_basis.iter().enumerate() {
265                let expectation = apply_process(prep, meas);
266                chi_matrix[[i, j]] = expectation;
267            }
268        }
269
270        // Post-process to enforce physicality (positive semidefinite, trace-preserving)
271        let chi_matrix = self.enforce_physicality(chi_matrix)?;
272
273        Ok(ProcessMatrix {
274            chi_matrix,
275            num_qubits: self.num_qubits,
276            basis_labels: self.preparation_basis.clone(),
277        })
278    }
279
280    /// Enforce physicality constraints on the process matrix
281    fn enforce_physicality(&self, chi: Array2<Complex64>) -> QuantRS2Result<Array2<Complex64>> {
282        // Simplified physicality enforcement
283        // In practice, this would use:
284        // 1. Maximum likelihood estimation
285        // 2. Projection onto physical process matrices
286        // 3. Constrained optimization
287
288        // For now, just normalize
289        let trace: Complex64 = chi.diag().iter().sum();
290        let normalized = if trace.norm() > 1e-10 {
291            &chi / trace
292        } else {
293            chi.clone()
294        };
295
296        Ok(normalized)
297    }
298
299    /// Compute process fidelity between two process matrices
300    pub fn process_fidelity(chi1: &Array2<Complex64>, chi2: &Array2<Complex64>) -> f64 {
301        // F_proc = Tr(chi1^† chi2)
302        let product = chi1.t().mapv(|x| x.conj()).dot(chi2);
303        let trace: Complex64 = product.diag().iter().sum();
304        trace.norm()
305    }
306
307    /// Compute average gate fidelity from process matrix
308    pub fn average_gate_fidelity(
309        &self,
310        chi: &Array2<Complex64>,
311        ideal_chi: &Array2<Complex64>,
312    ) -> f64 {
313        let dim = 1 << self.num_qubits;
314        let d = dim as f64;
315
316        // F_avg = (d * F_proc + 1) / (d + 1)
317        let f_proc = Self::process_fidelity(chi, ideal_chi);
318        (d * f_proc + 1.0) / (d + 1.0)
319    }
320}
321
322/// Reconstructed process matrix from QPT
323#[derive(Debug, Clone)]
324pub struct ProcessMatrix {
325    /// Chi matrix in Pauli basis
326    pub chi_matrix: Array2<Complex64>,
327    /// Number of qubits
328    pub num_qubits: usize,
329    /// Basis labels
330    pub basis_labels: Vec<String>,
331}
332
333impl ProcessMatrix {
334    /// Get the process matrix element for specific Pauli operators
335    pub fn get_element(&self, prep_pauli: &str, meas_pauli: &str) -> Option<Complex64> {
336        let i = self.basis_labels.iter().position(|s| s == prep_pauli)?;
337        let j = self.basis_labels.iter().position(|s| s == meas_pauli)?;
338        Some(self.chi_matrix[[i, j]])
339    }
340
341    /// Check if the process is trace-preserving
342    pub fn is_trace_preserving(&self, tolerance: f64) -> bool {
343        let trace: Complex64 = self.chi_matrix.diag().iter().sum();
344        (trace - Complex64::new(1.0, 0.0)).norm() < tolerance
345    }
346
347    /// Check if the process is completely positive
348    pub fn is_completely_positive(&self, tolerance: f64) -> bool {
349        // Simplified check: chi should be positive semidefinite
350        // In practice, would compute eigenvalues
351
352        // For now, check diagonal elements are non-negative
353        self.chi_matrix.diag().iter().all(|&x| x.re >= -tolerance)
354    }
355
356    /// Compute the diamond norm distance to another process
357    pub fn diamond_distance(&self, other: &ProcessMatrix) -> QuantRS2Result<f64> {
358        if self.num_qubits != other.num_qubits {
359            return Err(QuantRS2Error::InvalidInput(
360                "Process matrices must have same dimension".to_string(),
361            ));
362        }
363
364        // Simplified diamond distance computation
365        // Full implementation requires semidefinite programming
366
367        // Approximate using Frobenius norm
368        let diff = &self.chi_matrix - &other.chi_matrix;
369        let frobenius_norm = diff.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
370
371        Ok(frobenius_norm)
372    }
373}
374
375/// Gate Set Tomography (GST)
376///
377/// More comprehensive than QPT, GST characterizes an entire gate set
378/// including state preparation and measurement errors.
379pub struct GateSetTomography {
380    /// Number of qubits
381    pub num_qubits: usize,
382    /// Gate set to characterize
383    pub gate_set: Vec<String>,
384    /// Maximum sequence length
385    pub max_length: usize,
386}
387
388impl GateSetTomography {
389    /// Create a new GST protocol
390    pub fn new(num_qubits: usize, gate_set: Vec<String>, max_length: usize) -> Self {
391        Self {
392            num_qubits,
393            gate_set,
394            max_length,
395        }
396    }
397
398    /// Run gate set tomography
399    ///
400    /// This is a placeholder for the full GST algorithm
401    pub fn run<F>(&self, mut execute_sequence: F) -> QuantRS2Result<GateSetModel>
402    where
403        F: FnMut(&[&str]) -> f64, // Gate sequence -> measurement probability
404    {
405        // GST consists of three types of sequences:
406        // 1. Germ sequences (repeated short sequences)
407        // 2. Fiducial sequences (state prep and measurement)
408        // 3. Amplification sequences (repeated germs)
409
410        let germs = self.generate_germs();
411        let fiducials = self.generate_fiducials();
412
413        // Collect data from all sequences
414        let mut data = HashMap::new();
415
416        for prep_fiducial in &fiducials {
417            for germ in &germs {
418                for meas_fiducial in &fiducials {
419                    // Build amplified sequence
420                    for power in 1..=self.max_length {
421                        let mut sequence = Vec::new();
422
423                        // Prep fiducial
424                        sequence.extend_from_slice(prep_fiducial);
425
426                        // Repeated germ
427                        for _ in 0..power {
428                            sequence.extend_from_slice(germ);
429                        }
430
431                        // Measurement fiducial
432                        sequence.extend_from_slice(meas_fiducial);
433
434                        // Execute and collect data
435                        let probability = execute_sequence(&sequence);
436                        data.insert(sequence.clone(), probability);
437                    }
438                }
439            }
440        }
441
442        // Fit model to data using maximum likelihood estimation
443        let model = self.fit_model(&data)?;
444
445        Ok(model)
446    }
447
448    /// Generate germ sequences
449    fn generate_germs(&self) -> Vec<Vec<&str>> {
450        // Standard germs for single qubit: I, X, Y, XY, XYX
451        // This is a simplified set
452        vec![vec!["I"], vec!["X"], vec!["Y"], vec!["X", "Y"]]
453    }
454
455    /// Generate fiducial sequences
456    fn generate_fiducials(&self) -> Vec<Vec<&str>> {
457        // Standard fiducials for single qubit
458        vec![
459            vec!["I"],
460            vec!["X"],
461            vec!["Y"],
462            vec!["X", "X"], // -I
463        ]
464    }
465
466    /// Fit GST model to data
467    fn fit_model(&self, _data: &HashMap<Vec<&str>, f64>) -> QuantRS2Result<GateSetModel> {
468        // Placeholder: maximum likelihood estimation
469        // Real implementation would use iterative optimization
470
471        Ok(GateSetModel {
472            num_qubits: self.num_qubits,
473            gate_errors: HashMap::new(),
474            spam_errors: vec![],
475        })
476    }
477}
478
479/// GST model describing errors in gates and measurements
480#[derive(Debug, Clone)]
481pub struct GateSetModel {
482    /// Number of qubits
483    pub num_qubits: usize,
484    /// Error models for each gate
485    pub gate_errors: HashMap<String, Array2<Complex64>>,
486    /// State preparation and measurement (SPAM) errors
487    pub spam_errors: Vec<f64>,
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    #[test]
495    fn test_quantum_volume_result() {
496        let mut result = QuantumVolumeResult {
497            quantum_volume: 16,
498            success_rates: HashMap::new(),
499            max_qubits_tested: 5,
500        };
501
502        result.success_rates.insert(1, 0.95);
503        result.success_rates.insert(2, 0.85);
504        result.success_rates.insert(3, 0.75);
505        result.success_rates.insert(4, 0.70);
506
507        assert_eq!(result.num_qubits_achieved(), 4);
508        assert!(result.is_qv_achieved(1));
509        assert!(result.is_qv_achieved(2));
510        assert!(result.is_qv_achieved(3));
511        assert!(result.is_qv_achieved(4));
512
513        println!("Quantum Volume: {}", result.quantum_volume);
514    }
515
516    #[test]
517    fn test_pauli_basis_generation() {
518        let basis = QuantumProcessTomography::generate_pauli_basis(1);
519        assert_eq!(basis.len(), 4);
520        assert!(basis.contains(&"I".to_string()));
521        assert!(basis.contains(&"X".to_string()));
522        assert!(basis.contains(&"Y".to_string()));
523        assert!(basis.contains(&"Z".to_string()));
524
525        let basis_2q = QuantumProcessTomography::generate_pauli_basis(2);
526        assert_eq!(basis_2q.len(), 16);
527    }
528
529    #[test]
530    fn test_process_matrix() {
531        let qpt = QuantumProcessTomography::new(1);
532
533        // Mock process: identity
534        let mock_process = |_prep: &str, meas: &str| {
535            if meas == "I" {
536                Complex64::new(1.0, 0.0)
537            } else {
538                Complex64::new(0.0, 0.0)
539            }
540        };
541
542        let result = qpt.run(mock_process).unwrap();
543
544        assert_eq!(result.num_qubits, 1);
545        assert!(result.is_trace_preserving(1e-6));
546        println!("Process matrix shape: {:?}", result.chi_matrix.dim());
547    }
548
549    #[test]
550    fn test_process_fidelity() {
551        let dim = 4;
552        let identity = Array2::eye(dim);
553        let noisy = &identity * Complex64::new(0.95, 0.0);
554
555        let fidelity = QuantumProcessTomography::process_fidelity(&identity, &noisy);
556
557        // Fidelity is the trace of the product, which for scaled identity is just the scaling factor times dim
558        // So for 0.95 * I with dim=4, we expect fidelity = 0.95 * 4 = 3.8
559        println!("Process fidelity: {}", fidelity);
560
561        // The fidelity should be proportional to the scaling
562        assert!(fidelity > 0.0 && fidelity <= dim as f64);
563    }
564
565    #[test]
566    fn test_gst_initialization() {
567        let gate_set = vec!["I".to_string(), "X".to_string(), "H".to_string()];
568        let gst = GateSetTomography::new(1, gate_set, 10);
569
570        assert_eq!(gst.num_qubits, 1);
571        assert_eq!(gst.max_length, 10);
572
573        let germs = gst.generate_germs();
574        assert!(!germs.is_empty());
575
576        let fiducials = gst.generate_fiducials();
577        assert!(!fiducials.is_empty());
578    }
579}