mdarray_linalg/testing/eig/
mod.rs

1use num_complex::{Complex, ComplexFloat};
2
3use approx::assert_relative_eq;
4
5use super::common::{naive_matmul, random_matrix};
6use crate::eig::EigDecomp;
7use crate::eig::SchurDecomp;
8use crate::{assert_complex_matrix_eq, assert_matrix_eq};
9use crate::{prelude::*, pretty_print};
10use mdarray::DTensor;
11
12fn test_eigen_reconstruction<T>(
13    a: &DTensor<T, 2>,
14    eigenvalues: &DTensor<Complex<T::Real>, 2>,
15    right_eigenvectors: &DTensor<Complex<T::Real>, 2>,
16) where
17    T: Default + std::fmt::Debug + ComplexFloat<Real = f64>,
18{
19    let (n, _) = *a.shape();
20
21    let x = T::default();
22
23    for i in 0..n {
24        let λ = eigenvalues[[0, i]];
25        let v = right_eigenvectors.view(.., i).to_owned();
26
27        let mut av = DTensor::<_, 1>::from_elem([n], Complex::new(x.re(), x.re()));
28        let mut λv = DTensor::<_, 1>::from_elem([n], Complex::new(x.re(), x.re()));
29
30        let norm: f64 = v.iter().map(|z| z.norm_sqr()).sum::<f64>().sqrt();
31        assert!(norm > 1e-12, "Null vector found");
32
33        for row in 0..n {
34            let mut sum = Complex::new(x.re(), x.re());
35            for col in 0..n {
36                sum += Complex::new(a[[row, col]].re(), a[[row, col]].im()) * v[[col]];
37            }
38            av[[row]] = sum;
39            λv[[row]] = λ * v[[row]];
40        }
41        for row in 0..n {
42            let diff = av[[row]] - λv[[row]];
43            assert_relative_eq!(diff.re(), 0.0, epsilon = 1e-10);
44            assert_relative_eq!(diff.im(), 0.0, epsilon = 1e-10);
45        }
46    }
47}
48
49pub fn test_non_square_matrix(bd: &impl Eig<f64>) {
50    let n = 3;
51    let m = 5;
52    let a = random_matrix(m, n);
53
54    let EigDecomp { .. } = bd
55        .eig(&mut a.clone())
56        .expect("Eigenvalue decomposition failed");
57}
58
59pub fn test_square_matrix(bd: &impl Eig<f64>) {
60    let n = 2;
61    let a = random_matrix(n, n);
62
63    let EigDecomp {
64        eigenvalues,
65        right_eigenvectors,
66        ..
67    } = bd
68        .eig(&mut a.clone())
69        .expect("Eigenvalue decomposition failed");
70
71    test_eigen_reconstruction(&a, &eigenvalues, &right_eigenvectors.unwrap());
72}
73
74pub fn test_eig_cplx_square_matrix(bd: &impl Eig<Complex<f64>>) {
75    let n = 4;
76    let a = DTensor::<Complex<f64>, 2>::from_fn([n, n], |i| {
77        Complex::new((i[0] + i[1]) as f64, (i[0] * i[1]) as f64)
78    });
79    println!("{a:?}");
80    let EigDecomp {
81        eigenvalues,
82        right_eigenvectors,
83        ..
84    } = bd
85        .eig(&mut a.clone())
86        .expect("Eigenvalue decomposition failed");
87    println!("{eigenvalues:?}");
88    println!("{right_eigenvectors:?}");
89
90    test_eigen_reconstruction(&a, &eigenvalues, &right_eigenvectors.unwrap());
91}
92
93// #[test]
94// fn eig_full() {
95//     test_eig_full(&Lapack::default());
96//     test_eig_full(&Faer);
97// }
98
99// fn test_eig_full(bd: &impl Eig<f64>) {
100//     let n = 3;
101//     let a = random_matrix(n, n);
102
103//     let EigDecomp {
104//         eigenvalues,
105//         left_eigenvectors,
106//         right_eigenvectors,
107//     } = bd
108//         .eig_full(&mut a.clone())
109//         .expect("Full eigenvalue decomposition failed");
110
111//     // Test right eigenvectors
112//     test_eigen_reconstruction(&a, &eigenvalues, &right_eigenvectors.unwrap());
113
114//     // Verify left eigenvectors are computed
115//     assert!(left_eigenvectors.is_some());
116// }
117
118// #[test]
119// TODO
120// fn test_eig_full_reconstruction() {
121//     test_eig_full_reconstruction_impl(&Lapack::default());
122//     // test_eig_full_reconstruction_impl(&Faer);
123// }
124
125// fn test_eig_full_reconstruction_impl(bd: &impl Eig<Complex<f64>>) {
126//     let n = 4;
127//     // let mut a = random_matrix(n, n);
128//     // let mut b = random_matrix(n, n);
129//     let mut a = DTensor::<Complex<f64>, 2>::from_fn([n, n], |i| {
130//         Complex::new((i[0] + i[1]) as f64, (i[0] * i[1]) as f64)
131//     });
132
133//     let EigDecomp {
134//         eigenvalues,
135//         left_eigenvectors,
136//         right_eigenvectors,
137//     } = bd
138//         .eig_full(&mut a.clone())
139//         .expect("Full eigen decomposition failed");
140
141//     pretty_print(&right_eigenvectors.clone().unwrap());
142//     pretty_print(&left_eigenvectors.clone().unwrap());
143
144//     test_eigen_reconstruction_full(
145//         &a,
146//         &eigenvalues,
147//         &left_eigenvectors.unwrap(),
148//         &right_eigenvectors.unwrap(),
149//     );
150// }
151
152pub fn test_eigen_reconstruction_full<T>(
153    a: &DTensor<T, 2>,
154    eigenvalues: &DTensor<Complex<T::Real>, 2>,
155    left_eigenvectors: &DTensor<Complex<T::Real>, 2>,
156    right_eigenvectors: &DTensor<Complex<T::Real>, 2>,
157) where
158    T: Default + std::fmt::Debug + ComplexFloat<Real = f64>,
159{
160    let (n, _) = *a.shape();
161    let x = T::default();
162
163    for i in 0..n {
164        let λ = eigenvalues[[0, i]];
165        let vr = right_eigenvectors.view(.., i).to_owned();
166        let vl = left_eigenvectors.view(.., i).to_owned();
167
168        let norm_r: f64 = vr.iter().map(|z| z.norm_sqr()).sum::<f64>().sqrt();
169        let norm_l: f64 = vl.iter().map(|z| z.norm_sqr()).sum::<f64>().sqrt();
170
171        assert!(norm_r > 1e-12, "Null right eigenvector found");
172        assert!(norm_l > 1e-12, "Null left eigenvector found");
173
174        // A * vr = λ * vr
175        for row in 0..n {
176            let mut sum = Complex::new(x.re(), x.re());
177            for col in 0..n {
178                sum += Complex::new(a[[row, col]].re(), a[[row, col]].im()) * vr[[col]];
179            }
180            let diff = sum - λ * vr[[row]];
181            assert_relative_eq!(diff.re(), 0.0, epsilon = 1e-10);
182            assert_relative_eq!(diff.im(), 0.0, epsilon = 1e-10);
183        }
184
185        //  vl^H * A = λ * vl^H
186        for col in 0..n {
187            let mut sum = Complex::new(x.re(), x.re());
188            for row in 0..n {
189                sum += vl[[row]].conj() * Complex::new(a[[row, col]].re(), a[[row, col]].im());
190            }
191            let diff = sum - λ * vl[[col]].conj();
192            assert_relative_eq!(diff.re(), 0.0, epsilon = 1e-10);
193            assert_relative_eq!(diff.im(), 0.0, epsilon = 1e-10);
194        }
195    }
196}
197
198pub fn test_eig_values_only(bd: &impl Eig<f64>) {
199    let n = 3;
200    let a = random_matrix(n, n);
201
202    let EigDecomp {
203        eigenvalues,
204        left_eigenvectors,
205        right_eigenvectors,
206    } = bd
207        .eig_values(&mut a.clone())
208        .expect("Eigenvalues computation failed");
209
210    // Check that eigenvalues are computed
211    assert_eq!(*eigenvalues.shape(), (1, n));
212
213    // Check that eigenvectors are not computed
214    assert!(left_eigenvectors.is_none());
215    assert!(right_eigenvectors.is_none());
216}
217
218// test on overwrite removed as the function has been temporary removed.
219
220// #[test]
221// fn eig_overwrite() {
222//     test_eig_overwrite(&Lapack::default());
223// }
224
225// fn test_eig_overwrite(bd: &impl Eig<f64>) {
226//     let n = 3;
227//     let mut a = random_matrix(n, n);
228//     let original_a = a.clone();
229
230//     let mut eigenvalues = DTensor::<Complex<f64>, 2>::zeros([1, n]);
231//     let mut right_eigenvectors_raw = DTensor::<f64, 2>::zeros([n, n]);
232
233//     bd.eig_overwrite::<Dense, Dense, Dense, Dense>(
234//         &mut a,
235//         &mut eigenvalues,
236//         &mut right_eigenvectors_raw,
237//     )
238//     .expect("Overwrite eigenvalue decomposition failed");
239
240//     // Reconstruct complex eigenvalues and eigenvectors from LAPACK format
241//     // let mut eigenvalues = DTensor::<Complex<f64>, 2>::zeros([1, n]);
242//     let mut complex_eigenvectors = DTensor::<Complex<f64>, 2>::zeros([n, n]);
243
244//     let mut j = 0_usize;
245//     while j < n {
246//         let imag = eigenvalues[[0, j]].im();
247//         if imag == 0.0 {
248//             // Real eigenvalue: copy the real eigenvector
249//             for i in 0..n {
250//                 let re = right_eigenvectors_raw[[i, j]];
251//                 complex_eigenvectors[[i, j]] = Complex::new(re, 0.0);
252//             }
253//             j += 1;
254//         } else {
255//             // Complex conjugate pair: reconstruct both eigenvectors
256//             for i in 0..n {
257//                 let re = right_eigenvectors_raw[[i, j]];
258//                 let im = right_eigenvectors_raw[[i, j + 1]];
259//                 complex_eigenvectors[[i, j]] = Complex::new(re, im); // v = Re + i*Im
260//                 complex_eigenvectors[[i, j + 1]] = Complex::new(re, -im); // v̄ = Re - i*Im
261//             }
262//             j += 2;
263//         }
264//     }
265
266//     test_eigen_reconstruction(&original_a, &eigenvalues, &complex_eigenvectors);
267// }
268
269pub fn test_eigh_symmetric(bd: &impl Eig<f64>) {
270    let n = 3;
271    let mut a = random_matrix(n, n);
272
273    // Make matrix symmetric
274    for i in 0..n {
275        for j in 0..n {
276            let val = (a[[i, j]] + a[[j, i]]) / 2.0;
277            a[[i, j]] = val;
278            a[[j, i]] = val;
279        }
280    }
281
282    let mut a_clone = a.clone();
283    println!("{a_clone:?}");
284
285    let EigDecomp {
286        eigenvalues,
287        right_eigenvectors,
288        ..
289    } = bd
290        .eigs(&mut a_clone)
291        .expect("Hermitian eigenvalue decomposition failed");
292
293    println!("{a_clone:?}");
294    println!("{right_eigenvectors:?}");
295    println!("{eigenvalues:?}");
296
297    // For symmetric real matrices, eigenvalues should be real
298    for i in 0..n {
299        assert_relative_eq!(eigenvalues[[0, i]].im(), 0.0, epsilon = 1e-10);
300    }
301
302    test_eigen_reconstruction(&a, &eigenvalues, &right_eigenvectors.unwrap());
303}
304
305pub fn test_eigh_complex_hermitian(bd: &impl Eig<Complex<f64>>) {
306    let n = 3;
307    let mut a = DTensor::<Complex<f64>, 2>::from_fn([n, n], |i| {
308        Complex::new((i[0] + i[1]) as f64, (i[0] * i[1]) as f64)
309    });
310
311    // Make matrix Hermitian
312    for i in 0..n {
313        for j in 0..n {
314            if i == j {
315                a[[i, j]] = Complex::new(a[[i, j]].re(), 0.0); // Diagonal must be real
316            } else {
317                a[[j, i]] = a[[i, j]].conj();
318            }
319        }
320    }
321    a[[0, 0]] = Complex::new(1., 0.);
322
323    // println!("{:?}", a);
324    pretty_print(&a);
325
326    let EigDecomp {
327        eigenvalues,
328        right_eigenvectors,
329        ..
330    } = bd
331        .eigh(&mut a.clone())
332        .expect("Complex Hermitian eigenvalue decomposition failed");
333
334    pretty_print(&right_eigenvectors.clone().unwrap());
335    println!("{eigenvalues:?}");
336
337    // For Hermitian matrices, eigenvalues should be real
338    for i in 0..n {
339        assert_relative_eq!(eigenvalues[[0, i]].im(), 0.0, epsilon = 1e-10);
340    }
341
342    test_eigen_reconstruction(&a, &eigenvalues, &right_eigenvectors.unwrap());
343}
344
345pub fn test_eig_full_non_square(bd: &impl Eig<f64>) {
346    let n = 3;
347    let m = 5;
348    let a = random_matrix(m, n);
349
350    let EigDecomp { .. } = bd
351        .eig_full(&mut a.clone())
352        .expect("Full eigenvalue decomposition failed");
353}
354
355pub fn test_eig_values_non_square(bd: &impl Eig<f64>) {
356    let n = 3;
357    let m = 5;
358    let a = random_matrix(m, n);
359
360    let EigDecomp { .. } = bd
361        .eig_values(&mut a.clone())
362        .expect("Eigenvalues computation failed");
363}
364
365pub fn test_schur(bd: &impl Eig<f64>) {
366    let n = 4;
367    let a = random_matrix(n, n);
368
369    let SchurDecomp { t, z } = bd
370        .schur(&mut a.clone())
371        .expect("Schur decomposition failed");
372
373    assert_eq!(t.shape(), &(n, n));
374    assert_eq!(z.shape(), &(n, n));
375
376    let zt = z.transpose().to_tensor();
377
378    println!("{a:?}");
379    println!("{t:?}");
380    println!("{z:?}");
381
382    let reconstructed_tmp = naive_matmul(&z, &t);
383    let a_reconstructed = naive_matmul(&reconstructed_tmp, &zt);
384
385    assert_matrix_eq!(&a, &a_reconstructed);
386}
387
388pub fn test_schur_cplx(bd: &impl Eig<Complex<f64>>) {
389    let n = 4;
390    let a = random_matrix(n, n);
391    let b = random_matrix(n, n);
392
393    let c = DTensor::<Complex<f64>, 2>::from_fn([n, n], |i| {
394        Complex::new(a[[i[0], i[1]]], b[[i[0], i[1]]])
395    });
396
397    let SchurDecomp { t, z } = bd
398        .schur_complex(&mut c.clone())
399        .expect("Schur decomposition failed");
400
401    assert_eq!(t.shape(), &(n, n));
402    assert_eq!(z.shape(), &(n, n));
403
404    let mut zt = z.transpose().to_tensor();
405
406    for i in 0..n {
407        for j in 0..n {
408            zt[[i, j]] = zt[[i, j]].conj();
409        }
410    }
411
412    println!("{c:?}");
413    println!("{t:?}");
414    println!("{z:?}");
415
416    let c_reconstructed_tmp = naive_matmul(&z, &t);
417    let c_reconstructed = naive_matmul(&c_reconstructed_tmp, &zt);
418
419    println!("---------------------------------------------");
420    println!("{c:?}");
421    println!("{c_reconstructed:?}");
422
423    assert_complex_matrix_eq!(&c, &c_reconstructed);
424}