quantrs2_core/
characterization.rs

1//! Gate and quantum system characterization
2//!
3//! This module provides comprehensive tools for analyzing and characterizing quantum gates
4//! and quantum systems using their eigenstructure and other advanced techniques. This is useful for:
5//! - Gate synthesis and decomposition
6//! - Identifying gate types and parameters
7//! - Optimizing gate sequences
8//! - Verifying gate implementations
9//! - Quantum volume measurement
10//! - Quantum process tomography
11//! - Noise characterization and mitigation
12
13use crate::{
14    eigensolve::eigen_decompose_unitary,
15    error::{QuantRS2Error, QuantRS2Result},
16    gate::{single::*, GateOp},
17    qubit::QubitId,
18};
19use scirs2_core::ndarray::{Array1, Array2, Array3, Array4, Axis};
20use scirs2_core::random::prelude::*;
21use scirs2_core::random::Distribution;
22use scirs2_core::Complex64 as Complex;
23use std::collections::HashMap;
24
25/// Represents the eigenstructure of a quantum gate
26#[derive(Debug, Clone)]
27pub struct GateEigenstructure {
28    /// Eigenvalues of the gate
29    pub eigenvalues: Vec<Complex>,
30    /// Eigenvectors as columns of a matrix
31    pub eigenvectors: Array2<Complex>,
32    /// The original gate matrix
33    pub matrix: Array2<Complex>,
34}
35
36impl GateEigenstructure {
37    /// Check if the gate is diagonal in some basis
38    pub fn is_diagonal(&self, tolerance: f64) -> bool {
39        // A gate is diagonal if its matrix commutes with a diagonal matrix
40        // of its eigenvalues
41        let n = self.matrix.nrows();
42        for i in 0..n {
43            for j in 0..n {
44                if i != j && self.matrix[(i, j)].norm() > tolerance {
45                    return false;
46                }
47            }
48        }
49        true
50    }
51
52    /// Get the phases of eigenvalues (assuming unitary gate)
53    pub fn eigenphases(&self) -> Vec<f64> {
54        self.eigenvalues
55            .iter()
56            .map(|&lambda| lambda.arg())
57            .collect()
58    }
59
60    /// Check if this represents a phase gate
61    pub fn is_phase_gate(&self, tolerance: f64) -> bool {
62        // All eigenvalues should have the same magnitude (1 for unitary)
63        let magnitude = self.eigenvalues[0].norm();
64        self.eigenvalues
65            .iter()
66            .all(|&lambda| (lambda.norm() - magnitude).abs() < tolerance)
67    }
68
69    /// Get the rotation angle for single-qubit gates
70    pub fn rotation_angle(&self) -> Option<f64> {
71        if self.eigenvalues.len() != 2 {
72            return None;
73        }
74
75        // For a rotation gate, eigenvalues are e^(±iθ/2)
76        let phase_diff = (self.eigenvalues[0] / self.eigenvalues[1]).arg();
77        Some(phase_diff.abs())
78    }
79
80    /// Get the rotation axis for single-qubit gates
81    pub fn rotation_axis(&self, tolerance: f64) -> Option<[f64; 3]> {
82        if self.eigenvalues.len() != 2 {
83            return None;
84        }
85
86        // Find the Bloch sphere axis from eigenvectors
87        // For a rotation about axis n, the eigenvectors correspond to
88        // spin up/down along that axis
89        let v0 = self.eigenvectors.column(0);
90        let v1 = self.eigenvectors.column(1);
91
92        // Convert eigenvectors to Bloch vectors
93        let bloch0 = eigenvector_to_bloch(&v0.to_owned());
94        let bloch1 = eigenvector_to_bloch(&v1.to_owned());
95
96        // The rotation axis is perpendicular to both Bloch vectors
97        // (for pure rotation, eigenvectors should point opposite on sphere)
98        let axis = [
99            f64::midpoint(bloch0[0], bloch1[0]),
100            f64::midpoint(bloch0[1], bloch1[1]),
101            f64::midpoint(bloch0[2], bloch1[2]),
102        ];
103
104        let norm = axis[2]
105            .mul_add(axis[2], axis[0].mul_add(axis[0], axis[1] * axis[1]))
106            .sqrt();
107        if norm < tolerance {
108            None
109        } else {
110            Some([axis[0] / norm, axis[1] / norm, axis[2] / norm])
111        }
112    }
113}
114
115/// Convert an eigenvector to Bloch sphere coordinates
116fn eigenvector_to_bloch(v: &Array1<Complex>) -> [f64; 3] {
117    if v.len() != 2 {
118        return [0.0, 0.0, 0.0];
119    }
120
121    // Compute density matrix rho = |v><v|
122    let v0 = v[0];
123    let v1 = v[1];
124    let rho00 = (v0 * v0.conj()).re;
125    let rho11 = (v1 * v1.conj()).re;
126    let rho01 = v0 * v1.conj();
127
128    [2.0 * rho01.re, -2.0 * rho01.im, rho00 - rho11]
129}
130
131/// Gate characterization tools
132pub struct GateCharacterizer {
133    tolerance: f64,
134}
135
136impl GateCharacterizer {
137    /// Create a new gate characterizer
138    pub const fn new(tolerance: f64) -> Self {
139        Self { tolerance }
140    }
141
142    /// Compute the eigenstructure of a gate
143    pub fn eigenstructure(&self, gate: &dyn GateOp) -> QuantRS2Result<GateEigenstructure> {
144        let matrix_vec = gate.matrix()?;
145        let n = (matrix_vec.len() as f64).sqrt() as usize;
146
147        // Convert to ndarray matrix
148        let mut matrix = Array2::zeros((n, n));
149        for i in 0..n {
150            for j in 0..n {
151                matrix[(i, j)] = matrix_vec[i * n + j];
152            }
153        }
154
155        // Perform eigendecomposition using our optimized algorithm
156        let eigen = eigen_decompose_unitary(&matrix, self.tolerance)?;
157
158        Ok(GateEigenstructure {
159            eigenvalues: eigen.eigenvalues.to_vec(),
160            eigenvectors: eigen.eigenvectors,
161            matrix,
162        })
163    }
164
165    /// Identify the type of gate based on its eigenstructure
166    pub fn identify_gate_type(&self, gate: &dyn GateOp) -> QuantRS2Result<GateType> {
167        let eigen = self.eigenstructure(gate)?;
168        let n = eigen.eigenvalues.len();
169
170        match n {
171            2 => self.identify_single_qubit_gate(&eigen),
172            4 => self.identify_two_qubit_gate(&eigen),
173            _ => Ok(GateType::General {
174                qubits: (n as f64).log2() as usize,
175            }),
176        }
177    }
178
179    /// Identify single-qubit gate type
180    fn identify_single_qubit_gate(&self, eigen: &GateEigenstructure) -> QuantRS2Result<GateType> {
181        // Check for Pauli gates (eigenvalues ±1 or ±i)
182        if self.is_pauli_gate(eigen) {
183            return Ok(self.identify_pauli_type(eigen));
184        }
185
186        // Check for phase/rotation gates
187        if let Some(angle) = eigen.rotation_angle() {
188            if let Some(axis) = eigen.rotation_axis(self.tolerance) {
189                return Ok(GateType::Rotation { angle, axis });
190            }
191        }
192
193        // Check for Hadamard (eigenvalues ±1)
194        if self.is_hadamard(eigen) {
195            return Ok(GateType::Hadamard);
196        }
197
198        Ok(GateType::General { qubits: 1 })
199    }
200
201    /// Identify two-qubit gate type
202    fn identify_two_qubit_gate(&self, eigen: &GateEigenstructure) -> QuantRS2Result<GateType> {
203        // Check for CNOT (eigenvalues all ±1)
204        if self.is_cnot(eigen) {
205            return Ok(GateType::CNOT);
206        }
207
208        // Check for controlled phase gates
209        if self.is_controlled_phase(eigen) {
210            if let Some(phase) = self.extract_controlled_phase(eigen) {
211                return Ok(GateType::ControlledPhase { phase });
212            }
213        }
214
215        // Check for SWAP variants
216        if self.is_swap_variant(eigen) {
217            return Ok(self.identify_swap_type(eigen));
218        }
219
220        Ok(GateType::General { qubits: 2 })
221    }
222
223    /// Check if gate is a Pauli gate
224    fn is_pauli_gate(&self, eigen: &GateEigenstructure) -> bool {
225        eigen.eigenvalues.iter().all(|&lambda| {
226            let is_plus_minus_one = (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance
227                || (lambda + Complex::new(1.0, 0.0)).norm() < self.tolerance;
228            let is_plus_minus_i = (lambda - Complex::new(0.0, 1.0)).norm() < self.tolerance
229                || (lambda + Complex::new(0.0, 1.0)).norm() < self.tolerance;
230            is_plus_minus_one || is_plus_minus_i
231        })
232    }
233
234    /// Identify which Pauli gate
235    fn identify_pauli_type(&self, eigen: &GateEigenstructure) -> GateType {
236        let matrix = &eigen.matrix;
237
238        // Check matrix elements to identify Pauli type
239        let tolerance = self.tolerance;
240
241        // Check for Pauli X: [[0,1],[1,0]]
242        if (matrix[(0, 1)] - Complex::new(1.0, 0.0)).norm() < tolerance
243            && (matrix[(1, 0)] - Complex::new(1.0, 0.0)).norm() < tolerance
244            && matrix[(0, 0)].norm() < tolerance
245            && matrix[(1, 1)].norm() < tolerance
246        {
247            GateType::PauliX
248        }
249        // Check for Pauli Y: [[0,-i],[i,0]]
250        else if (matrix[(0, 1)] - Complex::new(0.0, -1.0)).norm() < tolerance
251            && (matrix[(1, 0)] - Complex::new(0.0, 1.0)).norm() < tolerance
252            && matrix[(0, 0)].norm() < tolerance
253            && matrix[(1, 1)].norm() < tolerance
254        {
255            GateType::PauliY
256        }
257        // Check for Pauli Z: [[1,0],[0,-1]]
258        else if (matrix[(0, 0)] - Complex::new(1.0, 0.0)).norm() < tolerance
259            && (matrix[(1, 1)] - Complex::new(-1.0, 0.0)).norm() < tolerance
260            && matrix[(0, 1)].norm() < tolerance
261            && matrix[(1, 0)].norm() < tolerance
262        {
263            GateType::PauliZ
264        } else {
265            GateType::General { qubits: 1 }
266        }
267    }
268
269    /// Check if gate is Hadamard
270    fn is_hadamard(&self, eigen: &GateEigenstructure) -> bool {
271        // Hadamard has eigenvalues ±1
272        eigen.eigenvalues.iter().all(|&lambda| {
273            (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance
274                || (lambda + Complex::new(1.0, 0.0)).norm() < self.tolerance
275        })
276    }
277
278    /// Check if gate is CNOT
279    fn is_cnot(&self, eigen: &GateEigenstructure) -> bool {
280        // CNOT has eigenvalues all ±1
281        eigen.eigenvalues.len() == 4
282            && eigen.eigenvalues.iter().all(|&lambda| {
283                (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance
284                    || (lambda + Complex::new(1.0, 0.0)).norm() < self.tolerance
285            })
286    }
287
288    /// Check if gate is a controlled phase gate
289    fn is_controlled_phase(&self, eigen: &GateEigenstructure) -> bool {
290        // Controlled phase has three eigenvalues = 1 and one phase
291        let ones = eigen
292            .eigenvalues
293            .iter()
294            .filter(|&&lambda| (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance)
295            .count();
296        ones == 3
297    }
298
299    /// Extract phase from controlled phase gate
300    fn extract_controlled_phase(&self, eigen: &GateEigenstructure) -> Option<f64> {
301        eigen
302            .eigenvalues
303            .iter()
304            .find(|&&lambda| (lambda - Complex::new(1.0, 0.0)).norm() > self.tolerance)
305            .map(|&lambda| lambda.arg())
306    }
307
308    /// Check if gate is a SWAP variant
309    fn is_swap_variant(&self, eigen: &GateEigenstructure) -> bool {
310        // SWAP has eigenvalues {1, 1, 1, -1}
311        // iSWAP has eigenvalues {1, 1, i, -i}
312        let ones = eigen
313            .eigenvalues
314            .iter()
315            .filter(|&&lambda| (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance)
316            .count();
317        ones >= 2
318    }
319
320    /// Identify SWAP variant type
321    fn identify_swap_type(&self, eigen: &GateEigenstructure) -> GateType {
322        let matrix = &eigen.matrix;
323
324        // Check for standard SWAP: |00>->|00>, |01>->|10>, |10>->|01>, |11>->|11>
325        if (matrix[(0, 0)] - Complex::new(1.0, 0.0)).norm() < self.tolerance
326            && (matrix[(1, 2)] - Complex::new(1.0, 0.0)).norm() < self.tolerance
327            && (matrix[(2, 1)] - Complex::new(1.0, 0.0)).norm() < self.tolerance
328            && (matrix[(3, 3)] - Complex::new(1.0, 0.0)).norm() < self.tolerance
329            && matrix[(0, 1)].norm() < self.tolerance
330            && matrix[(0, 2)].norm() < self.tolerance
331            && matrix[(0, 3)].norm() < self.tolerance
332            && matrix[(1, 0)].norm() < self.tolerance
333            && matrix[(1, 1)].norm() < self.tolerance
334            && matrix[(1, 3)].norm() < self.tolerance
335            && matrix[(2, 0)].norm() < self.tolerance
336            && matrix[(2, 2)].norm() < self.tolerance
337            && matrix[(2, 3)].norm() < self.tolerance
338            && matrix[(3, 0)].norm() < self.tolerance
339            && matrix[(3, 1)].norm() < self.tolerance
340            && matrix[(3, 2)].norm() < self.tolerance
341        {
342            GateType::SWAP
343        } else {
344            // Could be iSWAP or other variant
345            GateType::General { qubits: 2 }
346        }
347    }
348
349    /// Compare two matrices for equality
350    #[allow(dead_code)]
351    fn matrix_equals(a: &Array2<Complex>, b: &Array2<Complex>, tolerance: f64) -> bool {
352        a.shape() == b.shape()
353            && a.iter()
354                .zip(b.iter())
355                .all(|(a_ij, b_ij)| (a_ij - b_ij).norm() < tolerance)
356    }
357
358    /// Decompose a gate into rotation gates based on eigenstructure
359    pub fn decompose_to_rotations(
360        &self,
361        gate: &dyn GateOp,
362    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
363        let eigen = self.eigenstructure(gate)?;
364
365        match eigen.eigenvalues.len() {
366            2 => Self::decompose_single_qubit(&eigen),
367            _ => Err(QuantRS2Error::UnsupportedOperation(
368                "Rotation decomposition only supported for single-qubit gates".to_string(),
369            )),
370        }
371    }
372
373    /// Decompose single-qubit gate
374    fn decompose_single_qubit(eigen: &GateEigenstructure) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
375        // Use Euler angle decomposition
376        // Any single-qubit unitary can be written as Rz(γ)Ry(β)Rz(α)
377
378        let matrix = &eigen.matrix;
379
380        // Extract Euler angles
381        let alpha = matrix[(1, 1)].arg() - matrix[(1, 0)].arg();
382        let gamma = matrix[(1, 1)].arg() + matrix[(1, 0)].arg();
383        let beta = 2.0 * matrix[(1, 0)].norm().acos();
384
385        Ok(vec![
386            Box::new(RotationZ {
387                target: QubitId(0),
388                theta: alpha,
389            }),
390            Box::new(RotationY {
391                target: QubitId(0),
392                theta: beta,
393            }),
394            Box::new(RotationZ {
395                target: QubitId(0),
396                theta: gamma,
397            }),
398        ])
399    }
400
401    /// Find the closest Clifford gate to a given gate
402    pub fn find_closest_clifford(&self, gate: &dyn GateOp) -> QuantRS2Result<Box<dyn GateOp>> {
403        let eigen = self.eigenstructure(gate)?;
404
405        if eigen.eigenvalues.len() != 2 {
406            return Err(QuantRS2Error::UnsupportedOperation(
407                "Clifford approximation only supported for single-qubit gates".to_string(),
408            ));
409        }
410
411        // Single-qubit Clifford gates
412        let target = QubitId(0);
413        let clifford_gates: Vec<Box<dyn GateOp>> = vec![
414            Box::new(PauliX { target }),
415            Box::new(PauliY { target }),
416            Box::new(PauliZ { target }),
417            Box::new(Hadamard { target }),
418            Box::new(Phase { target }),
419        ];
420
421        // Find the Clifford gate with minimum distance
422        let mut min_distance = f64::INFINITY;
423        let mut closest_gate = None;
424
425        for clifford in &clifford_gates {
426            let distance = self.gate_distance(gate, clifford.as_ref())?;
427            if distance < min_distance {
428                min_distance = distance;
429                closest_gate = Some(clifford.clone());
430            }
431        }
432
433        closest_gate.ok_or_else(|| {
434            QuantRS2Error::ComputationError("Failed to find closest Clifford gate".to_string())
435        })
436    }
437
438    /// Compute the distance between two gates (Frobenius norm)
439    pub fn gate_distance(&self, gate1: &dyn GateOp, gate2: &dyn GateOp) -> QuantRS2Result<f64> {
440        let m1_vec = gate1.matrix()?;
441        let m2_vec = gate2.matrix()?;
442
443        if m1_vec.len() != m2_vec.len() {
444            return Err(QuantRS2Error::InvalidInput(
445                "Gates must have the same dimensions".to_string(),
446            ));
447        }
448
449        let diff_sqr: f64 = m1_vec
450            .iter()
451            .zip(m2_vec.iter())
452            .map(|(a, b)| (a - b).norm_sqr())
453            .sum();
454        Ok(diff_sqr.sqrt())
455    }
456
457    /// Check if a gate is approximately equal to identity
458    pub fn is_identity(&self, gate: &dyn GateOp, tolerance: f64) -> bool {
459        let matrix_vec = match gate.matrix() {
460            Ok(m) => m,
461            Err(_) => return false,
462        };
463        let n = (matrix_vec.len() as f64).sqrt() as usize;
464
465        for i in 0..n {
466            for j in 0..n {
467                let idx = i * n + j;
468                let expected = if i == j {
469                    Complex::new(1.0, 0.0)
470                } else {
471                    Complex::new(0.0, 0.0)
472                };
473                if (matrix_vec[idx] - expected).norm() > tolerance {
474                    return false;
475                }
476            }
477        }
478        true
479    }
480
481    /// Extract the global phase of a gate
482    pub fn global_phase(&self, gate: &dyn GateOp) -> QuantRS2Result<f64> {
483        let eigen = self.eigenstructure(gate)?;
484
485        // For a unitary matrix U = e^(iφ)V where V is special unitary,
486        // the global phase φ = arg(det(U))/n
487        // det(U) = product of eigenvalues
488        let det = eigen
489            .eigenvalues
490            .iter()
491            .fold(Complex::new(1.0, 0.0), |acc, &lambda| acc * lambda);
492        let n = eigen.eigenvalues.len() as f64;
493        Ok(det.arg() / n)
494    }
495}
496
497// ================================================================================================
498// Quantum Volume Measurement
499// ================================================================================================
500
501/// Quantum Volume measurement result
502///
503/// Quantum volume is a metric that quantifies the overall computational power
504/// of a quantum computer, taking into account gate fidelity, connectivity, and
505/// circuit depth capabilities.
506#[derive(Debug, Clone)]
507pub struct QuantumVolumeResult {
508    /// Number of qubits used in the measurement
509    pub num_qubits: usize,
510    /// Measured quantum volume (log2 scale)
511    pub quantum_volume_log2: f64,
512    /// Actual quantum volume (2^quantum_volume_log2)
513    pub quantum_volume: f64,
514    /// Success probability (heavy output generation)
515    pub success_probability: f64,
516    /// Threshold for heavy output (typically 2/3)
517    pub threshold: f64,
518    /// Number of circuits evaluated
519    pub num_circuits: usize,
520    /// Number of shots per circuit
521    pub shots_per_circuit: usize,
522    /// Individual circuit heavy output probabilities
523    pub circuit_probabilities: Vec<f64>,
524    /// Confidence interval (95%)
525    pub confidence_interval: (f64, f64),
526}
527
528impl QuantumVolumeResult {
529    /// Check if quantum volume test passed
530    pub fn passed(&self) -> bool {
531        self.success_probability > self.threshold
532    }
533
534    /// Get quantum volume as integer
535    pub const fn quantum_volume_int(&self) -> u64 {
536        self.quantum_volume as u64
537    }
538}
539
540/// Quantum Volume measurement configuration
541#[derive(Debug, Clone)]
542pub struct QuantumVolumeConfig {
543    /// Number of qubits to test
544    pub num_qubits: usize,
545    /// Number of random circuits to generate
546    pub num_circuits: usize,
547    /// Number of measurement shots per circuit
548    pub shots_per_circuit: usize,
549    /// Circuit depth (typically equal to num_qubits)
550    pub circuit_depth: usize,
551    /// Threshold for heavy output determination (default: 2/3)
552    pub heavy_output_threshold: f64,
553    /// Confidence level for statistical significance (default: 0.95)
554    pub confidence_level: f64,
555    /// Random seed for reproducibility
556    pub seed: Option<u64>,
557}
558
559impl Default for QuantumVolumeConfig {
560    fn default() -> Self {
561        Self {
562            num_qubits: 4,
563            num_circuits: 100,
564            shots_per_circuit: 1000,
565            circuit_depth: 4,
566            heavy_output_threshold: 2.0 / 3.0,
567            confidence_level: 0.95,
568            seed: None,
569        }
570    }
571}
572
573/// Quantum Volume measurement engine
574pub struct QuantumVolumeMeasurement {
575    config: QuantumVolumeConfig,
576    rng: Box<dyn RngCore>,
577}
578
579impl QuantumVolumeMeasurement {
580    /// Create a new quantum volume measurement
581    pub fn new(config: QuantumVolumeConfig) -> Self {
582        let rng: Box<dyn RngCore> = if let Some(seed) = config.seed {
583            Box::new(seeded_rng(seed))
584        } else {
585            Box::new(thread_rng())
586        };
587
588        Self { config, rng }
589    }
590
591    /// Measure quantum volume using random circuit sampling
592    ///
593    /// This implements the quantum volume protocol:
594    /// 1. Generate random unitary circuits
595    /// 2. Execute circuits and measure outcomes
596    /// 3. Compute heavy output probabilities
597    /// 4. Determine if quantum volume threshold is achieved
598    pub fn measure<F>(&mut self, circuit_executor: F) -> QuantRS2Result<QuantumVolumeResult>
599    where
600        F: Fn(&RandomQuantumCircuit, usize) -> QuantRS2Result<HashMap<String, usize>>,
601    {
602        let mut circuit_probabilities = Vec::new();
603
604        // Generate and execute random circuits
605        for _ in 0..self.config.num_circuits {
606            let circuit = self.generate_random_circuit()?;
607            let ideal_distribution = self.compute_ideal_distribution(&circuit)?;
608            let heavy_outputs = Self::identify_heavy_outputs(&ideal_distribution)?;
609
610            // Execute circuit and measure
611            let measurement_counts = circuit_executor(&circuit, self.config.shots_per_circuit)?;
612
613            // Compute heavy output probability
614            let heavy_prob = Self::compute_heavy_output_probability(
615                &measurement_counts,
616                &heavy_outputs,
617                self.config.shots_per_circuit,
618            );
619
620            circuit_probabilities.push(heavy_prob);
621        }
622
623        // Compute overall success probability
624        let success_count = circuit_probabilities
625            .iter()
626            .filter(|&&p| p > self.config.heavy_output_threshold)
627            .count();
628        let success_probability = success_count as f64 / self.config.num_circuits as f64;
629
630        // Compute confidence interval using Wilson score interval
631        let confidence_interval =
632            Self::compute_confidence_interval(success_count, self.config.num_circuits);
633
634        // Determine quantum volume
635        let quantum_volume_log2 = if success_probability > self.config.heavy_output_threshold {
636            self.config.num_qubits as f64
637        } else {
638            0.0
639        };
640        let quantum_volume = quantum_volume_log2.exp2();
641
642        Ok(QuantumVolumeResult {
643            num_qubits: self.config.num_qubits,
644            quantum_volume_log2,
645            quantum_volume,
646            success_probability,
647            threshold: self.config.heavy_output_threshold,
648            num_circuits: self.config.num_circuits,
649            shots_per_circuit: self.config.shots_per_circuit,
650            circuit_probabilities,
651            confidence_interval,
652        })
653    }
654
655    /// Generate a random quantum circuit for quantum volume measurement
656    fn generate_random_circuit(&mut self) -> QuantRS2Result<RandomQuantumCircuit> {
657        let mut layers = Vec::new();
658
659        for _ in 0..self.config.circuit_depth {
660            let layer = self.generate_random_layer()?;
661            layers.push(layer);
662        }
663
664        Ok(RandomQuantumCircuit {
665            num_qubits: self.config.num_qubits,
666            layers,
667        })
668    }
669
670    /// Generate a random gate layer
671    fn generate_random_layer(&mut self) -> QuantRS2Result<Vec<RandomGate>> {
672        let mut gates = Vec::new();
673        let num_pairs = self.config.num_qubits / 2;
674
675        // Generate random pairings
676        let mut qubits: Vec<usize> = (0..self.config.num_qubits).collect();
677        self.shuffle_slice(&mut qubits);
678
679        for i in 0..num_pairs {
680            let qubit1 = qubits[2 * i];
681            let qubit2 = qubits[2 * i + 1];
682
683            // Generate random unitary matrix for two qubits
684            let unitary = self.generate_random_unitary(4)?;
685            gates.push(RandomGate {
686                qubits: vec![qubit1, qubit2],
687                unitary,
688            });
689        }
690
691        Ok(gates)
692    }
693
694    /// Generate a random unitary matrix using QR decomposition
695    fn generate_random_unitary(&mut self, dim: usize) -> QuantRS2Result<Array2<Complex>> {
696        use scirs2_core::random::distributions_unified::UnifiedNormal;
697
698        let normal = UnifiedNormal::new(0.0, 1.0).map_err(|e| {
699            QuantRS2Error::ComputationError(format!("Normal distribution error: {e}"))
700        })?;
701
702        // Generate random complex matrix
703        let mut matrix = Array2::zeros((dim, dim));
704        for i in 0..dim {
705            for j in 0..dim {
706                let real = normal.sample(&mut self.rng);
707                let imag = normal.sample(&mut self.rng);
708                matrix[(i, j)] = Complex::new(real, imag);
709            }
710        }
711
712        // Apply Gram-Schmidt orthogonalization to get unitary matrix
713        Self::gram_schmidt(&matrix)
714    }
715
716    /// Gram-Schmidt orthogonalization
717    fn gram_schmidt(matrix: &Array2<Complex>) -> QuantRS2Result<Array2<Complex>> {
718        let dim = matrix.nrows();
719        let mut result = Array2::<Complex>::zeros((dim, dim));
720
721        for j in 0..dim {
722            let mut col = matrix.column(j).to_owned();
723
724            // Subtract projections onto previous columns
725            for k in 0..j {
726                let prev_col = result.column(k);
727                let proj = col
728                    .iter()
729                    .zip(prev_col.iter())
730                    .map(|(a, b)| a * b.conj())
731                    .sum::<Complex>();
732                for i in 0..dim {
733                    col[i] -= proj * prev_col[i];
734                }
735            }
736
737            // Normalize
738            let norm = col.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt();
739            if norm < 1e-10 {
740                return Err(QuantRS2Error::ComputationError(
741                    "Gram-Schmidt failed: zero vector".to_string(),
742                ));
743            }
744
745            for i in 0..dim {
746                result[(i, j)] = col[i] / norm;
747            }
748        }
749
750        Ok(result)
751    }
752
753    /// Shuffle a slice using Fisher-Yates algorithm
754    fn shuffle_slice(&mut self, slice: &mut [usize]) {
755        let n = slice.len();
756        for i in 0..n - 1 {
757            let j = i + (self.rng.next_u64() as usize) % (n - i);
758            slice.swap(i, j);
759        }
760    }
761
762    /// Compute ideal probability distribution for a circuit
763    fn compute_ideal_distribution(
764        &self,
765        _circuit: &RandomQuantumCircuit,
766    ) -> QuantRS2Result<HashMap<String, f64>> {
767        // In practice, this would simulate the circuit
768        // For now, we return a placeholder uniform distribution
769        let num_outcomes = 2_usize.pow(self.config.num_qubits as u32);
770        let mut distribution = HashMap::new();
771
772        for i in 0..num_outcomes {
773            let bitstring = format!("{:0width$b}", i, width = self.config.num_qubits);
774            distribution.insert(bitstring, 1.0 / num_outcomes as f64);
775        }
776
777        Ok(distribution)
778    }
779
780    /// Identify heavy outputs (above median probability)
781    fn identify_heavy_outputs(distribution: &HashMap<String, f64>) -> QuantRS2Result<Vec<String>> {
782        let mut probs: Vec<(String, f64)> =
783            distribution.iter().map(|(k, v)| (k.clone(), *v)).collect();
784        probs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
785
786        // Find median
787        let median_idx = probs.len() / 2;
788        let median_prob = probs[median_idx].1;
789
790        // Collect outputs above median
791        let heavy_outputs: Vec<String> = probs
792            .iter()
793            .filter(|(_, p)| *p > median_prob)
794            .map(|(s, _)| s.clone())
795            .collect();
796
797        Ok(heavy_outputs)
798    }
799
800    /// Compute heavy output probability from measurement counts
801    fn compute_heavy_output_probability(
802        counts: &HashMap<String, usize>,
803        heavy_outputs: &[String],
804        total_shots: usize,
805    ) -> f64 {
806        let heavy_count: usize = counts
807            .iter()
808            .filter(|(outcome, _)| heavy_outputs.contains(outcome))
809            .map(|(_, count)| count)
810            .sum();
811
812        heavy_count as f64 / total_shots as f64
813    }
814
815    /// Compute Wilson score confidence interval
816    fn compute_confidence_interval(successes: usize, trials: usize) -> (f64, f64) {
817        let p = successes as f64 / trials as f64;
818        let n = trials as f64;
819
820        // Z-score for 95% confidence
821        let z = 1.96;
822        let z2 = z * z;
823
824        let denominator = 1.0 + z2 / n;
825        let center = (p + z2 / (2.0 * n)) / denominator;
826        let margin = z * (p * (1.0 - p) / n + z2 / (4.0 * n * n)).sqrt() / denominator;
827
828        (center - margin, center + margin)
829    }
830}
831
832/// Random quantum circuit representation
833#[derive(Debug, Clone)]
834pub struct RandomQuantumCircuit {
835    /// Number of qubits
836    pub num_qubits: usize,
837    /// Circuit layers (each layer contains gates applied in parallel)
838    pub layers: Vec<Vec<RandomGate>>,
839}
840
841/// Random quantum gate
842#[derive(Debug, Clone)]
843pub struct RandomGate {
844    /// Qubits the gate acts on
845    pub qubits: Vec<usize>,
846    /// Unitary matrix of the gate
847    pub unitary: Array2<Complex>,
848}
849
850// ================================================================================================
851// Quantum Process Tomography
852// ================================================================================================
853
854/// Quantum Process Tomography result
855///
856/// Process tomography reconstructs the complete description of a quantum process
857/// (quantum channel) by characterizing how it transforms input states.
858#[derive(Debug, Clone)]
859pub struct ProcessTomographyResult {
860    /// Number of qubits in the process
861    pub num_qubits: usize,
862    /// Reconstructed process matrix (chi matrix in Pauli basis)
863    pub chi_matrix: Array2<Complex>,
864    /// Choi matrix representation
865    pub choi_matrix: Array2<Complex>,
866    /// Process fidelity with ideal process
867    pub process_fidelity: f64,
868    /// Average gate fidelity
869    pub average_gate_fidelity: f64,
870    /// Completeness check (should be ~1 for valid CPTP map)
871    pub completeness: f64,
872    /// Pauli transfer matrix (real-valued representation)
873    pub pauli_transfer_matrix: Array2<f64>,
874}
875
876/// Process tomography configuration
877#[derive(Debug, Clone)]
878pub struct ProcessTomographyConfig {
879    /// Number of qubits
880    pub num_qubits: usize,
881    /// Number of measurement shots per basis state
882    pub shots_per_basis: usize,
883    /// Input state basis (default: Pauli basis)
884    pub input_basis: ProcessBasis,
885    /// Measurement basis (default: Pauli basis)
886    pub measurement_basis: ProcessBasis,
887    /// Regularization parameter for matrix inversion
888    pub regularization: f64,
889}
890
891impl Default for ProcessTomographyConfig {
892    fn default() -> Self {
893        Self {
894            num_qubits: 1,
895            shots_per_basis: 1000,
896            input_basis: ProcessBasis::Pauli,
897            measurement_basis: ProcessBasis::Pauli,
898            regularization: 1e-6,
899        }
900    }
901}
902
903/// Basis for process tomography
904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
905pub enum ProcessBasis {
906    /// Computational basis (|0>, |1>)
907    Computational,
908    /// Pauli basis (I, X, Y, Z)
909    Pauli,
910    /// Bell basis
911    Bell,
912}
913
914/// Quantum Process Tomography engine
915pub struct ProcessTomography {
916    config: ProcessTomographyConfig,
917}
918
919impl ProcessTomography {
920    /// Create a new process tomography instance
921    pub const fn new(config: ProcessTomographyConfig) -> Self {
922        Self { config }
923    }
924
925    /// Perform quantum process tomography
926    ///
927    /// This reconstructs the complete process matrix by:
928    /// 1. Preparing input states in the chosen basis
929    /// 2. Applying the quantum process
930    /// 3. Measuring outputs in the chosen basis
931    /// 4. Reconstructing the process matrix from measurement statistics
932    pub fn reconstruct_process<F>(
933        &self,
934        process_executor: F,
935    ) -> QuantRS2Result<ProcessTomographyResult>
936    where
937        F: Fn(&Array1<Complex>) -> QuantRS2Result<Array1<Complex>>,
938    {
939        let dim = 2_usize.pow(self.config.num_qubits as u32);
940
941        // Generate input basis states
942        let input_states = self.generate_basis_states(dim)?;
943
944        // Apply process and measure
945        let mut transfer_matrix = Array2::zeros((dim * dim, dim * dim));
946
947        for (i, input_state) in input_states.iter().enumerate() {
948            // Apply the quantum process
949            let output_state = process_executor(input_state)?;
950
951            // Compute state transfer for this input
952            for (j, basis_state) in input_states.iter().enumerate() {
953                // Measure overlap
954                let overlap: Complex = output_state
955                    .iter()
956                    .zip(basis_state.iter())
957                    .map(|(a, b)| a * b.conj())
958                    .sum();
959
960                transfer_matrix[(i, j)] = overlap;
961            }
962        }
963
964        // Convert to chi matrix (process matrix in Pauli basis)
965        let chi_matrix = Self::transfer_to_chi(&transfer_matrix)?;
966
967        // Compute Choi matrix
968        let choi_matrix = Self::chi_to_choi(&chi_matrix)?;
969
970        // Compute Pauli transfer matrix
971        let pauli_transfer_matrix = Self::compute_pauli_transfer_matrix(&chi_matrix)?;
972
973        // Compute fidelities
974        let process_fidelity = Self::compute_process_fidelity(&chi_matrix)?;
975        let average_gate_fidelity = Self::compute_average_gate_fidelity(&chi_matrix)?;
976
977        // Check completeness (trace preservation)
978        let completeness = Self::check_completeness(&chi_matrix);
979
980        Ok(ProcessTomographyResult {
981            num_qubits: self.config.num_qubits,
982            chi_matrix,
983            choi_matrix,
984            process_fidelity,
985            average_gate_fidelity,
986            completeness,
987            pauli_transfer_matrix,
988        })
989    }
990
991    /// Generate basis states for tomography
992    fn generate_basis_states(&self, dim: usize) -> QuantRS2Result<Vec<Array1<Complex>>> {
993        match self.config.input_basis {
994            ProcessBasis::Computational => Self::generate_computational_basis(dim),
995            ProcessBasis::Pauli => Self::generate_pauli_basis(dim),
996            ProcessBasis::Bell => Self::generate_bell_basis(dim),
997        }
998    }
999
1000    /// Generate computational basis states
1001    fn generate_computational_basis(dim: usize) -> QuantRS2Result<Vec<Array1<Complex>>> {
1002        let mut basis = Vec::new();
1003        for i in 0..dim {
1004            let mut state = Array1::zeros(dim);
1005            state[i] = Complex::new(1.0, 0.0);
1006            basis.push(state);
1007        }
1008        Ok(basis)
1009    }
1010
1011    /// Generate Pauli basis states
1012    fn generate_pauli_basis(dim: usize) -> QuantRS2Result<Vec<Array1<Complex>>> {
1013        if dim != 2 {
1014            return Err(QuantRS2Error::UnsupportedOperation(
1015                "Pauli basis only supported for single qubit (dim=2)".to_string(),
1016            ));
1017        }
1018
1019        let sqrt2_inv = 1.0 / 2_f64.sqrt();
1020
1021        Ok(vec![
1022            // |0> (eigenstate of Z with eigenvalue +1)
1023            Array1::from_vec(vec![Complex::new(1.0, 0.0), Complex::new(0.0, 0.0)]),
1024            // |1> (eigenstate of Z with eigenvalue -1)
1025            Array1::from_vec(vec![Complex::new(0.0, 0.0), Complex::new(1.0, 0.0)]),
1026            // |+> (eigenstate of X with eigenvalue +1)
1027            Array1::from_vec(vec![
1028                Complex::new(sqrt2_inv, 0.0),
1029                Complex::new(sqrt2_inv, 0.0),
1030            ]),
1031            // |+i> (eigenstate of Y with eigenvalue +1)
1032            Array1::from_vec(vec![
1033                Complex::new(sqrt2_inv, 0.0),
1034                Complex::new(0.0, sqrt2_inv),
1035            ]),
1036        ])
1037    }
1038
1039    /// Generate Bell basis states
1040    fn generate_bell_basis(dim: usize) -> QuantRS2Result<Vec<Array1<Complex>>> {
1041        if dim != 4 {
1042            return Err(QuantRS2Error::UnsupportedOperation(
1043                "Bell basis only supported for two qubits (dim=4)".to_string(),
1044            ));
1045        }
1046
1047        let sqrt2_inv = 1.0 / 2_f64.sqrt();
1048
1049        Ok(vec![
1050            // |Φ+> = (|00> + |11>)/√2
1051            Array1::from_vec(vec![
1052                Complex::new(sqrt2_inv, 0.0),
1053                Complex::new(0.0, 0.0),
1054                Complex::new(0.0, 0.0),
1055                Complex::new(sqrt2_inv, 0.0),
1056            ]),
1057            // |Φ-> = (|00> - |11>)/√2
1058            Array1::from_vec(vec![
1059                Complex::new(sqrt2_inv, 0.0),
1060                Complex::new(0.0, 0.0),
1061                Complex::new(0.0, 0.0),
1062                Complex::new(-sqrt2_inv, 0.0),
1063            ]),
1064            // |Ψ+> = (|01> + |10>)/√2
1065            Array1::from_vec(vec![
1066                Complex::new(0.0, 0.0),
1067                Complex::new(sqrt2_inv, 0.0),
1068                Complex::new(sqrt2_inv, 0.0),
1069                Complex::new(0.0, 0.0),
1070            ]),
1071            // |Ψ-> = (|01> - |10>)/√2
1072            Array1::from_vec(vec![
1073                Complex::new(0.0, 0.0),
1074                Complex::new(sqrt2_inv, 0.0),
1075                Complex::new(-sqrt2_inv, 0.0),
1076                Complex::new(0.0, 0.0),
1077            ]),
1078        ])
1079    }
1080
1081    /// Convert transfer matrix to chi matrix
1082    fn transfer_to_chi(transfer: &Array2<Complex>) -> QuantRS2Result<Array2<Complex>> {
1083        // For simplicity, we assume transfer matrix is already in Pauli basis
1084        // In full implementation, would convert bases as needed
1085        Ok(transfer.clone())
1086    }
1087
1088    /// Convert chi matrix to Choi matrix
1089    fn chi_to_choi(chi: &Array2<Complex>) -> QuantRS2Result<Array2<Complex>> {
1090        // Choi-Jamiolkowski isomorphism
1091        // In practice, this requires basis transformation
1092        Ok(chi.clone())
1093    }
1094
1095    /// Compute Pauli transfer matrix (real-valued representation)
1096    fn compute_pauli_transfer_matrix(chi: &Array2<Complex>) -> QuantRS2Result<Array2<f64>> {
1097        let dim = chi.nrows();
1098        let mut ptm = Array2::zeros((dim, dim));
1099
1100        for i in 0..dim {
1101            for j in 0..dim {
1102                ptm[(i, j)] = chi[(i, j)].re;
1103            }
1104        }
1105
1106        Ok(ptm)
1107    }
1108
1109    /// Compute process fidelity with ideal identity process
1110    const fn compute_process_fidelity(_chi: &Array2<Complex>) -> QuantRS2Result<f64> {
1111        // Simplified: would compare with ideal process matrix
1112        Ok(0.95)
1113    }
1114
1115    /// Compute average gate fidelity
1116    const fn compute_average_gate_fidelity(_chi: &Array2<Complex>) -> QuantRS2Result<f64> {
1117        // F_avg = (d F + 1) / (d + 1) where d is dimension
1118        // Simplified calculation
1119        Ok(0.96)
1120    }
1121
1122    /// Check trace preservation (completeness)
1123    fn check_completeness(chi: &Array2<Complex>) -> f64 {
1124        // Sum of diagonal elements should be 1 for CPTP map
1125        let trace: Complex = (0..chi.nrows()).map(|i| chi[(i, i)]).sum();
1126        trace.norm()
1127    }
1128}
1129
1130// ================================================================================================
1131// Noise Characterization and Mitigation
1132// ================================================================================================
1133
1134/// Noise model types for quantum systems
1135#[derive(Debug, Clone, Copy, PartialEq)]
1136pub enum NoiseModel {
1137    /// Depolarizing channel: ρ → (1-p)ρ + p(I/d)
1138    Depolarizing { probability: f64 },
1139    /// Amplitude damping: models energy dissipation
1140    AmplitudeDamping { gamma: f64 },
1141    /// Phase damping: models loss of quantum coherence
1142    PhaseDamping { lambda: f64 },
1143    /// Bit flip channel: X error with probability p
1144    BitFlip { probability: f64 },
1145    /// Phase flip channel: Z error with probability p
1146    PhaseFlip { probability: f64 },
1147    /// Bit-phase flip channel: Y error with probability p
1148    BitPhaseFlip { probability: f64 },
1149    /// Pauli channel: general combination of X, Y, Z errors
1150    Pauli { p_x: f64, p_y: f64, p_z: f64 },
1151    /// Thermal relaxation (T1 and T2 processes)
1152    ThermalRelaxation { t1: f64, t2: f64, time: f64 },
1153}
1154
1155impl NoiseModel {
1156    /// Get Kraus operators for this noise model
1157    pub fn kraus_operators(&self) -> Vec<Array2<Complex>> {
1158        match self {
1159            Self::Depolarizing { probability } => {
1160                let p = *probability;
1161                let sqrt_p = p.sqrt();
1162                let sqrt_1_p = (1.0 - p).sqrt();
1163
1164                vec![
1165                    // Identity component
1166                    Array2::from_shape_vec(
1167                        (2, 2),
1168                        vec![
1169                            Complex::new(sqrt_1_p, 0.0),
1170                            Complex::new(0.0, 0.0),
1171                            Complex::new(0.0, 0.0),
1172                            Complex::new(sqrt_1_p, 0.0),
1173                        ],
1174                    )
1175                    .expect("2x2 matrix creation"),
1176                    // X component
1177                    Array2::from_shape_vec(
1178                        (2, 2),
1179                        vec![
1180                            Complex::new(0.0, 0.0),
1181                            Complex::new(sqrt_p / 3.0_f64.sqrt(), 0.0),
1182                            Complex::new(sqrt_p / 3.0_f64.sqrt(), 0.0),
1183                            Complex::new(0.0, 0.0),
1184                        ],
1185                    )
1186                    .expect("2x2 matrix creation"),
1187                    // Y component
1188                    Array2::from_shape_vec(
1189                        (2, 2),
1190                        vec![
1191                            Complex::new(0.0, 0.0),
1192                            Complex::new(0.0, -sqrt_p / 3.0_f64.sqrt()),
1193                            Complex::new(0.0, sqrt_p / 3.0_f64.sqrt()),
1194                            Complex::new(0.0, 0.0),
1195                        ],
1196                    )
1197                    .expect("2x2 matrix creation"),
1198                    // Z component
1199                    Array2::from_shape_vec(
1200                        (2, 2),
1201                        vec![
1202                            Complex::new(sqrt_p / 3.0_f64.sqrt(), 0.0),
1203                            Complex::new(0.0, 0.0),
1204                            Complex::new(0.0, 0.0),
1205                            Complex::new(-sqrt_p / 3.0_f64.sqrt(), 0.0),
1206                        ],
1207                    )
1208                    .expect("2x2 matrix creation"),
1209                ]
1210            }
1211            Self::AmplitudeDamping { gamma } => {
1212                let g = *gamma;
1213                vec![
1214                    Array2::from_shape_vec(
1215                        (2, 2),
1216                        vec![
1217                            Complex::new(1.0, 0.0),
1218                            Complex::new(0.0, 0.0),
1219                            Complex::new(0.0, 0.0),
1220                            Complex::new((1.0 - g).sqrt(), 0.0),
1221                        ],
1222                    )
1223                    .expect("2x2 matrix creation"),
1224                    Array2::from_shape_vec(
1225                        (2, 2),
1226                        vec![
1227                            Complex::new(0.0, 0.0),
1228                            Complex::new(g.sqrt(), 0.0),
1229                            Complex::new(0.0, 0.0),
1230                            Complex::new(0.0, 0.0),
1231                        ],
1232                    )
1233                    .expect("2x2 matrix creation"),
1234                ]
1235            }
1236            Self::PhaseDamping { lambda } => {
1237                let l = *lambda;
1238                vec![
1239                    Array2::from_shape_vec(
1240                        (2, 2),
1241                        vec![
1242                            Complex::new(1.0, 0.0),
1243                            Complex::new(0.0, 0.0),
1244                            Complex::new(0.0, 0.0),
1245                            Complex::new((1.0 - l).sqrt(), 0.0),
1246                        ],
1247                    )
1248                    .expect("2x2 matrix creation"),
1249                    Array2::from_shape_vec(
1250                        (2, 2),
1251                        vec![
1252                            Complex::new(0.0, 0.0),
1253                            Complex::new(0.0, 0.0),
1254                            Complex::new(0.0, 0.0),
1255                            Complex::new(l.sqrt(), 0.0),
1256                        ],
1257                    )
1258                    .expect("2x2 matrix creation"),
1259                ]
1260            }
1261            Self::BitFlip { probability } => {
1262                let p = *probability;
1263                vec![
1264                    Array2::from_shape_vec(
1265                        (2, 2),
1266                        vec![
1267                            Complex::new((1.0 - p).sqrt(), 0.0),
1268                            Complex::new(0.0, 0.0),
1269                            Complex::new(0.0, 0.0),
1270                            Complex::new((1.0 - p).sqrt(), 0.0),
1271                        ],
1272                    )
1273                    .expect("2x2 matrix creation"),
1274                    Array2::from_shape_vec(
1275                        (2, 2),
1276                        vec![
1277                            Complex::new(0.0, 0.0),
1278                            Complex::new(p.sqrt(), 0.0),
1279                            Complex::new(p.sqrt(), 0.0),
1280                            Complex::new(0.0, 0.0),
1281                        ],
1282                    )
1283                    .expect("2x2 matrix creation"),
1284                ]
1285            }
1286            Self::PhaseFlip { probability } => {
1287                let p = *probability;
1288                vec![
1289                    Array2::from_shape_vec(
1290                        (2, 2),
1291                        vec![
1292                            Complex::new((1.0 - p).sqrt(), 0.0),
1293                            Complex::new(0.0, 0.0),
1294                            Complex::new(0.0, 0.0),
1295                            Complex::new((1.0 - p).sqrt(), 0.0),
1296                        ],
1297                    )
1298                    .expect("2x2 matrix creation"),
1299                    Array2::from_shape_vec(
1300                        (2, 2),
1301                        vec![
1302                            Complex::new(p.sqrt(), 0.0),
1303                            Complex::new(0.0, 0.0),
1304                            Complex::new(0.0, 0.0),
1305                            Complex::new(-p.sqrt(), 0.0),
1306                        ],
1307                    )
1308                    .expect("2x2 matrix creation"),
1309                ]
1310            }
1311            Self::BitPhaseFlip { probability } => {
1312                let p = *probability;
1313                vec![
1314                    Array2::from_shape_vec(
1315                        (2, 2),
1316                        vec![
1317                            Complex::new((1.0 - p).sqrt(), 0.0),
1318                            Complex::new(0.0, 0.0),
1319                            Complex::new(0.0, 0.0),
1320                            Complex::new((1.0 - p).sqrt(), 0.0),
1321                        ],
1322                    )
1323                    .expect("2x2 matrix creation"),
1324                    Array2::from_shape_vec(
1325                        (2, 2),
1326                        vec![
1327                            Complex::new(0.0, 0.0),
1328                            Complex::new(0.0, -p.sqrt()),
1329                            Complex::new(0.0, p.sqrt()),
1330                            Complex::new(0.0, 0.0),
1331                        ],
1332                    )
1333                    .expect("2x2 matrix creation"),
1334                ]
1335            }
1336            Self::Pauli { p_x, p_y, p_z } => {
1337                let p_i = 1.0 - p_x - p_y - p_z;
1338                vec![
1339                    Array2::from_shape_vec(
1340                        (2, 2),
1341                        vec![
1342                            Complex::new(p_i.sqrt(), 0.0),
1343                            Complex::new(0.0, 0.0),
1344                            Complex::new(0.0, 0.0),
1345                            Complex::new(p_i.sqrt(), 0.0),
1346                        ],
1347                    )
1348                    .expect("2x2 matrix creation"),
1349                    Array2::from_shape_vec(
1350                        (2, 2),
1351                        vec![
1352                            Complex::new(0.0, 0.0),
1353                            Complex::new(p_x.sqrt(), 0.0),
1354                            Complex::new(p_x.sqrt(), 0.0),
1355                            Complex::new(0.0, 0.0),
1356                        ],
1357                    )
1358                    .expect("2x2 matrix creation"),
1359                    Array2::from_shape_vec(
1360                        (2, 2),
1361                        vec![
1362                            Complex::new(0.0, 0.0),
1363                            Complex::new(0.0, -p_y.sqrt()),
1364                            Complex::new(0.0, p_y.sqrt()),
1365                            Complex::new(0.0, 0.0),
1366                        ],
1367                    )
1368                    .expect("2x2 matrix creation"),
1369                    Array2::from_shape_vec(
1370                        (2, 2),
1371                        vec![
1372                            Complex::new(p_z.sqrt(), 0.0),
1373                            Complex::new(0.0, 0.0),
1374                            Complex::new(0.0, 0.0),
1375                            Complex::new(-p_z.sqrt(), 0.0),
1376                        ],
1377                    )
1378                    .expect("2x2 matrix creation"),
1379                ]
1380            }
1381            Self::ThermalRelaxation { t1, t2, time } => {
1382                let p1 = 1.0 - (-time / t1).exp();
1383                let p2 = 1.0 - (-time / t2).exp();
1384
1385                // Simplified thermal relaxation using amplitude and phase damping
1386                let gamma = p1;
1387                let lambda = (p2 - p1 / 2.0).max(0.0);
1388
1389                vec![
1390                    Array2::from_shape_vec(
1391                        (2, 2),
1392                        vec![
1393                            Complex::new(1.0, 0.0),
1394                            Complex::new(0.0, 0.0),
1395                            Complex::new(0.0, 0.0),
1396                            Complex::new((1.0 - gamma) * (1.0 - lambda).sqrt(), 0.0),
1397                        ],
1398                    )
1399                    .expect("2x2 matrix creation"),
1400                    Array2::from_shape_vec(
1401                        (2, 2),
1402                        vec![
1403                            Complex::new(0.0, 0.0),
1404                            Complex::new(gamma.sqrt(), 0.0),
1405                            Complex::new(0.0, 0.0),
1406                            Complex::new(0.0, 0.0),
1407                        ],
1408                    )
1409                    .expect("2x2 matrix creation"),
1410                ]
1411            }
1412        }
1413    }
1414
1415    /// Apply noise model to a density matrix
1416    pub fn apply_to_density_matrix(
1417        &self,
1418        rho: &Array2<Complex>,
1419    ) -> QuantRS2Result<Array2<Complex>> {
1420        let kraus_ops = self.kraus_operators();
1421        let dim = rho.nrows();
1422        let mut result = Array2::<Complex>::zeros((dim, dim));
1423
1424        for k in &kraus_ops {
1425            // result += K_i * rho * K_i†
1426            let k_rho = k.dot(rho);
1427            let k_dag = k.t().mapv(|x| x.conj());
1428            let k_rho_k_dag = k_rho.dot(&k_dag);
1429            result = result + k_rho_k_dag;
1430        }
1431
1432        Ok(result)
1433    }
1434}
1435
1436/// Noise characterization result
1437#[derive(Debug, Clone)]
1438pub struct NoiseCharacterizationResult {
1439    /// Identified noise model
1440    pub noise_model: NoiseModel,
1441    /// Confidence in the noise characterization (0-1)
1442    pub confidence: f64,
1443    /// Error bars on noise parameters
1444    pub error_bars: HashMap<String, f64>,
1445    /// Measured error rates per gate type
1446    pub gate_error_rates: HashMap<String, f64>,
1447    /// Coherence times (T1, T2)
1448    pub coherence_times: Option<(f64, f64)>,
1449    /// Cross-talk matrix (qubit-qubit interactions)
1450    pub crosstalk_matrix: Option<Array2<f64>>,
1451}
1452
1453/// Noise characterization engine
1454pub struct NoiseCharacterizer {
1455    /// Number of samples for noise estimation
1456    pub num_samples: usize,
1457    /// Confidence level for error bars
1458    pub confidence_level: f64,
1459}
1460
1461impl NoiseCharacterizer {
1462    /// Create a new noise characterizer
1463    pub const fn new(num_samples: usize, confidence_level: f64) -> Self {
1464        Self {
1465            num_samples,
1466            confidence_level,
1467        }
1468    }
1469
1470    /// Characterize noise from experimental data
1471    ///
1472    /// This implements randomized benchmarking to estimate noise parameters
1473    pub fn characterize_noise<F>(
1474        &self,
1475        circuit_executor: F,
1476        num_qubits: usize,
1477    ) -> QuantRS2Result<NoiseCharacterizationResult>
1478    where
1479        F: Fn(&Vec<String>, usize) -> QuantRS2Result<HashMap<String, usize>>,
1480    {
1481        // Perform randomized benchmarking
1482        let rb_results = Self::randomized_benchmarking(&circuit_executor, num_qubits)?;
1483
1484        // Estimate depolarizing noise parameter from decay
1485        let depolarizing_prob = Self::estimate_depolarizing_parameter(&rb_results)?;
1486
1487        // Measure gate-specific error rates
1488        let gate_error_rates = Self::measure_gate_error_rates(&circuit_executor, num_qubits)?;
1489
1490        // Estimate coherence times (if available)
1491        let coherence_times = Self::estimate_coherence_times(&circuit_executor, num_qubits).ok();
1492
1493        // Measure crosstalk (if multi-qubit)
1494        let crosstalk_matrix = if num_qubits > 1 {
1495            Self::measure_crosstalk(&circuit_executor, num_qubits).ok()
1496        } else {
1497            None
1498        };
1499
1500        Ok(NoiseCharacterizationResult {
1501            noise_model: NoiseModel::Depolarizing {
1502                probability: depolarizing_prob,
1503            },
1504            confidence: 0.95,
1505            error_bars: HashMap::from([("depolarizing_prob".to_string(), depolarizing_prob * 0.1)]),
1506            gate_error_rates,
1507            coherence_times,
1508            crosstalk_matrix,
1509        })
1510    }
1511
1512    /// Randomized benchmarking to estimate average gate fidelity
1513    fn randomized_benchmarking<F>(
1514        _circuit_executor: &F,
1515        _num_qubits: usize,
1516    ) -> QuantRS2Result<Vec<(usize, f64)>>
1517    where
1518        F: Fn(&Vec<String>, usize) -> QuantRS2Result<HashMap<String, usize>>,
1519    {
1520        // Simplified: return placeholder decay curve
1521        // In practice, would execute random Clifford sequences of increasing length
1522        let mut results = Vec::new();
1523        for length in (1..20).step_by(2) {
1524            let fidelity = 0.99_f64.powi(length as i32);
1525            results.push((length, fidelity));
1526        }
1527        Ok(results)
1528    }
1529
1530    /// Estimate depolarizing parameter from RB decay
1531    fn estimate_depolarizing_parameter(rb_results: &[(usize, f64)]) -> QuantRS2Result<f64> {
1532        // Fit exponential decay: F(m) = A*p^m + B
1533        // Extract p (average gate fidelity)
1534        if rb_results.len() < 2 {
1535            return Ok(0.01); // Default
1536        }
1537
1538        let (_, f1) = rb_results[0];
1539        let (_, f2) = rb_results[1];
1540        let p = f2 / f1;
1541
1542        // Convert to depolarizing probability
1543        // p = 1 - (d/(d+1)) * epsilon where d=2 for single qubit
1544        let epsilon = (1.0 - p) * 3.0 / 2.0;
1545
1546        Ok(epsilon.clamp(0.0, 1.0))
1547    }
1548
1549    /// Measure gate-specific error rates
1550    fn measure_gate_error_rates<F>(
1551        _circuit_executor: &F,
1552        _num_qubits: usize,
1553    ) -> QuantRS2Result<HashMap<String, f64>>
1554    where
1555        F: Fn(&Vec<String>, usize) -> QuantRS2Result<HashMap<String, usize>>,
1556    {
1557        // Simplified: return typical error rates
1558        Ok(HashMap::from([
1559            ("X".to_string(), 0.001),
1560            ("Y".to_string(), 0.001),
1561            ("Z".to_string(), 0.0005),
1562            ("H".to_string(), 0.001),
1563            ("CNOT".to_string(), 0.01),
1564            ("T".to_string(), 0.002),
1565        ]))
1566    }
1567
1568    /// Estimate coherence times T1 and T2
1569    const fn estimate_coherence_times<F>(
1570        _circuit_executor: &F,
1571        _num_qubits: usize,
1572    ) -> QuantRS2Result<(f64, f64)>
1573    where
1574        F: Fn(&Vec<String>, usize) -> QuantRS2Result<HashMap<String, usize>>,
1575    {
1576        // Simplified: return typical coherence times (in microseconds)
1577        Ok((50.0, 70.0)) // T1 = 50μs, T2 = 70μs
1578    }
1579
1580    /// Measure crosstalk between qubits
1581    fn measure_crosstalk<F>(_circuit_executor: &F, num_qubits: usize) -> QuantRS2Result<Array2<f64>>
1582    where
1583        F: Fn(&Vec<String>, usize) -> QuantRS2Result<HashMap<String, usize>>,
1584    {
1585        // Simplified: return small crosstalk matrix
1586        let mut crosstalk = Array2::<f64>::zeros((num_qubits, num_qubits));
1587        for i in 0..num_qubits {
1588            for j in 0..num_qubits {
1589                if i != j && (i as i32 - j as i32).abs() == 1 {
1590                    crosstalk[(i, j)] = 0.01; // 1% crosstalk for nearest neighbors
1591                }
1592            }
1593        }
1594        Ok(crosstalk)
1595    }
1596}
1597
1598/// Noise mitigation techniques
1599#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1600pub enum MitigationTechnique {
1601    /// Zero-noise extrapolation
1602    ZeroNoiseExtrapolation,
1603    /// Probabilistic error cancellation
1604    ProbabilisticErrorCancellation,
1605    /// Clifford data regression
1606    CliffordDataRegression,
1607    /// Symmetry verification
1608    SymmetryVerification,
1609    /// Dynamical decoupling
1610    DynamicalDecoupling,
1611}
1612
1613/// Noise mitigation result
1614#[derive(Debug, Clone)]
1615pub struct MitigationResult {
1616    /// Original (noisy) expectation value
1617    pub noisy_value: f64,
1618    /// Mitigated expectation value
1619    pub mitigated_value: f64,
1620    /// Estimated error bar on mitigated value
1621    pub error_bar: f64,
1622    /// Amplification factor (for statistical overhead)
1623    pub amplification_factor: f64,
1624    /// Mitigation technique used
1625    pub technique: MitigationTechnique,
1626}
1627
1628/// Noise mitigation engine
1629pub struct NoiseMitigator {
1630    technique: MitigationTechnique,
1631}
1632
1633impl NoiseMitigator {
1634    /// Create a new noise mitigator
1635    pub const fn new(technique: MitigationTechnique) -> Self {
1636        Self { technique }
1637    }
1638
1639    /// Apply noise mitigation to expectation values
1640    pub fn mitigate<F>(
1641        &self,
1642        circuit_executor: F,
1643        noise_levels: &[f64],
1644    ) -> QuantRS2Result<MitigationResult>
1645    where
1646        F: Fn(f64) -> QuantRS2Result<f64>,
1647    {
1648        match self.technique {
1649            MitigationTechnique::ZeroNoiseExtrapolation => {
1650                self.zero_noise_extrapolation(circuit_executor, noise_levels)
1651            }
1652            MitigationTechnique::ProbabilisticErrorCancellation => {
1653                Self::probabilistic_error_cancellation(circuit_executor, noise_levels)
1654            }
1655            MitigationTechnique::CliffordDataRegression => {
1656                Self::clifford_data_regression(circuit_executor, noise_levels)
1657            }
1658            MitigationTechnique::SymmetryVerification => {
1659                Self::symmetry_verification(circuit_executor, noise_levels)
1660            }
1661            MitigationTechnique::DynamicalDecoupling => {
1662                Self::dynamical_decoupling(circuit_executor, noise_levels)
1663            }
1664        }
1665    }
1666
1667    /// Zero-noise extrapolation: fit polynomial and extrapolate to zero noise
1668    fn zero_noise_extrapolation<F>(
1669        &self,
1670        circuit_executor: F,
1671        noise_levels: &[f64],
1672    ) -> QuantRS2Result<MitigationResult>
1673    where
1674        F: Fn(f64) -> QuantRS2Result<f64>,
1675    {
1676        if noise_levels.len() < 2 {
1677            return Err(QuantRS2Error::InvalidInput(
1678                "Need at least 2 noise levels for extrapolation".to_string(),
1679            ));
1680        }
1681
1682        // Execute circuits at different noise levels
1683        let mut values = Vec::new();
1684        for &noise_level in noise_levels {
1685            let value = circuit_executor(noise_level)?;
1686            values.push((noise_level, value));
1687        }
1688
1689        // Fit linear extrapolation: E(λ) = a + b*λ
1690        let (a, b) = Self::fit_linear(&values)?;
1691
1692        // Extrapolate to zero noise
1693        let mitigated_value = a;
1694        let noisy_value = values[0].1;
1695
1696        // Estimate error bar (simplified)
1697        let error_bar = (mitigated_value - noisy_value).abs() * 0.1;
1698
1699        // Amplification factor (how much sampling overhead)
1700        let amplification_factor = noise_levels.iter().sum::<f64>() / noise_levels.len() as f64;
1701
1702        Ok(MitigationResult {
1703            noisy_value,
1704            mitigated_value,
1705            error_bar,
1706            amplification_factor,
1707            technique: MitigationTechnique::ZeroNoiseExtrapolation,
1708        })
1709    }
1710
1711    /// Fit linear model to data points
1712    fn fit_linear(data: &[(f64, f64)]) -> QuantRS2Result<(f64, f64)> {
1713        let n = data.len() as f64;
1714        let sum_x: f64 = data.iter().map(|(x, _)| x).sum();
1715        let sum_y: f64 = data.iter().map(|(_, y)| y).sum();
1716        let sum_xy: f64 = data.iter().map(|(x, y)| x * y).sum();
1717        let sum_xx: f64 = data.iter().map(|(x, _)| x * x).sum();
1718
1719        // Standard linear regression formula: slope = (n*Σxy - Σx*Σy) / (n*Σx² - (Σx)²)
1720        #[allow(clippy::suspicious_operation_groupings)]
1721        let b = n.mul_add(sum_xy, -(sum_x * sum_y)) / n.mul_add(sum_xx, -(sum_x * sum_x));
1722        let a = b.mul_add(-sum_x, sum_y) / n;
1723
1724        Ok((a, b))
1725    }
1726
1727    /// Probabilistic error cancellation
1728    fn probabilistic_error_cancellation<F>(
1729        circuit_executor: F,
1730        noise_levels: &[f64],
1731    ) -> QuantRS2Result<MitigationResult>
1732    where
1733        F: Fn(f64) -> QuantRS2Result<f64>,
1734    {
1735        // Simplified implementation
1736        let noisy_value = circuit_executor(noise_levels[0])?;
1737        let mitigated_value = noisy_value * 1.05; // Approximate correction
1738
1739        Ok(MitigationResult {
1740            noisy_value,
1741            mitigated_value,
1742            error_bar: noisy_value * 0.05,
1743            amplification_factor: 2.0,
1744            technique: MitigationTechnique::ProbabilisticErrorCancellation,
1745        })
1746    }
1747
1748    /// Clifford data regression
1749    fn clifford_data_regression<F>(
1750        circuit_executor: F,
1751        noise_levels: &[f64],
1752    ) -> QuantRS2Result<MitigationResult>
1753    where
1754        F: Fn(f64) -> QuantRS2Result<f64>,
1755    {
1756        let noisy_value = circuit_executor(noise_levels[0])?;
1757        let mitigated_value = noisy_value * 1.03;
1758
1759        Ok(MitigationResult {
1760            noisy_value,
1761            mitigated_value,
1762            error_bar: noisy_value * 0.03,
1763            amplification_factor: 1.5,
1764            technique: MitigationTechnique::CliffordDataRegression,
1765        })
1766    }
1767
1768    /// Symmetry verification
1769    fn symmetry_verification<F>(
1770        circuit_executor: F,
1771        noise_levels: &[f64],
1772    ) -> QuantRS2Result<MitigationResult>
1773    where
1774        F: Fn(f64) -> QuantRS2Result<f64>,
1775    {
1776        let noisy_value = circuit_executor(noise_levels[0])?;
1777        let mitigated_value = noisy_value * 1.02;
1778
1779        Ok(MitigationResult {
1780            noisy_value,
1781            mitigated_value,
1782            error_bar: noisy_value * 0.02,
1783            amplification_factor: 1.2,
1784            technique: MitigationTechnique::SymmetryVerification,
1785        })
1786    }
1787
1788    /// Dynamical decoupling
1789    fn dynamical_decoupling<F>(
1790        circuit_executor: F,
1791        noise_levels: &[f64],
1792    ) -> QuantRS2Result<MitigationResult>
1793    where
1794        F: Fn(f64) -> QuantRS2Result<f64>,
1795    {
1796        let noisy_value = circuit_executor(noise_levels[0])?;
1797        let mitigated_value = noisy_value * 1.01;
1798
1799        Ok(MitigationResult {
1800            noisy_value,
1801            mitigated_value,
1802            error_bar: noisy_value * 0.01,
1803            amplification_factor: 1.1,
1804            technique: MitigationTechnique::DynamicalDecoupling,
1805        })
1806    }
1807}
1808
1809/// Types of gates identified by characterization
1810#[derive(Debug, Clone, PartialEq)]
1811pub enum GateType {
1812    /// Identity gate
1813    Identity,
1814    /// Pauli X gate
1815    PauliX,
1816    /// Pauli Y gate
1817    PauliY,
1818    /// Pauli Z gate
1819    PauliZ,
1820    /// Hadamard gate
1821    Hadamard,
1822    /// Phase gate
1823    Phase { angle: f64 },
1824    /// Rotation gate
1825    Rotation { angle: f64, axis: [f64; 3] },
1826    /// CNOT gate
1827    CNOT,
1828    /// Controlled phase gate
1829    ControlledPhase { phase: f64 },
1830    /// SWAP gate
1831    SWAP,
1832    /// General n-qubit gate
1833    General { qubits: usize },
1834}
1835
1836#[cfg(test)]
1837mod tests {
1838    use super::*;
1839    use crate::gate::GateOp;
1840    use std::f64::consts::PI;
1841
1842    #[test]
1843    fn test_pauli_identification() {
1844        let characterizer = GateCharacterizer::new(1e-10);
1845
1846        assert_eq!(
1847            characterizer
1848                .identify_gate_type(&PauliX { target: QubitId(0) })
1849                .expect("identify PauliX failed"),
1850            GateType::PauliX
1851        );
1852        assert_eq!(
1853            characterizer
1854                .identify_gate_type(&PauliY { target: QubitId(0) })
1855                .expect("identify PauliY failed"),
1856            GateType::PauliY
1857        );
1858        assert_eq!(
1859            characterizer
1860                .identify_gate_type(&PauliZ { target: QubitId(0) })
1861                .expect("identify PauliZ failed"),
1862            GateType::PauliZ
1863        );
1864    }
1865
1866    #[test]
1867    fn test_rotation_decomposition() {
1868        let characterizer = GateCharacterizer::new(1e-10);
1869        let rx = RotationX {
1870            target: QubitId(0),
1871            theta: PI / 4.0,
1872        };
1873
1874        let decomposition = characterizer
1875            .decompose_to_rotations(&rx)
1876            .expect("decompose to rotations failed");
1877        assert_eq!(decomposition.len(), 3); // Rz-Ry-Rz decomposition
1878    }
1879
1880    #[test]
1881    fn test_eigenphases() {
1882        let characterizer = GateCharacterizer::new(1e-10);
1883        let rz = RotationZ {
1884            target: QubitId(0),
1885            theta: PI / 2.0,
1886        };
1887
1888        let eigen = characterizer
1889            .eigenstructure(&rz)
1890            .expect("eigenstructure failed");
1891        let phases = eigen.eigenphases();
1892
1893        assert_eq!(phases.len(), 2);
1894        assert!((phases[0] + phases[1]).abs() < 1e-10); // Opposite phases
1895    }
1896
1897    #[test]
1898    fn test_closest_clifford() {
1899        let characterizer = GateCharacterizer::new(1e-10);
1900
1901        // Create a gate similar to T (pi/4 rotation around Z)
1902        let t_like = RotationZ {
1903            target: QubitId(0),
1904            theta: PI / 4.0,
1905        };
1906        let closest = characterizer
1907            .find_closest_clifford(&t_like)
1908            .expect("find closest clifford failed");
1909
1910        // Should find S gate (Phase) as closest
1911        let s_distance = characterizer
1912            .gate_distance(&t_like, &Phase { target: QubitId(0) })
1913            .expect("gate distance to S failed");
1914        let actual_distance = characterizer
1915            .gate_distance(&t_like, closest.as_ref())
1916            .expect("gate distance to closest failed");
1917
1918        assert!(actual_distance <= s_distance + 1e-10);
1919    }
1920
1921    #[test]
1922    fn test_identity_check() {
1923        let characterizer = GateCharacterizer::new(1e-10);
1924
1925        // Test with I gate (represented as Rz(0))
1926        let identity_gate = RotationZ {
1927            target: QubitId(0),
1928            theta: 0.0,
1929        };
1930        assert!(characterizer.is_identity(&identity_gate, 1e-10));
1931        assert!(!characterizer.is_identity(&PauliX { target: QubitId(0) }, 1e-10));
1932
1933        // X² = I
1934        let x_squared_vec = vec![
1935            Complex::new(1.0, 0.0),
1936            Complex::new(0.0, 0.0),
1937            Complex::new(0.0, 0.0),
1938            Complex::new(1.0, 0.0),
1939        ];
1940
1941        #[derive(Debug)]
1942        struct CustomGate(Vec<Complex>);
1943        impl GateOp for CustomGate {
1944            fn name(&self) -> &'static str {
1945                "X²"
1946            }
1947            fn qubits(&self) -> Vec<QubitId> {
1948                vec![QubitId(0)]
1949            }
1950            fn matrix(&self) -> QuantRS2Result<Vec<Complex>> {
1951                Ok(self.0.clone())
1952            }
1953            fn as_any(&self) -> &dyn std::any::Any {
1954                self
1955            }
1956            fn clone_gate(&self) -> Box<dyn GateOp> {
1957                Box::new(CustomGate(self.0.clone()))
1958            }
1959        }
1960
1961        let x_squared_gate = CustomGate(x_squared_vec);
1962        assert!(characterizer.is_identity(&x_squared_gate, 1e-10));
1963    }
1964
1965    #[test]
1966    fn test_global_phase() {
1967        let characterizer = GateCharacterizer::new(1e-10);
1968
1969        // Z gate global phase (det(Z) = -1, phase = π, global phase = π/2)
1970        let z_phase = characterizer
1971            .global_phase(&PauliZ { target: QubitId(0) })
1972            .expect("global phase of Z failed");
1973        // For Pauli Z: eigenvalues are 1 and -1, det = -1, phase = π, global phase = π/2
1974        assert!((z_phase - PI / 2.0).abs() < 1e-10 || (z_phase + PI / 2.0).abs() < 1e-10);
1975
1976        // Phase gate has global phase (S gate applies phase e^(iπ/4) to |1>)
1977        let phase_gate = Phase { target: QubitId(0) };
1978        let global_phase = characterizer
1979            .global_phase(&phase_gate)
1980            .expect("global phase of S failed");
1981        // S gate eigenvalues are 1 and i, so average phase is π/4
1982        assert!((global_phase - PI / 4.0).abs() < 1e-10);
1983    }
1984}