Skip to main content

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        &mut 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    /// Builds the standard quantum-volume "square" circuit: `depth = n_qubits`
122    /// layers, where each layer randomly permutes the qubits (Fisher-Yates),
123    /// pairs them up, and applies a Haar-random 2-qubit unitary (an SU(4) element)
124    /// to each pair. The circuit is then classically simulated to determine the
125    /// heavy outputs.
126    ///
127    /// Returns the circuit (a non-empty list of gates) and the set of heavy
128    /// outputs (computational-basis indices with strictly-above-median ideal
129    /// probability).
130    fn generate_random_circuit(
131        &mut self,
132        n_qubits: usize,
133    ) -> QuantRS2Result<(Vec<Box<dyn GateOp>>, Vec<usize>)> {
134        // For quantum volume, the model circuit depth equals the qubit count.
135        let depth = n_qubits;
136        let mut circuit: Vec<Box<dyn GateOp>> = Vec::new();
137
138        for _layer in 0..depth {
139            // Random permutation of the qubits, then pair adjacent entries.
140            let mut order: Vec<usize> = (0..n_qubits).collect();
141            self.shuffle(&mut order);
142
143            let num_pairs = n_qubits / 2;
144            for pair in 0..num_pairs {
145                let q1 = order[2 * pair];
146                let q2 = order[2 * pair + 1];
147                let unitary = self.random_su4()?;
148                circuit.push(Box::new(TwoQubitUnitaryGate::new(unitary, q1, q2)));
149            }
150        }
151
152        // Classically simulate the ideal circuit to find heavy outputs.
153        let heavy_outputs = self.find_heavy_outputs(n_qubits, &circuit)?;
154
155        Ok((circuit, heavy_outputs))
156    }
157
158    /// Find heavy outputs: computational-basis states whose ideal probability is
159    /// strictly above the median probability.
160    ///
161    /// The circuit is simulated to a full state vector starting from `|0...0>`,
162    /// every `|amplitude|^2` probability is computed, the median probability is
163    /// taken, and the indices with probability strictly greater than the median
164    /// are returned. This is the genuine quantum-volume heavy-output definition.
165    fn find_heavy_outputs(
166        &self,
167        n_qubits: usize,
168        circuit: &[Box<dyn GateOp>],
169    ) -> QuantRS2Result<Vec<usize>> {
170        let num_states = 1usize << n_qubits;
171
172        // Simulate the circuit to obtain the ideal probability distribution.
173        let state = simulate_circuit(circuit, n_qubits)?;
174        let probabilities: Vec<f64> = state.iter().map(|amp| amp.norm_sqr()).collect();
175
176        // Median of the probability list.
177        let mut sorted = probabilities.clone();
178        sorted.sort_by(|a, b| a.total_cmp(b));
179        let median = if num_states % 2 == 0 {
180            0.5 * (sorted[num_states / 2 - 1] + sorted[num_states / 2])
181        } else {
182            sorted[num_states / 2]
183        };
184
185        // Indices strictly above the median probability.
186        let heavy_outputs: Vec<usize> = probabilities
187            .iter()
188            .enumerate()
189            .filter(|(_, &p)| p > median)
190            .map(|(idx, _)| idx)
191            .collect();
192
193        Ok(heavy_outputs)
194    }
195
196    /// Fisher-Yates shuffle using the protocol's RNG.
197    fn shuffle(&mut self, slice: &mut [usize]) {
198        let n = slice.len();
199        if n < 2 {
200            return;
201        }
202        for i in 0..n - 1 {
203            let j = self.rng.random_range(i..n);
204            slice.swap(i, j);
205        }
206    }
207
208    /// Generate a Haar-random 4x4 unitary (an element of U(4), which contains the
209    /// SU(4) gates used by the quantum-volume protocol).
210    ///
211    /// A matrix with i.i.d. complex-Gaussian entries is orthonormalised via the
212    /// Gram-Schmidt process; the resulting unitary is Haar-distributed (up to the
213    /// usual phase convention), giving a genuine random 2-qubit gate rather than a
214    /// fixed or parameterised placeholder.
215    fn random_su4(&mut self) -> QuantRS2Result<Array2<Complex64>> {
216        let dim = 4;
217        let mut matrix = Array2::<Complex64>::zeros((dim, dim));
218        for i in 0..dim {
219            for j in 0..dim {
220                let (re, im) = self.standard_normal_pair();
221                matrix[[i, j]] = Complex64::new(re, im);
222            }
223        }
224        gram_schmidt_unitary(&matrix)
225    }
226
227    /// Draw a pair of independent standard-normal samples via the Box-Muller
228    /// transform, sourcing uniforms from the protocol's RNG.
229    fn standard_normal_pair(&mut self) -> (f64, f64) {
230        // Guard against log(0) by clamping u1 away from zero.
231        let u1: f64 = self.rng.random_range(f64::EPSILON..1.0);
232        let u2: f64 = self.rng.random_range(0.0..1.0);
233        let r = (-2.0 * u1.ln()).sqrt();
234        let theta = 2.0 * std::f64::consts::PI * u2;
235        (r * theta.cos(), r * theta.sin())
236    }
237
238    /// Calculate heavy output probability
239    fn calculate_heavy_output_probability(
240        &self,
241        measurements: &[usize],
242        heavy_outputs: &[usize],
243    ) -> f64 {
244        let heavy_count = measurements
245            .iter()
246            .filter(|&&bitstring| heavy_outputs.contains(&bitstring))
247            .count();
248
249        heavy_count as f64 / measurements.len() as f64
250    }
251}
252
253/// A general 2-qubit unitary gate wrapping an arbitrary 4x4 unitary matrix.
254///
255/// The matrix is stored in row-major order over the local 2-qubit basis
256/// `{|q1 q2>}` with `q1` the high-order local bit, consistent with the row-major
257/// `matrix()` convention used by the gates in [`crate::gate::functions`].
258#[derive(Debug, Clone)]
259struct TwoQubitUnitaryGate {
260    matrix: Array2<Complex64>,
261    qubit1: QubitId,
262    qubit2: QubitId,
263}
264
265impl TwoQubitUnitaryGate {
266    fn new(matrix: Array2<Complex64>, qubit1: usize, qubit2: usize) -> Self {
267        Self {
268            matrix,
269            qubit1: QubitId::new(qubit1 as u32),
270            qubit2: QubitId::new(qubit2 as u32),
271        }
272    }
273}
274
275impl GateOp for TwoQubitUnitaryGate {
276    fn name(&self) -> &'static str {
277        "QV_SU4"
278    }
279
280    fn qubits(&self) -> Vec<QubitId> {
281        vec![self.qubit1, self.qubit2]
282    }
283
284    fn matrix(&self) -> QuantRS2Result<Vec<Complex64>> {
285        // Row-major flatten.
286        let (rows, cols) = self.matrix.dim();
287        let mut flat = Vec::with_capacity(rows * cols);
288        for i in 0..rows {
289            for j in 0..cols {
290                flat.push(self.matrix[[i, j]]);
291            }
292        }
293        Ok(flat)
294    }
295
296    fn as_any(&self) -> &dyn std::any::Any {
297        self
298    }
299
300    fn clone_gate(&self) -> Box<dyn GateOp> {
301        Box::new(self.clone())
302    }
303}
304
305/// Gram-Schmidt orthonormalisation of the columns of `matrix`, yielding a unitary.
306fn gram_schmidt_unitary(matrix: &Array2<Complex64>) -> QuantRS2Result<Array2<Complex64>> {
307    let dim = matrix.nrows();
308    let mut result = Array2::<Complex64>::zeros((dim, dim));
309
310    for j in 0..dim {
311        let mut col = matrix.column(j).to_owned();
312
313        // Subtract projections onto previously-computed orthonormal columns.
314        for k in 0..j {
315            let prev = result.column(k);
316            let proj: Complex64 = col.iter().zip(prev.iter()).map(|(a, b)| b.conj() * a).sum();
317            for i in 0..dim {
318                col[i] -= proj * prev[i];
319            }
320        }
321
322        let norm = col.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
323        if norm < 1e-12 {
324            return Err(QuantRS2Error::ComputationError(
325                "Gram-Schmidt failed: degenerate random matrix".to_string(),
326            ));
327        }
328        for i in 0..dim {
329            result[[i, j]] = col[i] / Complex64::new(norm, 0.0);
330        }
331    }
332
333    Ok(result)
334}
335
336/// Classically simulate a gate circuit on `n_qubits` qubits starting from
337/// `|0...0>`, returning the final state vector.
338///
339/// Each gate's `matrix()` (row-major over its local basis, with `qubits()[0]` the
340/// high-order local bit) is applied to the relevant amplitude tuples via direct
341/// bit-mask indexing — the standard state-vector update.
342fn simulate_circuit(
343    circuit: &[Box<dyn GateOp>],
344    n_qubits: usize,
345) -> QuantRS2Result<Array1<Complex64>> {
346    let dim = 1usize << n_qubits;
347    let mut state = Array1::<Complex64>::zeros(dim);
348    state[0] = Complex64::new(1.0, 0.0);
349
350    for gate in circuit {
351        apply_gate(&mut state, gate.as_ref(), n_qubits)?;
352    }
353
354    Ok(state)
355}
356
357/// Apply a single (1- or multi-qubit) gate to the state vector in place.
358fn apply_gate(
359    state: &mut Array1<Complex64>,
360    gate: &dyn GateOp,
361    n_qubits: usize,
362) -> QuantRS2Result<()> {
363    let qubits = gate.qubits();
364    let k = qubits.len();
365    let gate_dim = 1usize << k;
366
367    let flat = gate.matrix()?;
368    if flat.len() != gate_dim * gate_dim {
369        return Err(QuantRS2Error::InvalidInput(format!(
370            "Gate matrix has {} entries, expected {} for {}-qubit gate",
371            flat.len(),
372            gate_dim * gate_dim,
373            k
374        )));
375    }
376    // Reshape row-major flat matrix into a 2D view.
377    let gate_matrix = Array2::from_shape_vec((gate_dim, gate_dim), flat)
378        .map_err(|e| QuantRS2Error::ComputationError(format!("Gate reshape failed: {e}")))?;
379
380    // Global bit positions for the gate's local bits. Local bit 0 (most
381    // significant in the gate basis) corresponds to qubits[0].
382    let qubit_bits: Vec<usize> = qubits.iter().map(|q| q.id() as usize).collect();
383    for &b in &qubit_bits {
384        if b >= n_qubits {
385            return Err(QuantRS2Error::InvalidInput(format!(
386                "Gate acts on qubit {b} but circuit has only {n_qubits} qubits"
387            )));
388        }
389    }
390
391    let dim = 1usize << n_qubits;
392    // Iterate over all "base" indices where the gate's qubits are 0, then update
393    // the 2^k amplitudes for each combination of the gate's local bits.
394    let mut visited = vec![false; dim];
395    for base in 0..dim {
396        // Skip indices that set any of the gate qubits (we enumerate those via
397        // the local-combination loop below) and any already-processed group.
398        if visited[base] {
399            continue;
400        }
401        let mut anchor = base;
402        for &b in &qubit_bits {
403            anchor &= !(1 << b);
404        }
405        if anchor != base {
406            continue;
407        }
408
409        // Gather the 2^k amplitudes of this group.
410        let mut indices = vec![0usize; gate_dim];
411        let mut amplitudes = vec![Complex64::new(0.0, 0.0); gate_dim];
412        for local in 0..gate_dim {
413            let mut idx = anchor;
414            for (pos, &b) in qubit_bits.iter().enumerate() {
415                // Local bit `pos` is bit `(k - 1 - pos)` of `local` so that
416                // qubits[0] is the most-significant local bit.
417                let bit = (local >> (k - 1 - pos)) & 1;
418                if bit == 1 {
419                    idx |= 1 << b;
420                }
421            }
422            indices[local] = idx;
423            amplitudes[local] = state[idx];
424            visited[idx] = true;
425        }
426
427        // Apply the gate matrix: new[r] = Σ_c M[r,c] * old[c].
428        for r in 0..gate_dim {
429            let mut acc = Complex64::new(0.0, 0.0);
430            for c in 0..gate_dim {
431                acc += gate_matrix[[r, c]] * amplitudes[c];
432            }
433            state[indices[r]] = acc;
434        }
435    }
436
437    Ok(())
438}
439
440/// Result of quantum volume protocol
441#[derive(Debug, Clone)]
442pub struct QuantumVolumeResult {
443    /// Achieved quantum volume (2^n)
444    pub quantum_volume: usize,
445    /// Success rates for each qubit count tested
446    pub success_rates: HashMap<usize, f64>,
447    /// Maximum number of qubits tested
448    pub max_qubits_tested: usize,
449}
450
451impl QuantumVolumeResult {
452    /// Get the number of qubits achieved
453    pub fn num_qubits_achieved(&self) -> usize {
454        (self.quantum_volume as f64).log2() as usize
455    }
456
457    /// Check if quantum volume was achieved for n qubits
458    pub fn is_qv_achieved(&self, n_qubits: usize) -> bool {
459        self.success_rates
460            .get(&n_qubits)
461            .is_some_and(|&rate| rate >= 2.0 / 3.0)
462    }
463}
464
465/// Quantum Process Tomography Protocol
466///
467/// Completely characterizes a quantum operation by measuring its action
468/// on a complete set of input states.
469pub struct QuantumProcessTomography {
470    /// Number of qubits in the process
471    pub num_qubits: usize,
472    /// Basis for state preparation (typically Pauli basis)
473    pub preparation_basis: Vec<String>,
474    /// Basis for measurement (typically Pauli basis)
475    pub measurement_basis: Vec<String>,
476}
477
478impl QuantumProcessTomography {
479    /// Create a new QPT protocol
480    pub fn new(num_qubits: usize) -> Self {
481        // Generate Pauli basis for preparation and measurement
482        let basis = Self::generate_pauli_basis(num_qubits);
483
484        Self {
485            num_qubits,
486            preparation_basis: basis.clone(),
487            measurement_basis: basis,
488        }
489    }
490
491    /// Generate Pauli basis strings for n qubits
492    fn generate_pauli_basis(n_qubits: usize) -> Vec<String> {
493        let paulis = ['I', 'X', 'Y', 'Z'];
494        let basis_size = 4_usize.pow(n_qubits as u32);
495
496        let mut basis = Vec::with_capacity(basis_size);
497
498        for i in 0..basis_size {
499            let mut pauli_string = String::with_capacity(n_qubits);
500            let mut idx = i;
501
502            for _ in 0..n_qubits {
503                pauli_string.push(paulis[idx % 4]);
504                idx /= 4;
505            }
506
507            basis.push(pauli_string);
508        }
509
510        basis
511    }
512
513    /// Run quantum process tomography
514    ///
515    /// Returns the reconstructed process matrix (chi matrix)
516    pub fn run<F>(&self, mut apply_process: F) -> QuantRS2Result<ProcessMatrix>
517    where
518        F: FnMut(&str, &str) -> Complex64, // (prep_basis, meas_basis) -> expectation value
519    {
520        let dim = 1 << self.num_qubits;
521        let basis_size = self.preparation_basis.len();
522
523        // Allocate chi matrix
524        let mut chi_matrix = Array2::zeros((basis_size, basis_size));
525
526        // Perform tomography: measure E[P_out | P_in] for all Pauli pairs
527        for (i, prep) in self.preparation_basis.iter().enumerate() {
528            for (j, meas) in self.measurement_basis.iter().enumerate() {
529                let expectation = apply_process(prep, meas);
530                chi_matrix[[i, j]] = expectation;
531            }
532        }
533
534        // Post-process to enforce physicality (positive semidefinite, trace-preserving)
535        let chi_matrix = self.enforce_physicality(chi_matrix)?;
536
537        Ok(ProcessMatrix {
538            chi_matrix,
539            num_qubits: self.num_qubits,
540            basis_labels: self.preparation_basis.clone(),
541        })
542    }
543
544    /// Enforce physicality constraints on the process matrix
545    fn enforce_physicality(&self, chi: Array2<Complex64>) -> QuantRS2Result<Array2<Complex64>> {
546        // Simplified physicality enforcement
547        // In practice, this would use:
548        // 1. Maximum likelihood estimation
549        // 2. Projection onto physical process matrices
550        // 3. Constrained optimization
551
552        // For now, just normalize
553        let trace: Complex64 = chi.diag().iter().sum();
554        let normalized = if trace.norm() > 1e-10 {
555            &chi / trace
556        } else {
557            chi
558        };
559
560        Ok(normalized)
561    }
562
563    /// Compute process fidelity between two process matrices
564    pub fn process_fidelity(chi1: &Array2<Complex64>, chi2: &Array2<Complex64>) -> f64 {
565        // F_proc = Tr(chi1^† chi2)
566        let product = chi1.t().mapv(|x| x.conj()).dot(chi2);
567        let trace: Complex64 = product.diag().iter().sum();
568        trace.norm()
569    }
570
571    /// Compute average gate fidelity from process matrix
572    pub fn average_gate_fidelity(
573        &self,
574        chi: &Array2<Complex64>,
575        ideal_chi: &Array2<Complex64>,
576    ) -> f64 {
577        let dim = 1 << self.num_qubits;
578        let d = dim as f64;
579
580        // F_avg = (d * F_proc + 1) / (d + 1)
581        let f_proc = Self::process_fidelity(chi, ideal_chi);
582        (d * f_proc + 1.0) / (d + 1.0)
583    }
584}
585
586/// Reconstructed process matrix from QPT
587#[derive(Debug, Clone)]
588pub struct ProcessMatrix {
589    /// Chi matrix in Pauli basis
590    pub chi_matrix: Array2<Complex64>,
591    /// Number of qubits
592    pub num_qubits: usize,
593    /// Basis labels
594    pub basis_labels: Vec<String>,
595}
596
597impl ProcessMatrix {
598    /// Get the process matrix element for specific Pauli operators
599    pub fn get_element(&self, prep_pauli: &str, meas_pauli: &str) -> Option<Complex64> {
600        let i = self.basis_labels.iter().position(|s| s == prep_pauli)?;
601        let j = self.basis_labels.iter().position(|s| s == meas_pauli)?;
602        Some(self.chi_matrix[[i, j]])
603    }
604
605    /// Check if the process is trace-preserving
606    pub fn is_trace_preserving(&self, tolerance: f64) -> bool {
607        let trace: Complex64 = self.chi_matrix.diag().iter().sum();
608        (trace - Complex64::new(1.0, 0.0)).norm() < tolerance
609    }
610
611    /// Check if the process is completely positive
612    pub fn is_completely_positive(&self, tolerance: f64) -> bool {
613        // Simplified check: chi should be positive semidefinite
614        // In practice, would compute eigenvalues
615
616        // For now, check diagonal elements are non-negative
617        self.chi_matrix.diag().iter().all(|&x| x.re >= -tolerance)
618    }
619
620    /// Compute the diamond norm distance to another process
621    pub fn diamond_distance(&self, other: &Self) -> QuantRS2Result<f64> {
622        if self.num_qubits != other.num_qubits {
623            return Err(QuantRS2Error::InvalidInput(
624                "Process matrices must have same dimension".to_string(),
625            ));
626        }
627
628        // Simplified diamond distance computation
629        // Full implementation requires semidefinite programming
630
631        // Approximate using Frobenius norm
632        let diff = &self.chi_matrix - &other.chi_matrix;
633        let frobenius_norm = diff.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
634
635        Ok(frobenius_norm)
636    }
637}
638
639/// Gate Set Tomography (GST)
640///
641/// More comprehensive than QPT, GST characterizes an entire gate set
642/// including state preparation and measurement errors.
643pub struct GateSetTomography {
644    /// Number of qubits
645    pub num_qubits: usize,
646    /// Gate set to characterize
647    pub gate_set: Vec<String>,
648    /// Maximum sequence length
649    pub max_length: usize,
650}
651
652impl GateSetTomography {
653    /// Create a new GST protocol
654    pub const fn new(num_qubits: usize, gate_set: Vec<String>, max_length: usize) -> Self {
655        Self {
656            num_qubits,
657            gate_set,
658            max_length,
659        }
660    }
661
662    /// Run gate set tomography
663    ///
664    /// This is a placeholder for the full GST algorithm
665    pub fn run<F>(&self, mut execute_sequence: F) -> QuantRS2Result<GateSetModel>
666    where
667        F: FnMut(&[&str]) -> f64, // Gate sequence -> measurement probability
668    {
669        // GST consists of three types of sequences:
670        // 1. Germ sequences (repeated short sequences)
671        // 2. Fiducial sequences (state prep and measurement)
672        // 3. Amplification sequences (repeated germs)
673
674        let germs = self.generate_germs();
675        let fiducials = self.generate_fiducials();
676
677        // Collect data from all sequences
678        let mut data = HashMap::new();
679
680        for prep_fiducial in &fiducials {
681            for germ in &germs {
682                for meas_fiducial in &fiducials {
683                    // Build amplified sequence
684                    for power in 1..=self.max_length {
685                        let mut sequence = Vec::new();
686
687                        // Prep fiducial
688                        sequence.extend_from_slice(prep_fiducial);
689
690                        // Repeated germ
691                        for _ in 0..power {
692                            sequence.extend_from_slice(germ);
693                        }
694
695                        // Measurement fiducial
696                        sequence.extend_from_slice(meas_fiducial);
697
698                        // Execute and collect data
699                        let probability = execute_sequence(&sequence);
700                        data.insert(sequence.clone(), probability);
701                    }
702                }
703            }
704        }
705
706        // Fit model to data using maximum likelihood estimation
707        let model = self.fit_model(&data)?;
708
709        Ok(model)
710    }
711
712    /// Generate germ sequences
713    fn generate_germs(&self) -> Vec<Vec<&str>> {
714        // Standard germs for single qubit: I, X, Y, XY, XYX
715        // This is a simplified set
716        vec![vec!["I"], vec!["X"], vec!["Y"], vec!["X", "Y"]]
717    }
718
719    /// Generate fiducial sequences
720    fn generate_fiducials(&self) -> Vec<Vec<&str>> {
721        // Standard fiducials for single qubit
722        vec![
723            vec!["I"],
724            vec!["X"],
725            vec!["Y"],
726            vec!["X", "X"], // -I
727        ]
728    }
729
730    /// Fit GST model to data
731    fn fit_model(&self, _data: &HashMap<Vec<&str>, f64>) -> QuantRS2Result<GateSetModel> {
732        // Placeholder: maximum likelihood estimation
733        // Real implementation would use iterative optimization
734
735        Ok(GateSetModel {
736            num_qubits: self.num_qubits,
737            gate_errors: HashMap::new(),
738            spam_errors: vec![],
739        })
740    }
741}
742
743/// GST model describing errors in gates and measurements
744#[derive(Debug, Clone)]
745pub struct GateSetModel {
746    /// Number of qubits
747    pub num_qubits: usize,
748    /// Error models for each gate
749    pub gate_errors: HashMap<String, Array2<Complex64>>,
750    /// State preparation and measurement (SPAM) errors
751    pub spam_errors: Vec<f64>,
752}
753
754#[cfg(test)]
755mod tests {
756    use super::*;
757
758    #[test]
759    fn test_quantum_volume_result() {
760        let mut result = QuantumVolumeResult {
761            quantum_volume: 16,
762            success_rates: HashMap::new(),
763            max_qubits_tested: 5,
764        };
765
766        result.success_rates.insert(1, 0.95);
767        result.success_rates.insert(2, 0.85);
768        result.success_rates.insert(3, 0.75);
769        result.success_rates.insert(4, 0.70);
770
771        assert_eq!(result.num_qubits_achieved(), 4);
772        assert!(result.is_qv_achieved(1));
773        assert!(result.is_qv_achieved(2));
774        assert!(result.is_qv_achieved(3));
775        assert!(result.is_qv_achieved(4));
776
777        println!("Quantum Volume: {}", result.quantum_volume);
778    }
779
780    #[test]
781    fn test_pauli_basis_generation() {
782        let basis = QuantumProcessTomography::generate_pauli_basis(1);
783        assert_eq!(basis.len(), 4);
784        assert!(basis.contains(&"I".to_string()));
785        assert!(basis.contains(&"X".to_string()));
786        assert!(basis.contains(&"Y".to_string()));
787        assert!(basis.contains(&"Z".to_string()));
788
789        let basis_2q = QuantumProcessTomography::generate_pauli_basis(2);
790        assert_eq!(basis_2q.len(), 16);
791    }
792
793    #[test]
794    fn test_process_matrix() {
795        let qpt = QuantumProcessTomography::new(1);
796
797        // Mock process: identity
798        let mock_process = |_prep: &str, meas: &str| {
799            if meas == "I" {
800                Complex64::new(1.0, 0.0)
801            } else {
802                Complex64::new(0.0, 0.0)
803            }
804        };
805
806        let result = qpt
807            .run(mock_process)
808            .expect("QPT run should succeed with mock process");
809
810        assert_eq!(result.num_qubits, 1);
811        assert!(result.is_trace_preserving(1e-6));
812        println!("Process matrix shape: {:?}", result.chi_matrix.dim());
813    }
814
815    #[test]
816    fn test_process_fidelity() {
817        let dim = 4;
818        let identity = Array2::eye(dim);
819        let noisy = &identity * Complex64::new(0.95, 0.0);
820
821        let fidelity = QuantumProcessTomography::process_fidelity(&identity, &noisy);
822
823        // Fidelity is the trace of the product, which for scaled identity is just the scaling factor times dim
824        // So for 0.95 * I with dim=4, we expect fidelity = 0.95 * 4 = 3.8
825        println!("Process fidelity: {}", fidelity);
826
827        // The fidelity should be proportional to the scaling
828        assert!(fidelity > 0.0 && fidelity <= dim as f64);
829    }
830
831    #[test]
832    fn test_gst_initialization() {
833        let gate_set = vec!["I".to_string(), "X".to_string(), "H".to_string()];
834        let gst = GateSetTomography::new(1, gate_set, 10);
835
836        assert_eq!(gst.num_qubits, 1);
837        assert_eq!(gst.max_length, 10);
838
839        let germs = gst.generate_germs();
840        assert!(!germs.is_empty());
841
842        let fiducials = gst.generate_fiducials();
843        assert!(!fiducials.is_empty());
844    }
845
846    /// Build a 2-qubit gate whose action on |00> yields a chosen amplitude vector.
847    /// The supplied amplitudes form the first column of the unitary; the remaining
848    /// columns are completed by Gram-Schmidt from the standard basis.
849    fn gate_from_first_column(amps: [Complex64; 4], q1: usize, q2: usize) -> TwoQubitUnitaryGate {
850        let mut m = Array2::<Complex64>::zeros((4, 4));
851        for i in 0..4 {
852            m[[i, 0]] = amps[i];
853        }
854        // Seed the other columns with distinct standard basis vectors.
855        m[[1, 1]] = Complex64::new(1.0, 0.0);
856        m[[2, 2]] = Complex64::new(1.0, 0.0);
857        m[[3, 3]] = Complex64::new(1.0, 0.0);
858        let u = gram_schmidt_unitary(&m).expect("gram-schmidt");
859        TwoQubitUnitaryGate::new(u, q1, q2)
860    }
861
862    #[test]
863    fn test_apply_gate_bit_ordering_cnot() {
864        // A CNOT (control = qubit 0, target = qubit 1) in row-major form, with
865        // qubit 0 as the most-significant local bit. Applied to |10> (qubit 0
866        // set) it must produce |11>.
867        let cnot = scirs2_core::ndarray::array![
868            [
869                Complex64::new(1.0, 0.0),
870                Complex64::new(0.0, 0.0),
871                Complex64::new(0.0, 0.0),
872                Complex64::new(0.0, 0.0)
873            ],
874            [
875                Complex64::new(0.0, 0.0),
876                Complex64::new(1.0, 0.0),
877                Complex64::new(0.0, 0.0),
878                Complex64::new(0.0, 0.0)
879            ],
880            [
881                Complex64::new(0.0, 0.0),
882                Complex64::new(0.0, 0.0),
883                Complex64::new(0.0, 0.0),
884                Complex64::new(1.0, 0.0)
885            ],
886            [
887                Complex64::new(0.0, 0.0),
888                Complex64::new(0.0, 0.0),
889                Complex64::new(1.0, 0.0),
890                Complex64::new(0.0, 0.0)
891            ]
892        ];
893        let gate: Box<dyn GateOp> = Box::new(TwoQubitUnitaryGate::new(cnot, 0, 1));
894
895        // simulate_circuit starts at |00>; CNOT leaves it unchanged.
896        let state = simulate_circuit(std::slice::from_ref(&gate), 2).expect("simulate");
897        assert!((state[0].norm() - 1.0).abs() < 1e-12);
898
899        // Apply to a custom |10> state to check the controlled flip.
900        let mut custom = Array1::<Complex64>::zeros(4);
901        custom[1] = Complex64::new(1.0, 0.0); // qubit0 = 1, qubit1 = 0
902        apply_gate(&mut custom, gate.as_ref(), 2).expect("apply");
903        // Expect |11>: qubit0=1, qubit1=1 -> bits 0 and 1 set -> index 3.
904        assert!((custom[3].norm() - 1.0).abs() < 1e-12, "got {custom:?}");
905        assert!(custom[1].norm() < 1e-12);
906    }
907
908    #[test]
909    fn test_find_heavy_outputs_above_median_not_first_half() {
910        // Construct a circuit with a deliberately non-uniform output distribution
911        // (the four basis probabilities are all distinct), so the median is well
912        // defined and the heavy-output set is unambiguous.
913        let amps = [
914            Complex64::new(0.1_f64.sqrt(), 0.0),
915            Complex64::new(0.4_f64.sqrt(), 0.0),
916            Complex64::new(0.2_f64.sqrt(), 0.0),
917            Complex64::new(0.3_f64.sqrt(), 0.0),
918        ];
919        let gate = gate_from_first_column(amps, 0, 1);
920        let circuit: Vec<Box<dyn GateOp>> = vec![Box::new(gate)];
921
922        let qv = QuantumVolume::new(2, 1, 100);
923        let heavy = qv.find_heavy_outputs(2, &circuit).expect("heavy outputs");
924
925        // Independently recompute the expected heavy set directly from the
926        // simulated state vector (the source of truth).
927        let state = simulate_circuit(&circuit, 2).expect("simulate");
928        let probs: Vec<f64> = state.iter().map(|a| a.norm_sqr()).collect();
929
930        // The distribution must be genuinely non-uniform and normalised.
931        let total: f64 = probs.iter().sum();
932        assert!((total - 1.0).abs() < 1e-9);
933        let max_p = probs.iter().cloned().fold(0.0_f64, f64::max);
934        let min_p = probs.iter().cloned().fold(1.0_f64, f64::min);
935        assert!(max_p - min_p > 1e-3, "distribution should be non-uniform");
936
937        let mut sorted = probs.clone();
938        sorted.sort_by(|a, b| a.total_cmp(b));
939        let median = 0.5 * (sorted[1] + sorted[2]);
940        let mut expected: Vec<usize> = probs
941            .iter()
942            .enumerate()
943            .filter(|(_, &p)| p > median)
944            .map(|(i, _)| i)
945            .collect();
946        expected.sort_unstable();
947
948        let mut heavy_sorted = heavy.clone();
949        heavy_sorted.sort_unstable();
950        assert_eq!(
951            heavy_sorted, expected,
952            "heavy outputs must be exactly the strictly-above-median indices"
953        );
954        // For four distinct probabilities, exactly two are above the median.
955        assert_eq!(heavy_sorted.len(), 2);
956
957        // It must NOT be the old fabricated "first half" (0..num_states/2).
958        let first_half: Vec<usize> = (0..(1usize << 2) / 2).collect();
959        assert_ne!(
960            heavy_sorted, first_half,
961            "heavy outputs must be computed from probabilities, not the first half"
962        );
963    }
964
965    #[test]
966    fn test_generate_random_circuit_is_non_empty() {
967        // A real QV circuit must contain depth * (n/2) two-qubit gates, never an
968        // empty placeholder.
969        let mut qv = QuantumVolume::new(4, 1, 10);
970        let n = 4;
971        let (circuit, heavy) = qv.generate_random_circuit(n).expect("circuit");
972
973        // depth = n layers, each with n/2 = 2 gates -> 8 gates.
974        assert_eq!(circuit.len(), n * (n / 2));
975        assert!(!circuit.is_empty(), "QV circuit must not be empty");
976        for gate in &circuit {
977            assert_eq!(gate.qubits().len(), 2, "each QV gate acts on 2 qubits");
978        }
979
980        // The state must be normalised and heavy outputs computed from it.
981        let state = simulate_circuit(&circuit, n).expect("simulate");
982        let total: f64 = state.iter().map(|a| a.norm_sqr()).sum();
983        assert!(
984            (total - 1.0).abs() < 1e-9,
985            "state must stay normalised: {total}"
986        );
987
988        // Heavy outputs are a strict subset of all 2^n states and (for a generic
989        // random circuit) non-empty.
990        assert!(heavy.iter().all(|&i| i < (1usize << n)));
991    }
992}