File size: 4,868 Bytes
9425aed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Complex Hermitian eigensolver via nalgebra real-block reduction.
// Fixes spe_encoder.f90 sov_zheev stub.
//
// Method: for H Hermitian, decompose as H = A + iB where A = Re(H) symmetric,
// B = Im(H) antisymmetric. Build real block matrix M = [[A, -B],[B, A]] (2n×2n).
// M is real symmetric. Its eigenvalues are the doubled eigenvalues of H.
// Recover eigenvectors by decoding the real pairs back to complex.

use nalgebra::{DMatrix, DVector};
use num_complex::Complex64;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ZheevError {
    #[error("Matrix is not square: {rows}x{cols}")]
    NotSquare { rows: usize, cols: usize },
    #[error("Matrix is not Hermitian (max off-symmetry: {max_err})")]
    NotHermitian { max_err: f64 },
}

pub struct ZheevResult {
    pub eigenvalues: DVector<f64>,
    pub eigenvectors: DMatrix<Complex64>,
}

pub fn zheev(h: &DMatrix<Complex64>) -> Result<ZheevResult, ZheevError> {
    let (rows, cols) = h.shape();
    if rows != cols {
        return Err(ZheevError::NotSquare { rows, cols });
    }
    let n = rows;

    let max_err = hermitian_error(h);
    if max_err > 1e-10 {
        return Err(ZheevError::NotHermitian { max_err });
    }

    // Build real block matrix [[Re(H), -Im(H)], [Im(H), Re(H)]]
    let mut m = DMatrix::<f64>::zeros(2 * n, 2 * n);
    for i in 0..n {
        for j in 0..n {
            let h_ij = h[(i, j)];
            m[(i, j)] = h_ij.re;
            m[(i, n + j)] = -h_ij.im;
            m[(n + i, j)] = h_ij.im;
            m[(n + i, n + j)] = h_ij.re;
        }
    }

    let eig = m.symmetric_eigen();
    // eigenvalues come in pairs; take every other one (first n)
    let mut pairs: Vec<(f64, usize)> = (0..2 * n)
        .map(|i| (eig.eigenvalues[i], i))
        .collect();
    pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());

    let mut eigenvalues = DVector::<f64>::zeros(n);
    let mut eigenvectors = DMatrix::<Complex64>::zeros(n, n);

    // eigenvalues are doubled (each appears twice); take one from each pair: indices 0,2,4,...
    for (k, &(eval, col)) in pairs.iter().step_by(2).take(n).enumerate() {
        eigenvalues[k] = eval;
        for i in 0..n {
            let re = eig.eigenvectors[(i, col)];
            let im = eig.eigenvectors[(n + i, col)];
            eigenvectors[(i, k)] = Complex64::new(re, im);
        }
        // normalize
        let norm: f64 = (0..n).map(|i| eigenvectors[(i, k)].norm_sqr()).sum::<f64>().sqrt();
        if norm > 1e-14 {
            for i in 0..n {
                eigenvectors[(i, k)] /= norm;
            }
        }
    }

    Ok(ZheevResult { eigenvalues, eigenvectors })
}

/// exp(-i * dt * H) for Hermitian H via eigendecomposition.
/// This is the unitary evolution operator U = exp(-i H dt).
pub fn exp_hermitian(h: &DMatrix<Complex64>, dt: f64) -> DMatrix<Complex64> {
    let res = zheev(h).expect("exp_hermitian requires Hermitian input");
    let n = res.eigenvalues.len();
    let mut diag = DMatrix::<Complex64>::zeros(n, n);
    for i in 0..n {
        let phase = Complex64::new(0.0, -dt * res.eigenvalues[i]);
        diag[(i, i)] = phase.exp();
    }
    &res.eigenvectors * &diag * res.eigenvectors.adjoint()
}

fn hermitian_error(h: &DMatrix<Complex64>) -> f64 {
    let n = h.nrows();
    let mut max = 0.0_f64;
    for i in 0..n {
        for j in 0..n {
            let err = (h[(i, j)] - h[(j, i)].conj()).norm();
            if err > max {
                max = err;
            }
        }
    }
    max
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_real_diagonal() {
        // H = diag(1, 2, 3) — trivially Hermitian
        let mut h = DMatrix::<Complex64>::zeros(3, 3);
        h[(0, 0)] = Complex64::new(1.0, 0.0);
        h[(1, 1)] = Complex64::new(2.0, 0.0);
        h[(2, 2)] = Complex64::new(3.0, 0.0);
        let res = zheev(&h).unwrap();
        let mut evals: Vec<f64> = res.eigenvalues.iter().copied().collect();
        evals.sort_by(|a, b| a.partial_cmp(b).unwrap());
        assert!((evals[0] - 1.0).abs() < 1e-10);
        assert!((evals[1] - 2.0).abs() < 1e-10);
        assert!((evals[2] - 3.0).abs() < 1e-10);
    }

    #[test]
    fn test_exp_unitary() {
        // H = [[1, 0],[0, -1]], dt = pi/2 → U = diag(e^{-i pi/2}, e^{i pi/2})
        let mut h = DMatrix::<Complex64>::zeros(2, 2);
        h[(0, 0)] = Complex64::new(1.0, 0.0);
        h[(1, 1)] = Complex64::new(-1.0, 0.0);
        let u = exp_hermitian(&h, std::f64::consts::PI / 2.0);
        // U†U should be identity
        let prod = u.adjoint() * &u;
        let eye = DMatrix::<Complex64>::identity(2, 2);
        let residual = (&prod - &eye).norm();
        assert!(residual < 1e-12, "residual={}", residual);
    }
}