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    #[allow(dead_code)]
343    fn matrix_equals(&self, a: &Array2<Complex>, b: &Array2<Complex>, tolerance: f64) -> bool {
344        a.shape() == b.shape()
345            && a.iter()
346                .zip(b.iter())
347                .all(|(a_ij, b_ij)| (a_ij - b_ij).norm() < tolerance)
348    }
349
350    /// Decompose a gate into rotation gates based on eigenstructure
351    pub fn decompose_to_rotations(
352        &self,
353        gate: &dyn GateOp,
354    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
355        let eigen = self.eigenstructure(gate)?;
356
357        match eigen.eigenvalues.len() {
358            2 => self.decompose_single_qubit(&eigen),
359            _ => Err(QuantRS2Error::UnsupportedOperation(
360                "Rotation decomposition only supported for single-qubit gates".to_string(),
361            )),
362        }
363    }
364
365    /// Decompose single-qubit gate
366    fn decompose_single_qubit(
367        &self,
368        eigen: &GateEigenstructure,
369    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
370        // Use Euler angle decomposition
371        // Any single-qubit unitary can be written as Rz(γ)Ry(β)Rz(α)
372
373        let matrix = &eigen.matrix;
374
375        // Extract Euler angles
376        let alpha = matrix[(1, 1)].arg() - matrix[(1, 0)].arg();
377        let gamma = matrix[(1, 1)].arg() + matrix[(1, 0)].arg();
378        let beta = 2.0 * matrix[(1, 0)].norm().acos();
379
380        Ok(vec![
381            Box::new(RotationZ {
382                target: QubitId(0),
383                theta: alpha,
384            }),
385            Box::new(RotationY {
386                target: QubitId(0),
387                theta: beta,
388            }),
389            Box::new(RotationZ {
390                target: QubitId(0),
391                theta: gamma,
392            }),
393        ])
394    }
395
396    /// Find the closest Clifford gate to a given gate
397    pub fn find_closest_clifford(&self, gate: &dyn GateOp) -> QuantRS2Result<Box<dyn GateOp>> {
398        let eigen = self.eigenstructure(gate)?;
399
400        if eigen.eigenvalues.len() != 2 {
401            return Err(QuantRS2Error::UnsupportedOperation(
402                "Clifford approximation only supported for single-qubit gates".to_string(),
403            ));
404        }
405
406        // Single-qubit Clifford gates
407        let target = QubitId(0);
408        let clifford_gates: Vec<Box<dyn GateOp>> = vec![
409            Box::new(PauliX { target }),
410            Box::new(PauliY { target }),
411            Box::new(PauliZ { target }),
412            Box::new(Hadamard { target }),
413            Box::new(Phase { target }),
414        ];
415
416        // Find the Clifford gate with minimum distance
417        let mut min_distance = f64::INFINITY;
418        let mut closest_gate = None;
419
420        for clifford in &clifford_gates {
421            let distance = self.gate_distance(gate, clifford.as_ref())?;
422            if distance < min_distance {
423                min_distance = distance;
424                closest_gate = Some(clifford.clone());
425            }
426        }
427
428        closest_gate.ok_or_else(|| {
429            QuantRS2Error::ComputationError("Failed to find closest Clifford gate".to_string())
430        })
431    }
432
433    /// Compute the distance between two gates (Frobenius norm)
434    pub fn gate_distance(&self, gate1: &dyn GateOp, gate2: &dyn GateOp) -> QuantRS2Result<f64> {
435        let m1_vec = gate1.matrix()?;
436        let m2_vec = gate2.matrix()?;
437
438        if m1_vec.len() != m2_vec.len() {
439            return Err(QuantRS2Error::InvalidInput(
440                "Gates must have the same dimensions".to_string(),
441            ));
442        }
443
444        let diff_sqr: f64 = m1_vec
445            .iter()
446            .zip(m2_vec.iter())
447            .map(|(a, b)| (a - b).norm_sqr())
448            .sum();
449        Ok(diff_sqr.sqrt())
450    }
451
452    /// Check if a gate is approximately equal to identity
453    pub fn is_identity(&self, gate: &dyn GateOp, tolerance: f64) -> bool {
454        let matrix_vec = match gate.matrix() {
455            Ok(m) => m,
456            Err(_) => return false,
457        };
458        let n = (matrix_vec.len() as f64).sqrt() as usize;
459
460        for i in 0..n {
461            for j in 0..n {
462                let idx = i * n + j;
463                let expected = if i == j {
464                    Complex::new(1.0, 0.0)
465                } else {
466                    Complex::new(0.0, 0.0)
467                };
468                if (matrix_vec[idx] - expected).norm() > tolerance {
469                    return false;
470                }
471            }
472        }
473        true
474    }
475
476    /// Extract the global phase of a gate
477    pub fn global_phase(&self, gate: &dyn GateOp) -> QuantRS2Result<f64> {
478        let eigen = self.eigenstructure(gate)?;
479
480        // For a unitary matrix U = e^(iφ)V where V is special unitary,
481        // the global phase φ = arg(det(U))/n
482        // det(U) = product of eigenvalues
483        let det = eigen
484            .eigenvalues
485            .iter()
486            .fold(Complex::new(1.0, 0.0), |acc, &lambda| acc * lambda);
487        let n = eigen.eigenvalues.len() as f64;
488        Ok(det.arg() / n)
489    }
490}
491
492/// Types of gates identified by characterization
493#[derive(Debug, Clone, PartialEq)]
494pub enum GateType {
495    /// Identity gate
496    Identity,
497    /// Pauli X gate
498    PauliX,
499    /// Pauli Y gate
500    PauliY,
501    /// Pauli Z gate
502    PauliZ,
503    /// Hadamard gate
504    Hadamard,
505    /// Phase gate
506    Phase { angle: f64 },
507    /// Rotation gate
508    Rotation { angle: f64, axis: [f64; 3] },
509    /// CNOT gate
510    CNOT,
511    /// Controlled phase gate
512    ControlledPhase { phase: f64 },
513    /// SWAP gate
514    SWAP,
515    /// General n-qubit gate
516    General { qubits: usize },
517}
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522    use crate::gate::GateOp;
523    use std::f64::consts::PI;
524
525    #[test]
526    fn test_pauli_identification() {
527        let characterizer = GateCharacterizer::new(1e-10);
528
529        assert_eq!(
530            characterizer
531                .identify_gate_type(&PauliX { target: QubitId(0) })
532                .unwrap(),
533            GateType::PauliX
534        );
535        assert_eq!(
536            characterizer
537                .identify_gate_type(&PauliY { target: QubitId(0) })
538                .unwrap(),
539            GateType::PauliY
540        );
541        assert_eq!(
542            characterizer
543                .identify_gate_type(&PauliZ { target: QubitId(0) })
544                .unwrap(),
545            GateType::PauliZ
546        );
547    }
548
549    #[test]
550    fn test_rotation_decomposition() {
551        let characterizer = GateCharacterizer::new(1e-10);
552        let rx = RotationX {
553            target: QubitId(0),
554            theta: PI / 4.0,
555        };
556
557        let decomposition = characterizer.decompose_to_rotations(&rx).unwrap();
558        assert_eq!(decomposition.len(), 3); // Rz-Ry-Rz decomposition
559    }
560
561    #[test]
562    fn test_eigenphases() {
563        let characterizer = GateCharacterizer::new(1e-10);
564        let rz = RotationZ {
565            target: QubitId(0),
566            theta: PI / 2.0,
567        };
568
569        let eigen = characterizer.eigenstructure(&rz).unwrap();
570        let phases = eigen.eigenphases();
571
572        assert_eq!(phases.len(), 2);
573        assert!((phases[0] + phases[1]).abs() < 1e-10); // Opposite phases
574    }
575
576    #[test]
577    fn test_closest_clifford() {
578        let characterizer = GateCharacterizer::new(1e-10);
579
580        // Create a gate similar to T (pi/4 rotation around Z)
581        let t_like = RotationZ {
582            target: QubitId(0),
583            theta: PI / 4.0,
584        };
585        let closest = characterizer.find_closest_clifford(&t_like).unwrap();
586
587        // Should find S gate (Phase) as closest
588        let s_distance = characterizer
589            .gate_distance(&t_like, &Phase { target: QubitId(0) })
590            .unwrap();
591        let actual_distance = characterizer
592            .gate_distance(&t_like, closest.as_ref())
593            .unwrap();
594
595        assert!(actual_distance <= s_distance + 1e-10);
596    }
597
598    #[test]
599    fn test_identity_check() {
600        let characterizer = GateCharacterizer::new(1e-10);
601
602        // Test with I gate (represented as Rz(0))
603        let identity_gate = RotationZ {
604            target: QubitId(0),
605            theta: 0.0,
606        };
607        assert!(characterizer.is_identity(&identity_gate, 1e-10));
608        assert!(!characterizer.is_identity(&PauliX { target: QubitId(0) }, 1e-10));
609
610        // X² = I
611        let x_squared_vec = vec![
612            Complex::new(1.0, 0.0),
613            Complex::new(0.0, 0.0),
614            Complex::new(0.0, 0.0),
615            Complex::new(1.0, 0.0),
616        ];
617
618        #[derive(Debug)]
619        struct CustomGate(Vec<Complex>);
620        impl GateOp for CustomGate {
621            fn name(&self) -> &'static str {
622                "X²"
623            }
624            fn qubits(&self) -> Vec<QubitId> {
625                vec![QubitId(0)]
626            }
627            fn matrix(&self) -> QuantRS2Result<Vec<Complex>> {
628                Ok(self.0.clone())
629            }
630            fn as_any(&self) -> &dyn std::any::Any {
631                self
632            }
633            fn clone_gate(&self) -> Box<dyn GateOp> {
634                Box::new(CustomGate(self.0.clone()))
635            }
636        }
637
638        let x_squared_gate = CustomGate(x_squared_vec);
639        assert!(characterizer.is_identity(&x_squared_gate, 1e-10));
640    }
641
642    #[test]
643    fn test_global_phase() {
644        let characterizer = GateCharacterizer::new(1e-10);
645
646        // Z gate global phase (det(Z) = -1, phase = π, global phase = π/2)
647        let z_phase = characterizer
648            .global_phase(&PauliZ { target: QubitId(0) })
649            .unwrap();
650        // For Pauli Z: eigenvalues are 1 and -1, det = -1, phase = π, global phase = π/2
651        assert!((z_phase - PI / 2.0).abs() < 1e-10 || (z_phase + PI / 2.0).abs() < 1e-10);
652
653        // Phase gate has global phase (S gate applies phase e^(iπ/4) to |1>)
654        let phase_gate = Phase { target: QubitId(0) };
655        let global_phase = characterizer.global_phase(&phase_gate).unwrap();
656        // S gate eigenvalues are 1 and i, so average phase is π/4
657        assert!((global_phase - PI / 4.0).abs() < 1e-10);
658    }
659}