quantrs2_core/
eigensolve.rs

1//! Eigenvalue decomposition for quantum gates
2//!
3//! This module provides eigenvalue decomposition specifically optimized
4//! for unitary matrices (quantum gates). It uses the QR algorithm with
5//! shifts for efficient computation.
6
7use crate::error::{QuantRS2Error, QuantRS2Result};
8use ndarray::{Array1, Array2};
9use num_complex::Complex64 as Complex;
10use std::f64::EPSILON;
11
12/// Result of eigenvalue decomposition
13#[derive(Debug, Clone)]
14pub struct EigenDecomposition {
15    /// Eigenvalues
16    pub eigenvalues: Array1<Complex>,
17    /// Eigenvectors as columns
18    pub eigenvectors: Array2<Complex>,
19}
20
21/// Compute eigenvalue decomposition of a unitary matrix
22pub fn eigen_decompose_unitary(
23    matrix: &Array2<Complex>,
24    tolerance: f64,
25) -> QuantRS2Result<EigenDecomposition> {
26    let n = matrix.nrows();
27    if n != matrix.ncols() {
28        return Err(QuantRS2Error::InvalidInput(
29            "Matrix must be square".to_string(),
30        ));
31    }
32
33    // For small matrices, use analytical solutions
34    match n {
35        1 => eigen_1x1(matrix),
36        2 => eigen_2x2(matrix, tolerance),
37        _ => eigen_general(matrix, tolerance),
38    }
39}
40
41/// Analytical eigendecomposition for 1x1 matrix
42fn eigen_1x1(matrix: &Array2<Complex>) -> QuantRS2Result<EigenDecomposition> {
43    let eigenvalues = Array1::from_vec(vec![matrix[(0, 0)]]);
44    let eigenvectors = Array2::from_shape_vec((1, 1), vec![Complex::new(1.0, 0.0)])
45        .map_err(|e| QuantRS2Error::ComputationError(e.to_string()))?;
46
47    Ok(EigenDecomposition {
48        eigenvalues,
49        eigenvectors,
50    })
51}
52
53/// Analytical eigendecomposition for 2x2 matrix
54fn eigen_2x2(matrix: &Array2<Complex>, tolerance: f64) -> QuantRS2Result<EigenDecomposition> {
55    let a = matrix[(0, 0)];
56    let b = matrix[(0, 1)];
57    let c = matrix[(1, 0)];
58    let d = matrix[(1, 1)];
59
60    // Characteristic polynomial: det(A - λI) = 0
61    // λ² - (a+d)λ + (ad-bc) = 0
62    let trace = a + d;
63    let det = a * d - b * c;
64
65    // Solve quadratic equation
66    let discriminant = trace * trace - 4.0 * det;
67    let sqrt_disc = discriminant.sqrt();
68
69    let lambda1 = (trace + sqrt_disc) / 2.0;
70    let lambda2 = (trace - sqrt_disc) / 2.0;
71
72    let eigenvalues = Array1::from_vec(vec![lambda1, lambda2]);
73
74    // Find eigenvectors
75    let mut eigenvectors = Array2::zeros((2, 2));
76
77    // For each eigenvalue, solve (A - λI)v = 0
78    for (i, &lambda) in eigenvalues.iter().enumerate() {
79        // Use the first row of (A - λI) to find eigenvector
80        let _a_minus_lambda = a - lambda;
81
82        if b.norm() > tolerance {
83            // v = [b, λ - a]ᵀ (normalized)
84            let v1 = b;
85            let v2 = lambda - a;
86            let norm = (v1.norm_sqr() + v2.norm_sqr()).sqrt();
87            eigenvectors[(0, i)] = v1 / norm;
88            eigenvectors[(1, i)] = v2 / norm;
89        } else if c.norm() > tolerance {
90            // Use second row: v = [λ - d, c]ᵀ (normalized)
91            let v1 = lambda - d;
92            let v2 = c;
93            let norm = (v1.norm_sqr() + v2.norm_sqr()).sqrt();
94            eigenvectors[(0, i)] = v1 / norm;
95            eigenvectors[(1, i)] = v2 / norm;
96        } else {
97            // Matrix is diagonal
98            eigenvectors[(i, i)] = Complex::new(1.0, 0.0);
99        }
100    }
101
102    Ok(EigenDecomposition {
103        eigenvalues,
104        eigenvectors,
105    })
106}
107
108/// General eigendecomposition using QR algorithm with shifts
109fn eigen_general(matrix: &Array2<Complex>, tolerance: f64) -> QuantRS2Result<EigenDecomposition> {
110    let n = matrix.nrows();
111    let max_iterations = 1000;
112
113    // Start with Hessenberg reduction for efficiency
114    let mut h = matrix.clone();
115    hessenberg_reduction(&mut h)?;
116
117    // QR algorithm with shifts
118    let mut eigenvalues = Vec::with_capacity(n);
119    let mut eigenvectors = Array2::eye(n);
120
121    for _ in 0..max_iterations {
122        // Check for convergence
123        let mut converged = true;
124        for i in 1..n {
125            if h[(i, i - 1)].norm() > tolerance {
126                converged = false;
127                break;
128            }
129        }
130
131        if converged {
132            break;
133        }
134
135        // Wilkinson shift for faster convergence
136        let shift = wilkinson_shift(&h, n);
137
138        // QR decomposition of H - σI
139        let mut h_shifted = h.clone();
140        for i in 0..n {
141            h_shifted[(i, i)] -= shift;
142        }
143        let (q, r) = qr_decompose(&h_shifted)?;
144
145        // Update H = RQ + σI
146        h = r.dot(&q);
147        for i in 0..n {
148            h[(i, i)] += shift;
149        }
150
151        // Accumulate eigenvectors
152        eigenvectors = eigenvectors.dot(&q);
153    }
154
155    // Extract eigenvalues from diagonal
156    for i in 0..n {
157        eigenvalues.push(h[(i, i)]);
158    }
159
160    // Refine eigenvectors using inverse iteration
161    let refined_eigenvectors = refine_eigenvectors(matrix, &eigenvalues, tolerance)?;
162
163    Ok(EigenDecomposition {
164        eigenvalues: Array1::from_vec(eigenvalues),
165        eigenvectors: refined_eigenvectors,
166    })
167}
168
169/// Hessenberg reduction using Householder reflections
170fn hessenberg_reduction(matrix: &mut Array2<Complex>) -> QuantRS2Result<()> {
171    let n = matrix.nrows();
172
173    for k in 0..n - 2 {
174        // Compute Householder vector for column k
175        let mut x = Array1::zeros(n - k - 1);
176        for i in k + 1..n {
177            x[i - k - 1] = matrix[(i, k)];
178        }
179
180        let alpha = x.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
181        if alpha < EPSILON {
182            continue;
183        }
184
185        let mut v = x.clone();
186        v[0] += alpha * Complex::new(x[0].re.signum(), x[0].im.signum());
187        let v_norm = v.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
188        if v_norm < EPSILON {
189            continue;
190        }
191        for i in 0..v.len() {
192            v[i] /= v_norm;
193        }
194
195        // Apply Householder reflection: H = I - 2vv*
196        // Update A = HAH*
197        for j in k..n {
198            let mut sum = Complex::new(0.0, 0.0);
199            for i in k + 1..n {
200                sum += v[i - k - 1].conj() * matrix[(i, j)];
201            }
202            for i in k + 1..n {
203                matrix[(i, j)] -= 2.0 * v[i - k - 1] * sum;
204            }
205        }
206
207        for i in 0..n {
208            let mut sum = Complex::new(0.0, 0.0);
209            for j in k + 1..n {
210                sum += matrix[(i, j)] * v[j - k - 1];
211            }
212            for j in k + 1..n {
213                matrix[(i, j)] -= 2.0 * sum * v[j - k - 1].conj();
214            }
215        }
216    }
217
218    Ok(())
219}
220
221/// QR decomposition using Givens rotations
222fn qr_decompose(matrix: &Array2<Complex>) -> QuantRS2Result<(Array2<Complex>, Array2<Complex>)> {
223    let n = matrix.nrows();
224    let mut r = matrix.clone();
225    let mut q = Array2::eye(n);
226
227    // Use Givens rotations to zero out below-diagonal elements
228    for j in 0..n - 1 {
229        for i in (j + 1)..n {
230            if r[(i, j)].norm() < EPSILON {
231                continue;
232            }
233
234            // Compute Givens rotation
235            let (c, s) = givens_rotation(r[(j, j)], r[(i, j)]);
236
237            // Apply rotation to R
238            for k in j..n {
239                let rjk = r[(j, k)];
240                let rik = r[(i, k)];
241                r[(j, k)] = c * rjk + s * rik;
242                r[(i, k)] = -s.conj() * rjk + c * rik;
243            }
244
245            // Apply rotation to Q
246            for k in 0..n {
247                let qkj = q[(k, j)];
248                let qki = q[(k, i)];
249                q[(k, j)] = c * qkj + s * qki;
250                q[(k, i)] = -s.conj() * qkj + c * qki;
251            }
252        }
253    }
254
255    Ok((q, r))
256}
257
258/// Compute Givens rotation coefficients
259fn givens_rotation(a: Complex, b: Complex) -> (f64, Complex) {
260    let r = (a.norm_sqr() + b.norm_sqr()).sqrt();
261    if r < EPSILON {
262        (1.0, Complex::new(0.0, 0.0))
263    } else {
264        let c = a.norm() / r;
265        let s = (a.conj() * b) / (a.norm() * r);
266        (c, s)
267    }
268}
269
270/// Wilkinson shift for QR algorithm
271fn wilkinson_shift(matrix: &Array2<Complex>, n: usize) -> Complex {
272    if n < 2 {
273        return Complex::new(0.0, 0.0);
274    }
275
276    // Get 2x2 trailing submatrix
277    let a = matrix[(n - 2, n - 2)];
278    let b = matrix[(n - 2, n - 1)];
279    let c = matrix[(n - 1, n - 2)];
280    let d = matrix[(n - 1, n - 1)];
281
282    // Compute eigenvalue of 2x2 matrix closest to d
283    let trace = a + d;
284    let det = a * d - b * c;
285    let discriminant = trace * trace - 4.0 * det;
286    let sqrt_disc = discriminant.sqrt();
287
288    let lambda1 = (trace + sqrt_disc) / 2.0;
289    let lambda2 = (trace - sqrt_disc) / 2.0;
290
291    // Choose shift closest to bottom-right element
292    if (lambda1 - d).norm() < (lambda2 - d).norm() {
293        lambda1
294    } else {
295        lambda2
296    }
297}
298
299/// Refine eigenvectors using inverse iteration
300fn refine_eigenvectors(
301    matrix: &Array2<Complex>,
302    eigenvalues: &[Complex],
303    tolerance: f64,
304) -> QuantRS2Result<Array2<Complex>> {
305    let n = matrix.nrows();
306    let mut eigenvectors = Array2::zeros((n, n));
307
308    for (i, &lambda) in eigenvalues.iter().enumerate() {
309        // Start with random vector
310        let mut v = Array1::from_vec(vec![Complex::new(1.0, 0.0); n]);
311        v[i] = Complex::new(1.0, 0.0); // Bias towards standard basis
312
313        // Normalize
314        let norm = v.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
315        if norm > EPSILON {
316            for i in 0..v.len() {
317                v[i] /= norm;
318            }
319        }
320
321        // Inverse iteration: solve (A - λI)v_new = v_old
322        let shifted = matrix - lambda * Array2::eye(n);
323
324        for _ in 0..10 {
325            // Use QR decomposition to solve the system
326            let v_new = solve_system(&shifted, &v, tolerance)?;
327
328            // Normalize
329            let norm = v_new.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
330            if norm < EPSILON {
331                break;
332            }
333
334            let v_normalized = &v_new / norm;
335
336            // Check convergence
337            let diff: f64 = (&v_normalized - &v)
338                .iter()
339                .map(|x| x.norm_sqr())
340                .sum::<f64>()
341                .sqrt();
342            if diff < tolerance {
343                v = v_normalized;
344                break;
345            }
346
347            v = v_normalized;
348        }
349
350        // Store eigenvector
351        for j in 0..n {
352            eigenvectors[(j, i)] = v[j];
353        }
354    }
355
356    Ok(eigenvectors)
357}
358
359/// Solve linear system using QR decomposition
360fn solve_system(
361    a: &Array2<Complex>,
362    b: &Array1<Complex>,
363    tolerance: f64,
364) -> QuantRS2Result<Array1<Complex>> {
365    let n = a.nrows();
366
367    // Add small regularization for numerical stability
368    let mut regularized = a.clone();
369    for i in 0..n {
370        regularized[(i, i)] += Complex::new(tolerance, 0.0);
371    }
372
373    // QR decomposition
374    let (q, r) = qr_decompose(&regularized)?;
375
376    // Solve Rx = Q*b using back substitution
377    let qtb = q.t().dot(b);
378    let mut x = Array1::zeros(n);
379
380    for i in (0..n).rev() {
381        let mut sum = qtb[i];
382        for j in i + 1..n {
383            sum -= r[(i, j)] * x[j];
384        }
385
386        if r[(i, i)].norm() > tolerance {
387            x[i] = sum / r[(i, i)];
388        } else {
389            x[i] = Complex::new(0.0, 0.0);
390        }
391    }
392
393    Ok(x)
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use std::f64::consts::PI;
400
401    #[test]
402    fn test_eigen_2x2_pauli_x() {
403        // Pauli X matrix
404        let matrix = Array2::from_shape_vec(
405            (2, 2),
406            vec![
407                Complex::new(0.0, 0.0),
408                Complex::new(1.0, 0.0),
409                Complex::new(1.0, 0.0),
410                Complex::new(0.0, 0.0),
411            ],
412        )
413        .unwrap();
414
415        let result = eigen_decompose_unitary(&matrix, 1e-10).unwrap();
416
417        // Pauli X has eigenvalues ±1
418        let mut eigenvalues: Vec<_> = result.eigenvalues.iter().map(|&x| x.re).collect();
419        eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap());
420
421        assert!((eigenvalues[0] - (-1.0)).abs() < 1e-10);
422        assert!((eigenvalues[1] - 1.0).abs() < 1e-10);
423    }
424
425    #[test]
426    fn test_eigen_2x2_rotation() {
427        // Rotation matrix R(θ)
428        let theta = PI / 4.0;
429        let c = theta.cos();
430        let s = theta.sin();
431
432        let matrix = Array2::from_shape_vec(
433            (2, 2),
434            vec![
435                Complex::new(c, 0.0),
436                Complex::new(-s, 0.0),
437                Complex::new(s, 0.0),
438                Complex::new(c, 0.0),
439            ],
440        )
441        .unwrap();
442
443        let result = eigen_decompose_unitary(&matrix, 1e-10).unwrap();
444
445        // Rotation matrix has eigenvalues e^(±iθ)
446        for eigenvalue in result.eigenvalues.iter() {
447            assert!((eigenvalue.norm() - 1.0).abs() < 1e-10);
448        }
449
450        // Check that phases are ±θ
451        let phases: Vec<_> = result.eigenvalues.iter().map(|&x| x.arg()).collect();
452        assert!(phases.iter().any(|&p| (p - theta).abs() < 1e-10));
453        assert!(phases.iter().any(|&p| (p + theta).abs() < 1e-10));
454    }
455
456    #[test]
457    fn test_eigenvector_orthogonality() {
458        // Hadamard matrix
459        let sqrt2_inv = 1.0 / 2.0_f64.sqrt();
460        let matrix = Array2::from_shape_vec(
461            (2, 2),
462            vec![
463                Complex::new(sqrt2_inv, 0.0),
464                Complex::new(sqrt2_inv, 0.0),
465                Complex::new(sqrt2_inv, 0.0),
466                Complex::new(-sqrt2_inv, 0.0),
467            ],
468        )
469        .unwrap();
470
471        let result = eigen_decompose_unitary(&matrix, 1e-10).unwrap();
472
473        // Check eigenvectors are orthonormal
474        let v1 = result.eigenvectors.column(0);
475        let v2 = result.eigenvectors.column(1);
476
477        // Check normalization
478        assert!((v1.dot(&v1).norm() - 1.0).abs() < 1e-10);
479        assert!((v2.dot(&v2).norm() - 1.0).abs() < 1e-10);
480
481        // Check orthogonality
482        assert!(v1.dot(&v2).norm() < 1e-10);
483    }
484}