Skip to main content

qsym2/target/noci/backend/solver/
mod.rs

1//! Solvers for eigenvalue problems.
2
3use anyhow::{self, ensure, format_err};
4use duplicate::duplicate_item;
5use itertools::Itertools;
6use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
7use ndarray_linalg::{Eig, EigGeneralized, Eigh, GeneralizedEigenvalue, Lapack, Scalar, UPLO};
8use num::traits::FloatConst;
9use num::{Float, One};
10use num_complex::{Complex, ComplexFloat};
11
12use crate::analysis::EigenvalueComparisonMode;
13use crate::auxiliary::misc::{
14    check_complex_matrix_symmetry, check_real_matrix_symmetry, normalise_eigenvectors_complex,
15    normalise_eigenvectors_real, regularise_eigenvectors, sort_eigenvalues_eigenvectors,
16};
17use crate::target::noci::backend::nonortho::CanonicalOrthogonalisable;
18
19pub mod noci;
20
21#[cfg(test)]
22#[path = "solver_tests.rs"]
23mod solver_tests;
24
25// -----------------------------
26// GeneralisedEigenvalueSolvable
27// -----------------------------
28
29/// Trait to solve the generalised eigenvalue equation for a pair of square matrices $`\mathbf{A}`$
30/// and $`\mathbf{B}`$:
31/// ```math
32///     \mathbf{A} \mathbf{v} = \lambda \mathbf{B} \mathbf{v},
33/// ```
34/// where $`\mathbf{A}`$ and $`\mathbf{B}`$ are in general non-Hermitian and non-positive-definite.
35pub trait GeneralisedEigenvalueSolvable {
36    /// Numerical type of the matrix elements constituting the generalised eigenvalue problem.
37    type NumType;
38
39    /// Numerical type of the various thresholds for comparison.
40    type RealType;
41
42    /// Solves the *auxiliary* generalised eigenvalue problem
43    /// ```math
44    ///     \tilde{\mathbf{A}} \tilde{\mathbf{v}} = \tilde{\lambda} \tilde{\mathbf{B}} \tilde{\mathbf{v}},
45    /// ```
46    /// where $`\tilde{\mathbf{B}}`$ is the canonical-orthogonalised version of $`\mathbf{B}`$. If
47    /// $`\mathbf{B}`$ is not of full rank, then the two eigenvalue problems are different.
48    ///
49    /// # Arguments
50    ///
51    /// * `complex_symmetric` - Boolean indicating whether the provided pair of matrices are
52    ///   complex-symmetric.
53    /// * `thresh_offdiag` - Threshold for checking if any off-diagonal elements are non-zero when
54    ///   verifying orthogonality.
55    /// * `thresh_zeroov` - Threshold for determining zero eigenvalues of $`\mathbf{B}`$.
56    /// * `eigenvalue_comparison_mode` - Comparison mode for sorting eigenvalues and their
57    ///   corresponding eigenvectors.
58    ///
59    /// # Returns
60    ///
61    /// The generalised eigenvalue result.
62    fn solve_generalised_eigenvalue_problem_with_canonical_orthogonalisation(
63        &self,
64        complex_symmetric: bool,
65        thresh_offdiag: Self::RealType,
66        thresh_zeroov: Self::RealType,
67        eigenvalue_comparison_mode: EigenvalueComparisonMode,
68    ) -> Result<GeneralisedEigenvalueResult<Self::NumType>, anyhow::Error>;
69
70    /// Solves the generalised eigenvalue problem using LAPACK's `?ggev` generalised eigensolver.
71    ///
72    /// Note that this can be numerically unstable if $`\mathbf{B}`$ is not of full rank.
73    ///
74    /// # Arguments
75    ///
76    /// * `complex_symmetric` - Boolean indicating whether the provided pair of matrices are
77    ///   complex-symmetric.
78    /// * `thresh_offdiag` - Threshold for checking if any off-diagonal elements are non-zero when
79    ///   verifying orthogonality.
80    /// * `thresh_zeroov` - Threshold for determining zero eigenvalues of $`\mathbf{B}`$.
81    /// * `eigenvalue_comparison_mode` - Comparison mode for sorting eigenvalues and their
82    ///   corresponding eigenvectors.
83    ///
84    /// # Returns
85    ///
86    /// The generalised eigenvalue result.
87    fn solve_generalised_eigenvalue_problem_with_ggev(
88        &self,
89        complex_symmetric: bool,
90        thresh_offdiag: Self::RealType,
91        thresh_zeroov: Self::RealType,
92        eigenvalue_comparison_mode: EigenvalueComparisonMode,
93    ) -> Result<GeneralisedEigenvalueResult<Self::NumType>, anyhow::Error>;
94}
95
96/// Structure containing the eigenvalues and eigenvectors of a generalised
97/// eigenvalue problem.
98pub struct GeneralisedEigenvalueResult<T> {
99    /// The resulting eigenvalues.
100    eigenvalues: Array1<T>,
101
102    /// The corresponding eigenvectors.
103    eigenvectors: Array2<T>,
104}
105
106impl<T> GeneralisedEigenvalueResult<T> {
107    /// Returns the eigenvalues.
108    pub fn eigenvalues(&'_ self) -> ArrayView1<'_, T> {
109        self.eigenvalues.view()
110    }
111
112    /// Returns the eigenvectors.
113    pub fn eigenvectors(&'_ self) -> ArrayView2<'_, T> {
114        self.eigenvectors.view()
115    }
116}
117
118#[duplicate_item(
119    [
120        dtype_ [ f64 ]
121    ]
122    [
123        dtype_ [ f32 ]
124    ]
125)]
126impl GeneralisedEigenvalueSolvable for (&ArrayView2<'_, dtype_>, &ArrayView2<'_, dtype_>) {
127    type NumType = dtype_;
128    type RealType = dtype_;
129
130    fn solve_generalised_eigenvalue_problem_with_canonical_orthogonalisation(
131        &self,
132        _: bool,
133        thresh_offdiag: dtype_,
134        thresh_zeroov: dtype_,
135        eigenvalue_comparison_mode: EigenvalueComparisonMode,
136    ) -> Result<GeneralisedEigenvalueResult<Self::NumType>, anyhow::Error> {
137        let (hmat, smat) = (self.0.to_owned(), self.1.to_owned());
138
139        // Symmetrise `hmat` and `smat` to improve numerical stability
140        let (hmat, smat): (Array2<dtype_>, Array2<dtype_>) = {
141            // Real, symmetric S and H
142            check_real_matrix_symmetry(&hmat.view(), thresh_offdiag, "Hamiltonian", "H")?;
143            check_real_matrix_symmetry(&smat.view(), thresh_offdiag, "Overlap", "S")?;
144
145            (
146                (hmat.to_owned() + hmat.t().to_owned()).map(|v| v / (2.0)),
147                (smat.to_owned() + smat.t().to_owned()).map(|v| v / (2.0)),
148            )
149        };
150
151        // CanonicalOrthogonalisationResult::calc_canonical_orthogonal_matrix checks for
152        // real-symmetry of S.
153        // This will fail over the reals if smat contains negative eigenvalues.
154        let xmat_res = smat.view().calc_canonical_orthogonal_matrix(
155            true,
156            false,
157            thresh_offdiag,
158            thresh_zeroov,
159            &eigenvalue_comparison_mode
160        )?;
161
162        let xmat = xmat_res.xmat();
163        let xmat_d = xmat_res.xmat_d();
164
165        let hmat_t = xmat_d.dot(&hmat).dot(&xmat);
166        let smat_t = xmat_d.dot(&smat).dot(&xmat);
167
168        log::debug!("Canonical-orthogonalised NOCI Hamiltonian matrix H~:\n  {hmat_t:+.8e}");
169        log::debug!("Canonical-orthogonalised NOCI overlap matrix S~:\n  {smat_t:+.8e}");
170
171        // Over the reals, canonical orthogonalisation cannot handle `smat` with negative
172        // eigenvalues. This means that `smat_t` can only be the identity.
173        let (pos, max_diff) = (&smat_t - &Array2::<dtype_>::eye(smat_t.nrows()))
174            .iter()
175            .map(|x| ComplexFloat::abs(*x))
176            .enumerate()
177            .max_by(|(_, x), (_, y)| {
178                x.partial_cmp(y)
179                    .expect("Unable to compare two `abs` values.")
180            })
181            .ok_or_else(|| {
182                format_err!("Unable to determine the maximum element of the |S - I| matrix.")
183            })?;
184        let (pos_i, pos_j) = (pos.div_euclid(hmat.ncols()), pos.rem_euclid(hmat.ncols()));
185        ensure!(
186            max_diff <= thresh_offdiag,
187            "The orthogonalised overlap matrix is not the identity matrix: the maximum absolute deviation is {max_diff:.3e} > {thresh_offdiag:.3e} at ({pos_i}, {pos_j})."
188        );
189
190        let (eigvals_t, eigvecs_t) = hmat_t.eigh(UPLO::Lower)?;
191
192        // Sort the eigenvalues and eigenvectors
193        let (eigvals_t_sorted, eigvecs_t_sorted) = sort_eigenvalues_eigenvectors(
194            &eigvals_t.view(),
195            &eigvecs_t.view(),
196            &eigenvalue_comparison_mode,
197        );
198        let eigvecs_sorted = xmat.dot(&eigvecs_t_sorted);
199
200        // Normalise the eigenvectors
201        let eigvecs_sorted_normalised =
202            normalise_eigenvectors_real(&eigvecs_sorted.view(), &smat.view(), thresh_offdiag)?;
203
204        // Regularise the eigenvectors
205        let eigvecs_sorted_normalised_regularised =
206            regularise_eigenvectors(&eigvecs_sorted_normalised.view(), thresh_offdiag);
207
208        Ok(GeneralisedEigenvalueResult {
209            eigenvalues: eigvals_t_sorted,
210            eigenvectors: eigvecs_sorted_normalised_regularised,
211        })
212    }
213
214    fn solve_generalised_eigenvalue_problem_with_ggev(
215        &self,
216        _: bool,
217        thresh_offdiag: dtype_,
218        thresh_zeroov: dtype_,
219        eigenvalue_comparison_mode: EigenvalueComparisonMode,
220    ) -> Result<GeneralisedEigenvalueResult<Self::NumType>, anyhow::Error> {
221        let (hmat, smat) = (self.0.to_owned(), self.1.to_owned());
222
223        // Real, symmetric S and H
224        check_real_matrix_symmetry(&hmat.view(), thresh_offdiag, "Hamiltonian", "H")?;
225        check_real_matrix_symmetry(&smat.view(), thresh_offdiag, "Overlap", "S")?;
226
227        let (geneigvals, eigvecs) =
228            (hmat.clone(), smat.clone()).eig_generalized(Some(thresh_zeroov))?;
229
230        for gv in geneigvals.iter() {
231            if let GeneralizedEigenvalue::Finite(v, _) = gv {
232                ensure!(
233                    v.im().abs() <= thresh_offdiag,
234                    "Unexpected complex eigenvalue {v} for real, symmetric S and H."
235                );
236            }
237        }
238
239        // Filter and sort the eigenvalues and eigenvectors
240        let mut indices = (0..geneigvals.len())
241            .filter(|i| matches!(geneigvals[*i], GeneralizedEigenvalue::Finite(_, _)))
242            .collect_vec();
243
244        match eigenvalue_comparison_mode {
245            EigenvalueComparisonMode::Modulus => {
246                indices.sort_by(|i, j| {
247                    if let (
248                        GeneralizedEigenvalue::Finite(e_i, _),
249                        GeneralizedEigenvalue::Finite(e_j, _),
250                    ) = (&geneigvals[*i], &geneigvals[*j])
251                    {
252                        ComplexFloat::abs(*e_i)
253                            .partial_cmp(&ComplexFloat::abs(*e_j))
254                            .unwrap()
255                    } else {
256                        panic!("Unable to compare some eigenvalues.")
257                    }
258                });
259            }
260            EigenvalueComparisonMode::Real => {
261                indices.sort_by(|i, j| {
262                    if let (
263                        GeneralizedEigenvalue::Finite(e_i, _),
264                        GeneralizedEigenvalue::Finite(e_j, _),
265                    ) = (&geneigvals[*i], &geneigvals[*j])
266                    {
267                        e_i.re().partial_cmp(&e_j.re()).unwrap()
268                    } else {
269                        panic!("Unable to compare some eigenvalues.")
270                    }
271                });
272            }
273        }
274
275        let eigvals_re_sorted = geneigvals.select(Axis(0), &indices).map(|gv| {
276            if let GeneralizedEigenvalue::Finite(v, _) = gv {
277                v.re()
278            } else {
279                panic!("Unexpected indeterminate eigenvalue.")
280            }
281        });
282        let eigvecs_sorted = eigvecs.select(Axis(1), &indices);
283        ensure!(
284            eigvecs_sorted.iter().all(|v| v.im().abs() < thresh_offdiag),
285            "Unexpected complex eigenvectors."
286        );
287        let eigvecs_re_sorted = eigvecs_sorted.map(|v| v.re());
288
289        // Normalise the eigenvectors
290        let eigvecs_re_sorted_normalised =
291            normalise_eigenvectors_real(&eigvecs_re_sorted.view(), &smat.view(), thresh_offdiag)?;
292
293        // Regularise the eigenvectors
294        let eigvecs_re_sorted_normalised_regularised =
295            regularise_eigenvectors(&eigvecs_re_sorted_normalised.view(), thresh_offdiag);
296
297        Ok(GeneralisedEigenvalueResult {
298            eigenvalues: eigvals_re_sorted,
299            eigenvectors: eigvecs_re_sorted_normalised_regularised,
300        })
301    }
302}
303
304impl<T> GeneralisedEigenvalueSolvable for (&ArrayView2<'_, Complex<T>>, &ArrayView2<'_, Complex<T>>)
305where
306    T: Float + FloatConst + Scalar<Complex = Complex<T>>,
307    Complex<T>: ComplexFloat<Real = T> + Scalar<Real = T, Complex = Complex<T>> + Lapack,
308    for<'a> ArrayView2<'a, Complex<T>>:
309        CanonicalOrthogonalisable<NumType = Complex<T>, RealType = T>,
310{
311    type NumType = Complex<T>;
312
313    type RealType = T;
314
315    fn solve_generalised_eigenvalue_problem_with_canonical_orthogonalisation(
316        &self,
317        complex_symmetric: bool,
318        thresh_offdiag: T,
319        thresh_zeroov: T,
320        eigenvalue_comparison_mode: EigenvalueComparisonMode,
321    ) -> Result<GeneralisedEigenvalueResult<Complex<T>>, anyhow::Error> {
322        let (hmat, smat) = (self.0.to_owned(), self.1.to_owned());
323
324        // Symmetrise `hmat` and `smat` to improve numerical stability
325        let (hmat, smat): (Array2<Complex<T>>, Array2<Complex<T>>) = if complex_symmetric {
326            // Complex-symmetric
327            check_complex_matrix_symmetry(
328                &hmat.view(),
329                complex_symmetric,
330                thresh_offdiag,
331                "Hamiltonian",
332                "H",
333            )?;
334            check_complex_matrix_symmetry(
335                &smat.view(),
336                complex_symmetric,
337                thresh_offdiag,
338                "Overlap",
339                "S",
340            )?;
341            (
342                (hmat.to_owned() + hmat.t().to_owned())
343                    .map(|v| v / (Complex::<T>::one() + Complex::<T>::one())),
344                (smat.to_owned() + smat.t().to_owned())
345                    .map(|v| v / (Complex::<T>::one() + Complex::<T>::one())),
346            )
347        } else {
348            // Complex-Hermitian
349            check_complex_matrix_symmetry(
350                &hmat.view(),
351                complex_symmetric,
352                thresh_offdiag,
353                "Hamiltonian",
354                "H",
355            )?;
356            check_complex_matrix_symmetry(
357                &smat.view(),
358                complex_symmetric,
359                thresh_offdiag,
360                "Overlap",
361                "S",
362            )?;
363            (
364                (hmat.to_owned() + hmat.map(|v| v.conj()).t().to_owned())
365                    .map(|v| v / (Complex::<T>::one() + Complex::<T>::one())),
366                (smat.to_owned() + smat.map(|v| v.conj()).t().to_owned())
367                    .map(|v| v / (Complex::<T>::one() + Complex::<T>::one())),
368            )
369        };
370
371        // CanonicalOrthogonalisationResult::calc_canonical_orthogonal_matrix checks for
372        // complex-symmetry or complex-Hermiticity of S.
373        let xmat_res = smat.view().calc_canonical_orthogonal_matrix(
374            complex_symmetric,
375            false,
376            thresh_offdiag,
377            thresh_zeroov,
378            &eigenvalue_comparison_mode
379        )?;
380
381        let xmat = xmat_res.xmat();
382        let xmat_d = xmat_res.xmat_d();
383
384        let hmat_t = xmat_d.dot(&hmat).dot(&xmat);
385        let smat_t = xmat_d.dot(&smat).dot(&xmat);
386
387        // Symmetrise `hmat_t` and `smat_t` to improve numerical stability
388        let (hmat_t_sym, smat_t_sym): (Array2<Complex<T>>, Array2<Complex<T>>) =
389            if complex_symmetric {
390                // Complex-symmetric
391                check_complex_matrix_symmetry(
392                    &hmat_t.view(),
393                    complex_symmetric,
394                    thresh_offdiag,
395                    "Transformed Hamiltonian",
396                    "H~",
397                )?;
398                check_complex_matrix_symmetry(
399                    &smat_t.view(),
400                    complex_symmetric,
401                    thresh_offdiag,
402                    "Transformed Overlap",
403                    "S~",
404                )?;
405                let hmat_t_s = (hmat_t.to_owned() + hmat_t.t().to_owned())
406                    .map(|v| v / (Complex::<T>::one() + Complex::<T>::one()));
407                let smat_t_s = (smat_t.to_owned() + smat_t.t().to_owned())
408                    .map(|v| v / (Complex::<T>::one() + Complex::<T>::one()));
409                (hmat_t_s, smat_t_s)
410            } else {
411                // Complex-Hermitian
412                check_complex_matrix_symmetry(
413                    &hmat_t.view(),
414                    complex_symmetric,
415                    thresh_offdiag,
416                    "Transformed Hamiltonian",
417                    "H~",
418                )?;
419                check_complex_matrix_symmetry(
420                    &smat_t.view(),
421                    complex_symmetric,
422                    thresh_offdiag,
423                    "Transformed Overlap",
424                    "S~",
425                )?;
426                let hmat_t_s = (hmat_t.to_owned() + hmat_t.map(|v| v.conj()).t().to_owned())
427                    .map(|v| v / (Complex::<T>::one() + Complex::<T>::one()));
428                let smat_t_s = (smat_t.to_owned() + smat_t.map(|v| v.conj()).t().to_owned())
429                    .map(|v| v / (Complex::<T>::one() + Complex::<T>::one()));
430                (hmat_t_s, smat_t_s)
431            };
432        let smat_t_sym_d = smat_t_sym.map(|v| v.conj()).t().to_owned();
433        log::debug!("Complex-symmetric? {complex_symmetric}");
434        log::debug!("Canonical orthogonalisation X matrix:\n  {xmat:+.8e}");
435        log::debug!("Canonical-orthogonalised NOCI Hamiltonian matrix H~:\n  {hmat_t_sym:+.8e}");
436        log::debug!("Canonical-orthogonalised NOCI overlap matrix S~:\n  {smat_t_sym:+.8e}");
437
438        // smat_t_sym is not necessarily the identity, but is guaranteed to be Hermitian.
439        let max_diff = (&smat_t_sym_d.dot(&smat_t_sym) - &Array2::<T>::eye(smat_t_sym.nrows()))
440            .iter()
441            .map(|x| ComplexFloat::abs(*x))
442            .max_by(|x, y| {
443                x.partial_cmp(y)
444                    .expect("Unable to compare two `abs` values.")
445            })
446            .ok_or_else(|| {
447                format_err!("Unable to determine the maximum element of the |S^†.S - I| matrix.")
448            })?;
449        ensure!(
450            max_diff <= thresh_offdiag,
451            "The S^†.S matrix is not the identity matrix. S is therefore not Hermitian."
452        );
453        let smat_t_sym_d_hmat_t_sym = smat_t_sym_d.dot(&hmat_t_sym);
454        log::debug!(
455            "Hamiltonian matrix for diagonalisation (S~)^†.(H~):\n  {smat_t_sym_d_hmat_t_sym:+.8e}"
456        );
457
458        let (eigvals_t, eigvecs_t) = smat_t_sym_d_hmat_t_sym.eig()?;
459
460        // Sort the eigenvalues and eigenvectors
461        let (eigvals_t_sorted, eigvecs_t_sorted) = sort_eigenvalues_eigenvectors(
462            &eigvals_t.view(),
463            &eigvecs_t.view(),
464            &eigenvalue_comparison_mode,
465        );
466        log::debug!("Sorted eigenvalues of (S~)^†.(H~):");
467        for (i, eigval) in eigvals_t_sorted.iter().enumerate() {
468            log::debug!("  {i}: {eigval:+.8e}");
469        }
470        log::debug!("");
471        log::debug!("Sorted eigenvectors of (S~)^†.(H~):\n  {eigvecs_t_sorted:+.8e}");
472        log::debug!("");
473
474        // Check orthogonality
475        // let _ = normalise_eigenvectors_complex(
476        //     &eigvecs_t.view(),
477        //     &smat_t.view(),
478        //     complex_symmetric,
479        //     Some(thresh_offdiag),
480        // )?;
481
482        let eigvecs_sorted = xmat.dot(&eigvecs_t_sorted);
483
484        // Normalise the eigenvectors
485        let eigvecs_sorted_normalised = normalise_eigenvectors_complex(
486            &eigvecs_sorted.view(),
487            &smat.view(),
488            complex_symmetric,
489            None,
490        )?;
491
492        // Regularise the eigenvectors
493        let eigvecs_sorted_normalised_regularised =
494            regularise_eigenvectors(&eigvecs_sorted_normalised.view(), thresh_offdiag);
495
496        Ok(GeneralisedEigenvalueResult {
497            eigenvalues: eigvals_t_sorted,
498            eigenvectors: eigvecs_sorted_normalised_regularised,
499        })
500    }
501
502    fn solve_generalised_eigenvalue_problem_with_ggev(
503        &self,
504        complex_symmetric: bool,
505        thresh_offdiag: T,
506        thresh_zeroov: T,
507        eigenvalue_comparison_mode: EigenvalueComparisonMode,
508    ) -> Result<GeneralisedEigenvalueResult<Self::NumType>, anyhow::Error> {
509        let (hmat, smat) = (self.0.to_owned(), self.1.to_owned());
510
511        check_complex_matrix_symmetry(
512            &hmat.view(),
513            complex_symmetric,
514            thresh_offdiag,
515            "Hamiltonian",
516            "H",
517        )?;
518        check_complex_matrix_symmetry(
519            &smat.view(),
520            complex_symmetric,
521            thresh_offdiag,
522            "Overlap",
523            "S",
524        )?;
525
526        let (geneigvals, eigvecs) =
527            (hmat.clone(), smat.clone()).eig_generalized(Some(thresh_zeroov))?;
528
529        // Filter and sort the eigenvalues and eigenvectors
530        let mut indices = (0..geneigvals.len())
531            .filter(|i| matches!(geneigvals[*i], GeneralizedEigenvalue::Finite(_, _)))
532            .collect_vec();
533
534        match eigenvalue_comparison_mode {
535            EigenvalueComparisonMode::Modulus => {
536                indices.sort_by(|i, j| {
537                    if let (
538                        GeneralizedEigenvalue::Finite(e_i, _),
539                        GeneralizedEigenvalue::Finite(e_j, _),
540                    ) = (&geneigvals[*i], &geneigvals[*j])
541                    {
542                        ComplexFloat::abs(*e_i)
543                            .partial_cmp(&ComplexFloat::abs(*e_j))
544                            .unwrap()
545                    } else {
546                        panic!("Unable to compare some eigenvalues.")
547                    }
548                });
549            }
550            EigenvalueComparisonMode::Real => {
551                indices.sort_by(|i, j| {
552                    if let (
553                        GeneralizedEigenvalue::Finite(e_i, _),
554                        GeneralizedEigenvalue::Finite(e_j, _),
555                    ) = (&geneigvals[*i], &geneigvals[*j])
556                    {
557                        e_i.re().partial_cmp(&e_j.re()).unwrap()
558                    } else {
559                        panic!("Unable to compare some eigenvalues.")
560                    }
561                });
562            }
563        }
564
565        let eigvals_sorted = geneigvals.select(Axis(0), &indices).map(|gv| {
566            if let GeneralizedEigenvalue::Finite(v, _) = gv {
567                *v
568            } else {
569                panic!("Unexpected indeterminate eigenvalue.")
570            }
571        });
572        let eigvecs_sorted = eigvecs.select(Axis(1), &indices);
573
574        // Normalise the eigenvectors
575        let eigvecs_sorted_normalised = normalise_eigenvectors_complex(
576            &eigvecs_sorted.view(),
577            &smat.view(),
578            complex_symmetric,
579            Some(thresh_offdiag),
580        )?;
581
582        // Regularise the eigenvectors
583        let eigvecs_sorted_normalised_regularised =
584            regularise_eigenvectors(&eigvecs_sorted_normalised.view(), thresh_offdiag);
585
586        Ok(GeneralisedEigenvalueResult {
587            eigenvalues: eigvals_sorted,
588            eigenvectors: eigvecs_sorted_normalised_regularised,
589        })
590    }
591}