quantrs2_core/
characterization.rs

1//! Gate characterization using eigenvalue decomposition
2//!
3//! This module provides tools for analyzing and characterizing quantum gates
4//! using their eigenstructure. This is useful for:
5//! - Gate synthesis and decomposition
6//! - Identifying gate types and parameters
7//! - Optimizing gate sequences
8//! - Verifying gate implementations
9
10use crate::{
11    eigensolve::eigen_decompose_unitary,
12    error::{QuantRS2Error, QuantRS2Result},
13    gate::{single::*, GateOp},
14    qubit::QubitId,
15};
16use ndarray::{Array1, Array2};
17use num_complex::Complex64 as Complex;
18
19/// Represents the eigenstructure of a quantum gate
20#[derive(Debug, Clone)]
21pub struct GateEigenstructure {
22    /// Eigenvalues of the gate
23    pub eigenvalues: Vec<Complex>,
24    /// Eigenvectors as columns of a matrix
25    pub eigenvectors: Array2<Complex>,
26    /// The original gate matrix
27    pub matrix: Array2<Complex>,
28}
29
30impl GateEigenstructure {
31    /// Check if the gate is diagonal in some basis
32    pub fn is_diagonal(&self, tolerance: f64) -> bool {
33        // A gate is diagonal if its matrix commutes with a diagonal matrix
34        // of its eigenvalues
35        let n = self.matrix.nrows();
36        for i in 0..n {
37            for j in 0..n {
38                if i != j && self.matrix[(i, j)].norm() > tolerance {
39                    return false;
40                }
41            }
42        }
43        true
44    }
45
46    /// Get the phases of eigenvalues (assuming unitary gate)
47    pub fn eigenphases(&self) -> Vec<f64> {
48        self.eigenvalues
49            .iter()
50            .map(|&lambda| lambda.arg())
51            .collect()
52    }
53
54    /// Check if this represents a phase gate
55    pub fn is_phase_gate(&self, tolerance: f64) -> bool {
56        // All eigenvalues should have the same magnitude (1 for unitary)
57        let magnitude = self.eigenvalues[0].norm();
58        self.eigenvalues
59            .iter()
60            .all(|&lambda| (lambda.norm() - magnitude).abs() < tolerance)
61    }
62
63    /// Get the rotation angle for single-qubit gates
64    pub fn rotation_angle(&self) -> Option<f64> {
65        if self.eigenvalues.len() != 2 {
66            return None;
67        }
68
69        // For a rotation gate, eigenvalues are e^(±iθ/2)
70        let phase_diff = (self.eigenvalues[0] / self.eigenvalues[1]).arg();
71        Some(phase_diff.abs())
72    }
73
74    /// Get the rotation axis for single-qubit gates
75    pub fn rotation_axis(&self, tolerance: f64) -> Option<[f64; 3]> {
76        if self.eigenvalues.len() != 2 {
77            return None;
78        }
79
80        // Find the Bloch sphere axis from eigenvectors
81        // For a rotation about axis n, the eigenvectors correspond to
82        // spin up/down along that axis
83        let v0 = self.eigenvectors.column(0);
84        let v1 = self.eigenvectors.column(1);
85
86        // Convert eigenvectors to Bloch vectors
87        let bloch0 = eigenvector_to_bloch(&v0.to_owned());
88        let bloch1 = eigenvector_to_bloch(&v1.to_owned());
89
90        // The rotation axis is perpendicular to both Bloch vectors
91        // (for pure rotation, eigenvectors should point opposite on sphere)
92        let axis = [
93            (bloch0[0] + bloch1[0]) / 2.0,
94            (bloch0[1] + bloch1[1]) / 2.0,
95            (bloch0[2] + bloch1[2]) / 2.0,
96        ];
97
98        let norm = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
99        if norm < tolerance {
100            None
101        } else {
102            Some([axis[0] / norm, axis[1] / norm, axis[2] / norm])
103        }
104    }
105}
106
107/// Convert an eigenvector to Bloch sphere coordinates
108fn eigenvector_to_bloch(v: &Array1<Complex>) -> [f64; 3] {
109    if v.len() != 2 {
110        return [0.0, 0.0, 0.0];
111    }
112
113    // Compute density matrix rho = |v><v|
114    let v0 = v[0];
115    let v1 = v[1];
116    let rho00 = (v0 * v0.conj()).re;
117    let rho11 = (v1 * v1.conj()).re;
118    let rho01 = v0 * v1.conj();
119
120    [2.0 * rho01.re, -2.0 * rho01.im, rho00 - rho11]
121}
122
123/// Gate characterization tools
124pub struct GateCharacterizer {
125    tolerance: f64,
126}
127
128impl GateCharacterizer {
129    /// Create a new gate characterizer
130    pub fn new(tolerance: f64) -> Self {
131        Self { tolerance }
132    }
133
134    /// Compute the eigenstructure of a gate
135    pub fn eigenstructure(&self, gate: &dyn GateOp) -> QuantRS2Result<GateEigenstructure> {
136        let matrix_vec = gate.matrix()?;
137        let n = (matrix_vec.len() as f64).sqrt() as usize;
138
139        // Convert to ndarray matrix
140        let mut matrix = Array2::zeros((n, n));
141        for i in 0..n {
142            for j in 0..n {
143                matrix[(i, j)] = matrix_vec[i * n + j];
144            }
145        }
146
147        // Perform eigendecomposition using our optimized algorithm
148        let eigen = eigen_decompose_unitary(&matrix, self.tolerance)?;
149
150        Ok(GateEigenstructure {
151            eigenvalues: eigen.eigenvalues.to_vec(),
152            eigenvectors: eigen.eigenvectors,
153            matrix,
154        })
155    }
156
157    /// Identify the type of gate based on its eigenstructure
158    pub fn identify_gate_type(&self, gate: &dyn GateOp) -> QuantRS2Result<GateType> {
159        let eigen = self.eigenstructure(gate)?;
160        let n = eigen.eigenvalues.len();
161
162        match n {
163            2 => self.identify_single_qubit_gate(&eigen),
164            4 => self.identify_two_qubit_gate(&eigen),
165            _ => Ok(GateType::General {
166                qubits: (n as f64).log2() as usize,
167            }),
168        }
169    }
170
171    /// Identify single-qubit gate type
172    fn identify_single_qubit_gate(&self, eigen: &GateEigenstructure) -> QuantRS2Result<GateType> {
173        // Check for Pauli gates (eigenvalues ±1 or ±i)
174        if self.is_pauli_gate(eigen) {
175            return Ok(self.identify_pauli_type(eigen));
176        }
177
178        // Check for phase/rotation gates
179        if let Some(angle) = eigen.rotation_angle() {
180            if let Some(axis) = eigen.rotation_axis(self.tolerance) {
181                return Ok(GateType::Rotation { angle, axis });
182            }
183        }
184
185        // Check for Hadamard (eigenvalues ±1)
186        if self.is_hadamard(eigen) {
187            return Ok(GateType::Hadamard);
188        }
189
190        Ok(GateType::General { qubits: 1 })
191    }
192
193    /// Identify two-qubit gate type
194    fn identify_two_qubit_gate(&self, eigen: &GateEigenstructure) -> QuantRS2Result<GateType> {
195        // Check for CNOT (eigenvalues all ±1)
196        if self.is_cnot(eigen) {
197            return Ok(GateType::CNOT);
198        }
199
200        // Check for controlled phase gates
201        if self.is_controlled_phase(eigen) {
202            if let Some(phase) = self.extract_controlled_phase(eigen) {
203                return Ok(GateType::ControlledPhase { phase });
204            }
205        }
206
207        // Check for SWAP variants
208        if self.is_swap_variant(eigen) {
209            return Ok(self.identify_swap_type(eigen));
210        }
211
212        Ok(GateType::General { qubits: 2 })
213    }
214
215    /// Check if gate is a Pauli gate
216    fn is_pauli_gate(&self, eigen: &GateEigenstructure) -> bool {
217        eigen.eigenvalues.iter().all(|&lambda| {
218            let is_plus_minus_one = (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance
219                || (lambda + Complex::new(1.0, 0.0)).norm() < self.tolerance;
220            let is_plus_minus_i = (lambda - Complex::new(0.0, 1.0)).norm() < self.tolerance
221                || (lambda + Complex::new(0.0, 1.0)).norm() < self.tolerance;
222            is_plus_minus_one || is_plus_minus_i
223        })
224    }
225
226    /// Identify which Pauli gate
227    fn identify_pauli_type(&self, eigen: &GateEigenstructure) -> GateType {
228        let matrix = &eigen.matrix;
229
230        // Check matrix elements to identify Pauli type
231        let tolerance = self.tolerance;
232
233        // Check for Pauli X: [[0,1],[1,0]]
234        if (matrix[(0, 1)] - Complex::new(1.0, 0.0)).norm() < tolerance
235            && (matrix[(1, 0)] - Complex::new(1.0, 0.0)).norm() < tolerance
236            && matrix[(0, 0)].norm() < tolerance
237            && matrix[(1, 1)].norm() < tolerance
238        {
239            GateType::PauliX
240        }
241        // Check for Pauli Y: [[0,-i],[i,0]]
242        else if (matrix[(0, 1)] - Complex::new(0.0, -1.0)).norm() < tolerance
243            && (matrix[(1, 0)] - Complex::new(0.0, 1.0)).norm() < tolerance
244            && matrix[(0, 0)].norm() < tolerance
245            && matrix[(1, 1)].norm() < tolerance
246        {
247            GateType::PauliY
248        }
249        // Check for Pauli Z: [[1,0],[0,-1]]
250        else if (matrix[(0, 0)] - Complex::new(1.0, 0.0)).norm() < tolerance
251            && (matrix[(1, 1)] - Complex::new(-1.0, 0.0)).norm() < tolerance
252            && matrix[(0, 1)].norm() < tolerance
253            && matrix[(1, 0)].norm() < tolerance
254        {
255            GateType::PauliZ
256        } else {
257            GateType::General { qubits: 1 }
258        }
259    }
260
261    /// Check if gate is Hadamard
262    fn is_hadamard(&self, eigen: &GateEigenstructure) -> bool {
263        // Hadamard has eigenvalues ±1
264        eigen.eigenvalues.iter().all(|&lambda| {
265            (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance
266                || (lambda + Complex::new(1.0, 0.0)).norm() < self.tolerance
267        })
268    }
269
270    /// Check if gate is CNOT
271    fn is_cnot(&self, eigen: &GateEigenstructure) -> bool {
272        // CNOT has eigenvalues all ±1
273        eigen.eigenvalues.len() == 4
274            && eigen.eigenvalues.iter().all(|&lambda| {
275                (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance
276                    || (lambda + Complex::new(1.0, 0.0)).norm() < self.tolerance
277            })
278    }
279
280    /// Check if gate is a controlled phase gate
281    fn is_controlled_phase(&self, eigen: &GateEigenstructure) -> bool {
282        // Controlled phase has three eigenvalues = 1 and one phase
283        let ones = eigen
284            .eigenvalues
285            .iter()
286            .filter(|&&lambda| (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance)
287            .count();
288        ones == 3
289    }
290
291    /// Extract phase from controlled phase gate
292    fn extract_controlled_phase(&self, eigen: &GateEigenstructure) -> Option<f64> {
293        eigen
294            .eigenvalues
295            .iter()
296            .find(|&&lambda| (lambda - Complex::new(1.0, 0.0)).norm() > self.tolerance)
297            .map(|&lambda| lambda.arg())
298    }
299
300    /// Check if gate is a SWAP variant
301    fn is_swap_variant(&self, eigen: &GateEigenstructure) -> bool {
302        // SWAP has eigenvalues {1, 1, 1, -1}
303        // iSWAP has eigenvalues {1, 1, i, -i}
304        let ones = eigen
305            .eigenvalues
306            .iter()
307            .filter(|&&lambda| (lambda - Complex::new(1.0, 0.0)).norm() < self.tolerance)
308            .count();
309        ones >= 2
310    }
311
312    /// Identify SWAP variant type
313    fn identify_swap_type(&self, eigen: &GateEigenstructure) -> GateType {
314        let matrix = &eigen.matrix;
315
316        // Check for standard SWAP: |00>->|00>, |01>->|10>, |10>->|01>, |11>->|11>
317        if (matrix[(0, 0)] - Complex::new(1.0, 0.0)).norm() < self.tolerance
318            && (matrix[(1, 2)] - Complex::new(1.0, 0.0)).norm() < self.tolerance
319            && (matrix[(2, 1)] - Complex::new(1.0, 0.0)).norm() < self.tolerance
320            && (matrix[(3, 3)] - Complex::new(1.0, 0.0)).norm() < self.tolerance
321            && matrix[(0, 1)].norm() < self.tolerance
322            && matrix[(0, 2)].norm() < self.tolerance
323            && matrix[(0, 3)].norm() < self.tolerance
324            && matrix[(1, 0)].norm() < self.tolerance
325            && matrix[(1, 1)].norm() < self.tolerance
326            && matrix[(1, 3)].norm() < self.tolerance
327            && matrix[(2, 0)].norm() < self.tolerance
328            && matrix[(2, 2)].norm() < self.tolerance
329            && matrix[(2, 3)].norm() < self.tolerance
330            && matrix[(3, 0)].norm() < self.tolerance
331            && matrix[(3, 1)].norm() < self.tolerance
332            && matrix[(3, 2)].norm() < self.tolerance
333        {
334            GateType::SWAP
335        } else {
336            // Could be iSWAP or other variant
337            GateType::General { qubits: 2 }
338        }
339    }
340
341    /// Compare two matrices for equality
342    fn matrix_equals(&self, a: &Array2<Complex>, b: &Array2<Complex>, tolerance: f64) -> bool {
343        a.shape() == b.shape()
344            && a.iter()
345                .zip(b.iter())
346                .all(|(a_ij, b_ij)| (a_ij - b_ij).norm() < tolerance)
347    }
348
349    /// Decompose a gate into rotation gates based on eigenstructure
350    pub fn decompose_to_rotations(
351        &self,
352        gate: &dyn GateOp,
353    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
354        let eigen = self.eigenstructure(gate)?;
355
356        match eigen.eigenvalues.len() {
357            2 => self.decompose_single_qubit(&eigen),
358            _ => Err(QuantRS2Error::UnsupportedOperation(
359                "Rotation decomposition only supported for single-qubit gates".to_string(),
360            )),
361        }
362    }
363
364    /// Decompose single-qubit gate
365    fn decompose_single_qubit(
366        &self,
367        eigen: &GateEigenstructure,
368    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
369        // Use Euler angle decomposition
370        // Any single-qubit unitary can be written as Rz(γ)Ry(β)Rz(α)
371
372        let matrix = &eigen.matrix;
373
374        // Extract Euler angles
375        let alpha = matrix[(1, 1)].arg() - matrix[(1, 0)].arg();
376        let gamma = matrix[(1, 1)].arg() + matrix[(1, 0)].arg();
377        let beta = 2.0 * matrix[(1, 0)].norm().acos();
378
379        Ok(vec![
380            Box::new(RotationZ {
381                target: QubitId(0),
382                theta: alpha,
383            }),
384            Box::new(RotationY {
385                target: QubitId(0),
386                theta: beta,
387            }),
388            Box::new(RotationZ {
389                target: QubitId(0),
390                theta: gamma,
391            }),
392        ])
393    }
394
395    /// Find the closest Clifford gate to a given gate
396    pub fn find_closest_clifford(&self, gate: &dyn GateOp) -> QuantRS2Result<Box<dyn GateOp>> {
397        let eigen = self.eigenstructure(gate)?;
398
399        if eigen.eigenvalues.len() != 2 {
400            return Err(QuantRS2Error::UnsupportedOperation(
401                "Clifford approximation only supported for single-qubit gates".to_string(),
402            ));
403        }
404
405        // Single-qubit Clifford gates
406        let target = QubitId(0);
407        let clifford_gates: Vec<Box<dyn GateOp>> = vec![
408            Box::new(PauliX { target }),
409            Box::new(PauliY { target }),
410            Box::new(PauliZ { target }),
411            Box::new(Hadamard { target }),
412            Box::new(Phase { target }),
413        ];
414
415        // Find the Clifford gate with minimum distance
416        let mut min_distance = f64::INFINITY;
417        let mut closest_gate = None;
418
419        for clifford in &clifford_gates {
420            let distance = self.gate_distance(gate, clifford.as_ref())?;
421            if distance < min_distance {
422                min_distance = distance;
423                closest_gate = Some(clifford.clone());
424            }
425        }
426
427        closest_gate.ok_or_else(|| {
428            QuantRS2Error::ComputationError("Failed to find closest Clifford gate".to_string())
429        })
430    }
431
432    /// Compute the distance between two gates (Frobenius norm)
433    pub fn gate_distance(&self, gate1: &dyn GateOp, gate2: &dyn GateOp) -> QuantRS2Result<f64> {
434        let m1_vec = gate1.matrix()?;
435        let m2_vec = gate2.matrix()?;
436
437        if m1_vec.len() != m2_vec.len() {
438            return Err(QuantRS2Error::InvalidInput(
439                "Gates must have the same dimensions".to_string(),
440            ));
441        }
442
443        let diff_sqr: f64 = m1_vec
444            .iter()
445            .zip(m2_vec.iter())
446            .map(|(a, b)| (a - b).norm_sqr())
447            .sum();
448        Ok(diff_sqr.sqrt())
449    }
450
451    /// Check if a gate is approximately equal to identity
452    pub fn is_identity(&self, gate: &dyn GateOp, tolerance: f64) -> bool {
453        let matrix_vec = match gate.matrix() {
454            Ok(m) => m,
455            Err(_) => return false,
456        };
457        let n = (matrix_vec.len() as f64).sqrt() as usize;
458
459        for i in 0..n {
460            for j in 0..n {
461                let idx = i * n + j;
462                let expected = if i == j {
463                    Complex::new(1.0, 0.0)
464                } else {
465                    Complex::new(0.0, 0.0)
466                };
467                if (matrix_vec[idx] - expected).norm() > tolerance {
468                    return false;
469                }
470            }
471        }
472        true
473    }
474
475    /// Extract the global phase of a gate
476    pub fn global_phase(&self, gate: &dyn GateOp) -> QuantRS2Result<f64> {
477        let eigen = self.eigenstructure(gate)?;
478
479        // For a unitary matrix U = e^(iφ)V where V is special unitary,
480        // the global phase φ = arg(det(U))/n
481        // det(U) = product of eigenvalues
482        let det = eigen
483            .eigenvalues
484            .iter()
485            .fold(Complex::new(1.0, 0.0), |acc, &lambda| acc * lambda);
486        let n = eigen.eigenvalues.len() as f64;
487        Ok(det.arg() / n)
488    }
489}
490
491/// Types of gates identified by characterization
492#[derive(Debug, Clone, PartialEq)]
493pub enum GateType {
494    /// Identity gate
495    Identity,
496    /// Pauli X gate
497    PauliX,
498    /// Pauli Y gate
499    PauliY,
500    /// Pauli Z gate
501    PauliZ,
502    /// Hadamard gate
503    Hadamard,
504    /// Phase gate
505    Phase { angle: f64 },
506    /// Rotation gate
507    Rotation { angle: f64, axis: [f64; 3] },
508    /// CNOT gate
509    CNOT,
510    /// Controlled phase gate
511    ControlledPhase { phase: f64 },
512    /// SWAP gate
513    SWAP,
514    /// General n-qubit gate
515    General { qubits: usize },
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use crate::gate::{multi::*, single::*};
522    use std::f64::consts::PI;
523
524    #[test]
525    fn test_pauli_identification() {
526        let characterizer = GateCharacterizer::new(1e-10);
527
528        assert_eq!(
529            characterizer
530                .identify_gate_type(&PauliX { target: QubitId(0) })
531                .unwrap(),
532            GateType::PauliX
533        );
534        assert_eq!(
535            characterizer
536                .identify_gate_type(&PauliY { target: QubitId(0) })
537                .unwrap(),
538            GateType::PauliY
539        );
540        assert_eq!(
541            characterizer
542                .identify_gate_type(&PauliZ { target: QubitId(0) })
543                .unwrap(),
544            GateType::PauliZ
545        );
546    }
547
548    #[test]
549    fn test_rotation_decomposition() {
550        let characterizer = GateCharacterizer::new(1e-10);
551        let rx = RotationX {
552            target: QubitId(0),
553            theta: PI / 4.0,
554        };
555
556        let decomposition = characterizer.decompose_to_rotations(&rx).unwrap();
557        assert_eq!(decomposition.len(), 3); // Rz-Ry-Rz decomposition
558    }
559
560    #[test]
561    fn test_eigenphases() {
562        let characterizer = GateCharacterizer::new(1e-10);
563        let rz = RotationZ {
564            target: QubitId(0),
565            theta: PI / 2.0,
566        };
567
568        let eigen = characterizer.eigenstructure(&rz).unwrap();
569        let phases = eigen.eigenphases();
570
571        assert_eq!(phases.len(), 2);
572        assert!((phases[0] + phases[1]).abs() < 1e-10); // Opposite phases
573    }
574
575    #[test]
576    fn test_closest_clifford() {
577        let characterizer = GateCharacterizer::new(1e-10);
578
579        // Create a gate similar to T (pi/4 rotation around Z)
580        let t_like = RotationZ {
581            target: QubitId(0),
582            theta: PI / 4.0,
583        };
584        let closest = characterizer.find_closest_clifford(&t_like).unwrap();
585
586        // Should find S gate (Phase) as closest
587        let s_distance = characterizer
588            .gate_distance(&t_like, &Phase { target: QubitId(0) })
589            .unwrap();
590        let actual_distance = characterizer
591            .gate_distance(&t_like, closest.as_ref())
592            .unwrap();
593
594        assert!(actual_distance <= s_distance + 1e-10);
595    }
596
597    #[test]
598    fn test_identity_check() {
599        let characterizer = GateCharacterizer::new(1e-10);
600
601        // Test with I gate (represented as Rz(0))
602        let identity_gate = RotationZ {
603            target: QubitId(0),
604            theta: 0.0,
605        };
606        assert!(characterizer.is_identity(&identity_gate, 1e-10));
607        assert!(!characterizer.is_identity(&PauliX { target: QubitId(0) }, 1e-10));
608
609        // X² = I
610        let x_squared_vec = vec![
611            Complex::new(1.0, 0.0),
612            Complex::new(0.0, 0.0),
613            Complex::new(0.0, 0.0),
614            Complex::new(1.0, 0.0),
615        ];
616
617        #[derive(Debug)]
618        struct CustomGate(Vec<Complex>);
619        impl GateOp for CustomGate {
620            fn name(&self) -> &'static str {
621                "X²"
622            }
623            fn qubits(&self) -> Vec<QubitId> {
624                vec![QubitId(0)]
625            }
626            fn matrix(&self) -> QuantRS2Result<Vec<Complex>> {
627                Ok(self.0.clone())
628            }
629            fn as_any(&self) -> &dyn std::any::Any {
630                self
631            }
632            fn clone_gate(&self) -> Box<dyn GateOp> {
633                Box::new(CustomGate(self.0.clone()))
634            }
635        }
636
637        let x_squared_gate = CustomGate(x_squared_vec);
638        assert!(characterizer.is_identity(&x_squared_gate, 1e-10));
639    }
640
641    #[test]
642    fn test_global_phase() {
643        let characterizer = GateCharacterizer::new(1e-10);
644
645        // Z gate global phase (det(Z) = -1, phase = π, global phase = π/2)
646        let z_phase = characterizer
647            .global_phase(&PauliZ { target: QubitId(0) })
648            .unwrap();
649        // For Pauli Z: eigenvalues are 1 and -1, det = -1, phase = π, global phase = π/2
650        assert!((z_phase - PI / 2.0).abs() < 1e-10 || (z_phase + PI / 2.0).abs() < 1e-10);
651
652        // Phase gate has global phase (S gate applies phase e^(iπ/4) to |1>)
653        let phase_gate = Phase { target: QubitId(0) };
654        let global_phase = characterizer.global_phase(&phase_gate).unwrap();
655        // S gate eigenvalues are 1 and i, so average phase is π/4
656        assert!((global_phase - PI / 4.0).abs() < 1e-10);
657    }
658}