Skip to main content

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

1use std::fmt::LowerExp;
2use std::iter::Product;
3
4use anyhow::{self, ensure, format_err};
5use derive_builder::Builder;
6use duplicate::duplicate_item;
7use indexmap::IndexSet;
8use itertools::Itertools;
9use ndarray::{
10    Array1, Array2, ArrayView1, ArrayView2, ArrayView4, Axis, Ix0, Ix2, ScalarOperand, stack,
11};
12use ndarray_einsum::einsum;
13use ndarray_linalg::types::Lapack;
14use ndarray_linalg::{Determinant, Eig, Eigh, SVD, Scalar, UPLO};
15use num::{Complex, Float};
16use num_complex::ComplexFloat;
17
18use crate::analysis::EigenvalueComparisonMode;
19use crate::angmom::spinor_rotation_3d::StructureConstraint;
20use crate::auxiliary::misc::{check_complex_matrix_symmetry, check_real_matrix_symmetry};
21
22use super::denmat::{calc_unweighted_codensity_matrix, calc_weighted_codensity_matrix};
23
24#[cfg(test)]
25#[path = "nonortho_tests.rs"]
26mod nonortho_tests;
27
28/// Structure containing Löwdin-paired coefficients, the corresponding Löwdin overlaps, and the
29/// indices of the zero overlaps.
30///
31/// The Löwdin-paired coefficients satisfy
32/// ```math
33///     ^{wx}\mathbf{\Lambda}
34///         = \mathrm{diag}(^{wx}\lambda_i)
35///         = ^{w}\!\tilde{\mathbf{C}}^{\dagger\lozenge}
36///           \ \mathbf{S}_{\mathrm{AO}}
37///           \ ^{x}\tilde{\mathbf{C}}.
38/// ```
39#[derive(Builder)]
40#[builder(build_fn(validate = "Self::validate"))]
41pub struct LowdinPairedCoefficients<T: ComplexFloat> {
42    /// The $`^{w}\!\tilde{\mathbf{C}}`$ coefficient matrix.
43    paired_cw: Array2<T>,
44
45    /// The $`^{x}\!\tilde{\mathbf{C}}`$ coefficient matrix.
46    paired_cx: Array2<T>,
47
48    /// The Löwdin overlaps $`\{^{wx}\lambda_i\}`$.
49    lowdin_overlaps: Vec<T>,
50
51    /// The indices of the zero overlaps with respect to [`Self::thresh_zeroov`]. If these are not
52    /// provided, the specified [`Self::thresh_zeroov`] will be used to deduce them from the
53    /// specified `[Self::lowdin_overlaps]`.
54    #[builder(default = "self.default_zero_indices()?")]
55    zero_indices: IndexSet<usize>,
56
57    /// Threshold for determining zero overlaps.
58    thresh_zeroov: T::Real,
59
60    /// Boolean indicating whether the coefficients have been Löwdin-paired with respect to the
61    /// complex-symmetric inner product.
62    complex_symmetric: bool,
63}
64
65impl<T: ComplexFloat> LowdinPairedCoefficientsBuilder<T> {
66    fn validate(&self) -> Result<(), String> {
67        let paired_cw = self
68            .paired_cw
69            .as_ref()
70            .ok_or("Löwdin-paired coefficients `paired_cw` not set.".to_string())?;
71        let paired_cx = self
72            .paired_cx
73            .as_ref()
74            .ok_or("Löwdin-paired coefficients `paired_cx` not set.".to_string())?;
75        let lowdin_overlaps = self
76            .lowdin_overlaps
77            .as_ref()
78            .ok_or("Löwdin overlaps not set.".to_string())?;
79        let zero_indices = self
80            .zero_indices
81            .as_ref()
82            .ok_or("Indices of zero Löwdin overlaps not set.".to_string())?;
83
84        if paired_cw.shape() == paired_cx.shape() {
85            let lowdin_dim = paired_cw.shape()[1];
86            if lowdin_dim == lowdin_overlaps.len() {
87                if zero_indices.iter().all(|i| *i < lowdin_dim) {
88                    Ok(())
89                } else {
90                    Err("Some indices of zero Löwdin overlaps are out-of-bound!".to_string())
91                }
92            } else {
93                Err(
94                    "Inconsistent number of Löwdin-paired orbitals and Löwdin overlaps."
95                        .to_string(),
96                )
97            }
98        } else {
99            Err(format!(
100                "Inconsistent shapes between `paired_cw` ({:?}) and `paired_cx` ({:?}).",
101                paired_cw.shape(),
102                paired_cx.shape()
103            ))
104        }
105    }
106
107    fn default_zero_indices(&self) -> Result<IndexSet<usize>, String> {
108        let lowdin_overlaps = self
109            .lowdin_overlaps
110            .as_ref()
111            .ok_or("Löwdin overlaps not set.".to_string())?;
112        let thresh_zeroov = self
113            .thresh_zeroov
114            .as_ref()
115            .ok_or("threshold for zero Löwdin overlaps not set.".to_string())?;
116        let zero_indices = lowdin_overlaps
117            .iter()
118            .enumerate()
119            .filter(|(_, ov)| ComplexFloat::abs(**ov) < *thresh_zeroov)
120            .map(|(i, _)| i)
121            .collect::<IndexSet<_>>();
122        Ok(zero_indices)
123    }
124}
125
126impl<T: ComplexFloat> LowdinPairedCoefficients<T> {
127    pub fn builder() -> LowdinPairedCoefficientsBuilder<T> {
128        LowdinPairedCoefficientsBuilder::<T>::default()
129    }
130
131    /// Returns the Löwdin-paired coefficients.
132    pub fn paired_coefficients(&self) -> (&Array2<T>, &Array2<T>) {
133        (&self.paired_cw, &self.paired_cx)
134    }
135
136    /// Returns the number of atomic-orbital basis functions in which the coefficient matrices are
137    /// expressed.
138    pub fn nbasis(&self) -> usize {
139        self.paired_cw.nrows()
140    }
141
142    /// Returns the number of molecular orbitals being Löwdin-paired.
143    pub fn lowdin_dim(&self) -> usize {
144        self.lowdin_overlaps.len()
145    }
146
147    /// Returns the number of zero Löwdin overlaps.
148    pub fn n_lowdin_zeros(&self) -> usize {
149        self.zero_indices.len()
150    }
151
152    /// Returns the Löwdin overlaps.
153    pub fn lowdin_overlaps(&self) -> &Vec<T> {
154        &self.lowdin_overlaps
155    }
156
157    /// Returns the indices of the zero Löwdin overlaps.
158    pub fn zero_indices(&self) -> &IndexSet<usize> {
159        &self.zero_indices
160    }
161
162    /// Returns the threshold for determining the zero Löwdin overlaps.
163    pub fn thresh_zeroov(&self) -> T::Real {
164        self.thresh_zeroov
165    }
166
167    /// Returns the indices of the non-zero Löwdin overlaps.
168    pub fn nonzero_indices(&self) -> IndexSet<usize> {
169        (0..self.lowdin_dim())
170            .filter(|i| !self.zero_indices.contains(i))
171            .collect::<IndexSet<_>>()
172    }
173
174    /// Returns the boolean indicating whether the Löwdin pairing is with respect to the
175    /// complex-symmetric inner product.
176    pub fn complex_symmetric(&self) -> bool {
177        self.complex_symmetric
178    }
179}
180
181impl<T: ComplexFloat + Product> LowdinPairedCoefficients<T> {
182    /// The reduced overlap between the two determinants.
183    pub fn reduced_overlap(&self) -> T {
184        self.nonzero_indices()
185            .iter()
186            .map(|i| self.lowdin_overlaps[*i])
187            .product()
188    }
189}
190
191/// Performs Löwdin pairing on two coefficient matrices $`^{w}\mathbf{C}`$ and
192/// $`^{x}\mathbf{C}`$.
193///
194/// Löwdin pairing ensures that
195/// ```math
196///     ^{wx}\mathbf{\Lambda}
197///         = \mathrm{diag}(^{wx}\lambda_i)
198///         = ^{w}\!\tilde{\mathbf{C}}^{\dagger\lozenge}
199///           \ \mathbf{S}_{\mathrm{AO}}
200///           \ ^{x}\tilde{\mathbf{C}},
201/// ```
202/// where the Löwdin-paired coefficient matrices are given by
203/// ```math
204///     \begin{align*}
205///         ^{w}\!\tilde{\mathbf{C}}
206///             &=\ ^{w}\mathbf{C}\ ^{wx}\mathbf{U}^{\lozenge} \\
207///         ^{x}\!\tilde{\mathbf{C}}
208///             &=\ ^{x}\mathbf{C}\ ^{wx}\mathbf{V}
209///     \end{align*}
210/// ```
211/// with $`^{wx}\mathbf{U}`$ and $`^{wx}\mathbf{V}`$ being SVD factorisation matrices:
212/// ```math
213///     ^{w}\mathbf{C}^{\dagger\lozenge}
214///     \ \mathbf{S}_{\mathrm{AO}}
215///     \ ^{x}\mathbf{C}
216///     =
217///     \ ^{wx}\mathbf{U}
218///     \ ^{wx}\mathbf{\Lambda}
219///     \ ^{wx}\mathbf{V}^{\dagger}.
220/// ```
221///
222/// We note that the first columns of $`^{w}\!\tilde{\mathbf{C}}`$ and $`^{x}\!\tilde{\mathbf{C}}`$
223/// are also adjusted by the determinants of $`^{wx}\mathbf{U}`$ and $`^{wx}\mathbf{V}`$ as
224/// appropriate to ensure that the Slater determinants corresponding to them remain invariant with
225/// respect to the unitary transformations brought about by $`^{wx}\mathbf{U}`$ and $`^{wx}\mathbf{V}`$.
226///
227/// # Arguments
228///
229/// * `cw` - Coefficient matrix $`^{w}\mathbf{C}`$.
230/// * `cx` - Coefficient matrix $`^{x}\mathbf{C}`$.
231/// * `sao` - The overlap matrix $`\mathbf{S}_{\mathrm{AO}}`$ of the underlying atomic basis
232///   functions.
233/// * `complex_symmetric` - If `true`, $`\lozenge = \star`$. If `false`, $`\lozenge = \hat{e}`$.
234/// * `thresh_offdiag` - Threshold to check if the off-diagonal elements in the original orbital
235///   overlap matrix and in the Löwdin-paired orbital overlap matrix $`^{wx}\mathbf{\Lambda}`$ are zero.
236/// * `thresh_zeroov` - Threshold to identify which Löwdin overlaps $`^{wx}\lambda_i`$ are zero.
237///
238/// # Returns
239///
240/// A [`LowdinPairedCoefficients`] structure containing the result of the Löwdin pairing.
241pub fn calc_lowdin_pairing<T>(
242    cw: &ArrayView2<T>,
243    cx: &ArrayView2<T>,
244    sao: &ArrayView2<T>,
245    complex_symmetric: bool,
246    thresh_offdiag: <T as ComplexFloat>::Real,
247    thresh_zeroov: <T as ComplexFloat>::Real,
248) -> Result<LowdinPairedCoefficients<T>, anyhow::Error>
249where
250    T: ComplexFloat + Lapack,
251    <T as ComplexFloat>::Real: PartialOrd + LowerExp,
252{
253    if cw.shape() != cx.shape() {
254        Err(format_err!(
255            "Coefficient dimensions mismatched: cw ({:?}) !~ cx ({:?}).",
256            cw.shape(),
257            cx.shape()
258        ))
259    } else {
260        let init_orb_ovmat = if complex_symmetric {
261            einsum("ji,jk,kl->il", &[cw, sao, cx])
262        } else {
263            einsum("ji,jk,kl->il", &[&cw.map(|x| x.conj()), sao, cx])
264        }
265        .map_err(|err| format_err!(err))?
266        .into_dimensionality::<Ix2>()?;
267
268        let max_offdiag = (&init_orb_ovmat - &Array2::from_diag(&init_orb_ovmat.diag().to_owned()))
269            .iter()
270            .map(|x| ComplexFloat::abs(*x))
271            .max_by(|x, y| {
272                x.partial_cmp(y)
273                    .expect("Unable to compare two `abs` values.")
274            })
275            .ok_or_else(|| {
276                format_err!(
277                    "Unable to determine the maximum off-diagonal element for\n{}.",
278                    init_orb_ovmat
279                )
280            })?;
281
282        if max_offdiag <= thresh_offdiag {
283            let lowdin_overlaps = init_orb_ovmat.into_diag().to_vec();
284            let zero_indices = lowdin_overlaps
285                .iter()
286                .enumerate()
287                .filter(|(_, ov)| ComplexFloat::abs(**ov) < thresh_zeroov)
288                .map(|(i, _)| i)
289                .collect::<IndexSet<_>>();
290            LowdinPairedCoefficients::builder()
291                .paired_cw(cw.to_owned())
292                .paired_cx(cx.to_owned())
293                .lowdin_overlaps(lowdin_overlaps)
294                .zero_indices(zero_indices)
295                .thresh_zeroov(thresh_zeroov)
296                .complex_symmetric(complex_symmetric)
297                .build()
298                .map_err(|err| format_err!(err))
299        } else {
300            let (u_opt, _, vh_opt) = init_orb_ovmat.svd(true, true)?;
301            let u = u_opt.ok_or_else(|| format_err!("Unable to compute the U matrix from SVD."))?;
302            let vh =
303                vh_opt.ok_or_else(|| format_err!("Unable to compute the V matrix from SVD."))?;
304            let v = vh.t().map(|x| x.conj());
305            let det_v_c = v.det()?.conj();
306
307            let paired_cw = if complex_symmetric {
308                let uc = u.map(|x| x.conj());
309                let mut cwt = cw.dot(&uc);
310                let det_uc_c = uc.det()?.conj();
311                cwt.column_mut(0).iter_mut().for_each(|x| *x *= det_uc_c);
312                cwt
313            } else {
314                let mut cwt = cw.dot(&u);
315                let det_u_c = u.det()?.conj();
316                cwt.column_mut(0).iter_mut().for_each(|x| *x *= det_u_c);
317                cwt
318            };
319
320            let paired_cx = {
321                let mut cxt = cx.dot(&v);
322                cxt.column_mut(0).iter_mut().for_each(|x| *x *= det_v_c);
323                cxt
324            };
325
326            let lowdin_orb_ovmat = if complex_symmetric {
327                einsum("ji,jk,kl->il", &[&paired_cw, sao, &paired_cx])
328            } else {
329                einsum(
330                    "ji,jk,kl->il",
331                    &[&paired_cw.map(|x| x.conj()), sao, &paired_cx],
332                )
333            }
334            .map_err(|err| format_err!(err))?
335            .into_dimensionality::<Ix2>()?;
336
337            let max_offdiag_lowdin = (&lowdin_orb_ovmat - &Array2::from_diag(&lowdin_orb_ovmat.diag().to_owned()))
338                .iter()
339                .map(|x| ComplexFloat::abs(*x))
340                .max_by(|x, y| {
341                    x.partial_cmp(y)
342                        .expect("Unable to compare two `abs` values.")
343                })
344                .ok_or_else(|| format_err!("Unable to determine the maximum off-diagonal element of the Lowdin-paired overlap matrix."))?;
345            if max_offdiag_lowdin <= thresh_offdiag {
346                let lowdin_overlaps = lowdin_orb_ovmat.into_diag().to_vec();
347                let zero_indices = lowdin_overlaps
348                    .iter()
349                    .enumerate()
350                    .filter(|(_, ov)| ComplexFloat::abs(**ov) < thresh_zeroov)
351                    .map(|(i, _)| i)
352                    .collect::<IndexSet<_>>();
353                LowdinPairedCoefficients::builder()
354                    .paired_cw(paired_cw.clone())
355                    .paired_cx(paired_cx.clone())
356                    .lowdin_overlaps(lowdin_overlaps)
357                    .zero_indices(zero_indices)
358                    .thresh_zeroov(thresh_zeroov)
359                    .complex_symmetric(complex_symmetric)
360                    .build()
361                    .map_err(|err| format_err!(err))
362            } else {
363                Err(format_err!(
364                    "Löwdin overlap matrix deviates from diagonality. Maximum off-diagonal overlap has magnitude {max_offdiag_lowdin:.3e} > threshold of {thresh_offdiag:.3e}. Löwdin pairing has failed."
365                ))
366            }
367        }
368    }
369}
370
371/// Calculates the matrix element of a zero-particle operator between two Löwdin-paired
372/// determinants.
373///
374/// # Arguments
375///
376/// `lowdin_paired_coefficientss` - A sequence of pairs of Löwdin-paired coefficients, one for each
377/// subspace determined by the specified structure constraint.
378/// `o0` - The zero-particle operator.
379/// `structure_constraint` - The structure constraint governing the coefficients.
380///
381/// # Returns
382///
383/// The zero-particle matrix element.
384pub fn calc_o0_matrix_element<T, SC>(
385    lowdin_paired_coefficientss: &[LowdinPairedCoefficients<T>],
386    o0: T,
387    structure_constraint: &SC,
388) -> Result<T, anyhow::Error>
389where
390    T: ComplexFloat + ScalarOperand + Product,
391    SC: StructureConstraint,
392{
393    let nzeros_explicit: usize = lowdin_paired_coefficientss
394        .iter()
395        .map(|lpc| lpc.n_lowdin_zeros())
396        .sum();
397    let nzeros = nzeros_explicit * structure_constraint.implicit_factor()?;
398    if nzeros > 0 {
399        Ok(T::zero())
400    } else {
401        let reduced_ov_explicit: T = lowdin_paired_coefficientss
402            .iter()
403            .map(|lpc| lpc.reduced_overlap())
404            .product();
405        let reduced_ov = (0..structure_constraint.implicit_factor()?)
406            .fold(T::one(), |acc, _| acc * reduced_ov_explicit);
407        Ok(reduced_ov * o0)
408    }
409}
410
411/// Calculates the matrix element of a one-particle operator between two Löwdin-paired
412/// determinants.
413///
414/// # Arguments
415///
416/// * `lowdin_paired_coefficientss` - A sequence of pairs of Löwdin-paired coefficients, one for each
417///   subspace determined by the specified structure constraint.
418/// * `o1` - The one-particle operator in the atomic-orbital basis.
419/// * `structure_constraint` - The structure constraint governing the coefficients.
420///
421/// # Returns
422///
423/// The one-particle matrix element.
424pub fn calc_o1_matrix_element<T, SC>(
425    lowdin_paired_coefficientss: &[LowdinPairedCoefficients<T>],
426    o1: &ArrayView2<T>,
427    structure_constraint: &SC,
428) -> Result<T, anyhow::Error>
429where
430    T: ComplexFloat + ScalarOperand + Product,
431    SC: StructureConstraint,
432{
433    let nzeros_explicit: usize = lowdin_paired_coefficientss
434        .iter()
435        .map(|lpc| lpc.n_lowdin_zeros())
436        .sum();
437    let nzeros = nzeros_explicit * structure_constraint.implicit_factor()?;
438    if nzeros > 1 {
439        Ok(T::zero())
440    } else {
441        let reduced_ov_explicit: T = lowdin_paired_coefficientss
442            .iter()
443            .map(|lpc| lpc.reduced_overlap())
444            .product();
445        let reduced_ov = (0..structure_constraint.implicit_factor()?)
446            .fold(T::one(), |acc, _| acc * reduced_ov_explicit);
447
448        if nzeros == 0 {
449            let nbasis = lowdin_paired_coefficientss[0].nbasis();
450            let w = (0..structure_constraint.implicit_factor()?)
451                .cartesian_product(lowdin_paired_coefficientss.iter())
452                .try_fold(Array2::<T>::zeros((nbasis, nbasis)), |acc, (_, lpc)| {
453                    calc_weighted_codensity_matrix(lpc).map(|w| acc + w)
454                })?;
455            // i = μ, j = μ'
456            einsum("ij,ji->", &[o1, &w.view()])
457                .map_err(|err| format_err!(err))?
458                .into_dimensionality::<Ix0>()?
459                .into_iter()
460                .next()
461                .ok_or_else(|| {
462                    format_err!("Unable to extract the result of the einsum contraction.")
463                })
464                .map(|v| v * reduced_ov)
465        } else {
466            ensure!(
467                nzeros == 1,
468                "Unexpected number of zero Löwdin overlaps: {nzeros} != 1."
469            );
470            let ps = (0..structure_constraint.implicit_factor()?)
471                .flat_map(|_| {
472                    lowdin_paired_coefficientss.iter().flat_map(|lpc| {
473                        lpc.zero_indices()
474                            .iter()
475                            .map(|mbar| calc_unweighted_codensity_matrix(lpc, *mbar))
476                    })
477                })
478                .collect::<Result<Vec<_>, _>>()?;
479            ensure!(
480                ps.len() == 1,
481                "Unexpected number of unweighted codensity matrices ({}) for one zero overlap.",
482                ps.len()
483            );
484            let p_mbar = ps.first().ok_or_else(|| {
485                format_err!("Unable to retrieve the computed unweighted codensity matrix.")
486            })?;
487
488            // i = μ, j = μ'
489            einsum("ij,ji->", &[o1, &p_mbar.view()])
490                .map_err(|err| format_err!(err))?
491                .into_dimensionality::<Ix0>()?
492                .into_iter()
493                .next()
494                .ok_or_else(|| {
495                    format_err!("Unable to extract the result of the einsum contraction.")
496                })
497                .map(|v| v * reduced_ov)
498        }
499    }
500}
501
502/// Calculates the matrix element of a two-particle operator between two Löwdin-paired
503/// determinants.
504///
505/// # Arguments
506///
507/// * `lowdin_paired_coefficientss` - A sequence of pairs of Löwdin-paired coefficients, one for each
508///   subspace determined by the specified structure constraint.
509/// * `o2_opt` - The two-particle operator in the atomic-orbital basis in physicists' ordering.
510/// * `get_jk_opt` - The optional function callable on a density matrix $`\mathbf{D}`$ to calculate
511///   the corresponding $`\mathbf{J}`$ and $`\mathbf{K}`$ matrices by contracting with
512///   appropriate two-electron integrals calculated on-the-fly. Either this or `o2_opt` must be
513///   specified, but not both.
514/// * `structure_constraint` - The structure constraint governing the coefficients.
515///
516/// # Returns
517///
518/// The two-particle matrix element.
519pub fn calc_o2_matrix_element<'a, 'b, T, SC, F>(
520    lowdin_paired_coefficientss: &[LowdinPairedCoefficients<T>],
521    o2_opt: Option<&'b ArrayView4<'a, T>>,
522    get_jk_opt: Option<&F>,
523    structure_constraint: &SC,
524) -> Result<T, anyhow::Error>
525where
526    'a: 'b,
527    T: ComplexFloat + ScalarOperand + Product + std::fmt::Display,
528    SC: StructureConstraint,
529    F: Fn(&Array2<T>) -> Result<(Array2<T>, Array2<T>), anyhow::Error>,
530{
531    let nzeros_explicit: usize = lowdin_paired_coefficientss
532        .iter()
533        .map(|lpc| lpc.n_lowdin_zeros())
534        .sum();
535    let nzeros = nzeros_explicit * structure_constraint.implicit_factor()?;
536    if nzeros > 2 {
537        Ok(T::zero())
538    } else {
539        let reduced_ov_explicit: T = lowdin_paired_coefficientss
540            .iter()
541            .map(|lpc| lpc.reduced_overlap())
542            .product();
543        let reduced_ov = (0..structure_constraint.implicit_factor()?)
544            .fold(T::one(), |acc, _| acc * reduced_ov_explicit);
545
546        if nzeros == 0 {
547            let nbasis = lowdin_paired_coefficientss[0].nbasis();
548            let w_sigmas = (0..structure_constraint.implicit_factor()?)
549                .cartesian_product(lowdin_paired_coefficientss.iter())
550                .map(|(_, lpc)| calc_weighted_codensity_matrix(lpc))
551                .collect::<Result<Vec<_>, _>>()?;
552            let w = w_sigmas
553                .iter()
554                .fold(Array2::<T>::zeros((nbasis, nbasis)), |acc, w_sigma| {
555                    acc + w_sigma
556                });
557
558            match (o2_opt, get_jk_opt) {
559                (Some(o2), None) => {
560                    // i = μ, j = μ', k = ν, l = ν'
561                    let j_term = einsum("ikjl,ji,lk->", &[o2, &w.view(), &w.view()])
562                        .map_err(|err| format_err!(err))?
563                        .into_dimensionality::<Ix0>()?
564                        .into_iter()
565                        .next()
566                        .ok_or_else(|| {
567                            format_err!("Unable to extract the result of the einsum contraction.")
568                        })
569                        .map(|v| v * reduced_ov / (T::one() + T::one()))?;
570                    let k_term = w_sigmas
571                        .iter()
572                        .try_fold(T::zero(), |acc, w_sigma| {
573                            einsum("ikjl,li,jk->", &[o2, &w_sigma.view(), &w_sigma.view()])
574                                .map_err(|err| format_err!(err))?
575                                .into_dimensionality::<Ix0>()?
576                                .into_iter()
577                                .next()
578                                .ok_or_else(|| {
579                                    format_err!(
580                                        "Unable to extract the result of the einsum contraction."
581                                    )
582                                })
583                                .map(|v| acc + v)
584                        })
585                        .map(|v| v * reduced_ov / (T::one() + T::one()))?;
586                    Ok(j_term - k_term)
587                }
588                (None, Some(get_jk)) => {
589                    // i = μ, j = μ', k = ν, l = ν'
590                    let (j_w, _) = get_jk(&w)?;
591                    let j_term = einsum("ij,ji->", &[&j_w.view(), &w.view()])
592                        .map_err(|err| format_err!(err))?
593                        .into_dimensionality::<Ix0>()?
594                        .into_iter()
595                        .next()
596                        .ok_or_else(|| {
597                            format_err!("Unable to extract the result of the einsum contraction.")
598                        })
599                        .map(|v| v * reduced_ov / (T::one() + T::one()))?;
600                    let k_term = w_sigmas
601                        .iter()
602                        .try_fold(T::zero(), |acc, w_sigma| {
603                            let (_, k_w_sigma) = get_jk(w_sigma)?;
604                            einsum("il,li->", &[&k_w_sigma.view(), &w_sigma.view()])
605                                .map_err(|err| format_err!(err))?
606                                .into_dimensionality::<Ix0>()?
607                                .into_iter()
608                                .next()
609                                .ok_or_else(|| {
610                                    format_err!(
611                                        "Unable to extract the result of the einsum contraction."
612                                    )
613                                })
614                                .map(|v| acc + v)
615                        })
616                        .map(|v| v * reduced_ov / (T::one() + T::one()))?;
617                    Ok(j_term - k_term)
618                }
619                _ => Err(format_err!(
620                    "One and only one of `o2_opt` or `get_jk_opt` should be provided."
621                )),
622            }
623        } else if nzeros == 1 {
624            ensure!(
625                nzeros_explicit == 1,
626                "Unexpected number of explicit zero Löwdin overlaps: {nzeros_explicit} != 1."
627            );
628
629            let nbasis = lowdin_paired_coefficientss[0].nbasis();
630            let w = (0..structure_constraint.implicit_factor()?)
631                .cartesian_product(lowdin_paired_coefficientss.iter())
632                .try_fold(Array2::<T>::zeros((nbasis, nbasis)), |acc, (_, lpc)| {
633                    calc_weighted_codensity_matrix(lpc).map(|w_sigma| acc + w_sigma)
634                })?;
635
636            lowdin_paired_coefficientss
637                .iter()
638                .filter_map(|lpc| {
639                    if lpc.n_lowdin_zeros() == 1 {
640                        let w_sigma_res = calc_weighted_codensity_matrix(lpc);
641                        let mbar = lpc.zero_indices()[0];
642                        let p_mbar_sigma_res = calc_unweighted_codensity_matrix(lpc, mbar);
643                        Some((w_sigma_res, p_mbar_sigma_res))
644                    } else {
645                        None
646                    }
647                })
648                .try_fold(T::zero(), |acc, (w_sigma_res, p_mbar_sigma_res)| {
649                    w_sigma_res.and_then(|w_sigma| {
650                        p_mbar_sigma_res.and_then(|p_mbar_sigma| {
651                            match (o2_opt, get_jk_opt) {
652                                (Some(o2), None) => {
653                                    // i = μ, j = μ', k = ν, l = ν'
654                                    let j_term_1 = einsum(
655                                        "ikjl,ji,lk->",
656                                        &[o2, &w.view(), &p_mbar_sigma.view()],
657                                    )
658                                    .map_err(|err| format_err!(err))?
659                                    .into_dimensionality::<Ix0>()?
660                                    .into_iter()
661                                    .next()
662                                    .ok_or_else(|| {
663                                        format_err!(
664                                            "Unable to extract the result of the einsum contraction."
665                                        )
666                                    })?;
667                                    let j_term_2 = einsum(
668                                        "ikjl,ji,lk->",
669                                        &[o2, &p_mbar_sigma.view(), &w.view()],
670                                    )
671                                    .map_err(|err| format_err!(err))?
672                                    .into_dimensionality::<Ix0>()?
673                                    .into_iter()
674                                    .next()
675                                    .ok_or_else(|| {
676                                        format_err!(
677                                            "Unable to extract the result of the einsum contraction."
678                                        )
679                                    })?;
680                                    let k_term_1 = einsum(
681                                        "ikjl,li,jk->",
682                                        &[o2, &w_sigma.view(), &p_mbar_sigma.view()],
683                                    )
684                                    .map_err(|err| format_err!(err))?
685                                    .into_dimensionality::<Ix0>()?
686                                    .into_iter()
687                                    .next()
688                                    .ok_or_else(|| {
689                                        format_err!(
690                                            "Unable to extract the result of the einsum contraction."
691                                        )
692                                    })?;
693                                    let k_term_2 = einsum(
694                                        "ikjl,li,jk->",
695                                        &[o2, &p_mbar_sigma.view(), &w_sigma.view()],
696                                    )
697                                    .map_err(|err| format_err!(err))?
698                                    .into_dimensionality::<Ix0>()?
699                                    .into_iter()
700                                    .next()
701                                    .ok_or_else(|| {
702                                        format_err!(
703                                            "Unable to extract the result of the einsum contraction."
704                                        )
705                                    })?;
706                                    Ok(acc + j_term_1 + j_term_2 - k_term_1 - k_term_2)
707                                },
708                                (None, Some(get_jk)) => {
709                                    // i = μ, j = μ', k = ν, l = ν'
710                                    let (j_p_mbar_sigma, k_p_mbar_sigma) = get_jk(&p_mbar_sigma)?;
711                                    let j_term_1 = einsum(
712                                        "ij,ji->",
713                                        &[&j_p_mbar_sigma.view(), &w.view()],
714                                    )
715                                    .map_err(|err| format_err!(err))?
716                                    .into_dimensionality::<Ix0>()?
717                                    .into_iter()
718                                    .next()
719                                    .ok_or_else(|| {
720                                        format_err!(
721                                            "Unable to extract the result of the einsum contraction."
722                                        )
723                                    })?;
724                                    let (j_w, _) = get_jk(&w)?;
725                                    let j_term_2 = einsum(
726                                        "ij,ji->",
727                                        &[&j_w.view(), &p_mbar_sigma.view()],
728                                    )
729                                    .map_err(|err| format_err!(err))?
730                                    .into_dimensionality::<Ix0>()?
731                                    .into_iter()
732                                    .next()
733                                    .ok_or_else(|| {
734                                        format_err!(
735                                            "Unable to extract the result of the einsum contraction."
736                                        )
737                                    })?;
738                                    let k_term_1 = einsum(
739                                        "il,li->",
740                                        &[&k_p_mbar_sigma.view(), &w_sigma.view()],
741                                    )
742                                    .map_err(|err| format_err!(err))?
743                                    .into_dimensionality::<Ix0>()?
744                                    .into_iter()
745                                    .next()
746                                    .ok_or_else(|| {
747                                        format_err!(
748                                            "Unable to extract the result of the einsum contraction."
749                                        )
750                                    })?;
751                                    let (_, k_w_sigma) = get_jk(&w_sigma)?;
752                                    let k_term_2 = einsum(
753                                        "il,li->",
754                                        &[&k_w_sigma.view(), &p_mbar_sigma.view()],
755                                    )
756                                    .map_err(|err| format_err!(err))?
757                                    .into_dimensionality::<Ix0>()?
758                                    .into_iter()
759                                    .next()
760                                    .ok_or_else(|| {
761                                        format_err!(
762                                            "Unable to extract the result of the einsum contraction."
763                                        )
764                                    })?;
765                                    Ok(acc + j_term_1 + j_term_2 - k_term_1 - k_term_2)
766                                }
767                                _ => Err(format_err!(
768                                    "One and only one of `o2` or `get_jk` should be provided."
769                                )),
770                            }
771                        })
772                    })
773                })
774                .map(|v| v * reduced_ov / (T::one() + T::one()))
775        } else {
776            ensure!(
777                nzeros == 2,
778                "Unexpected number of zero Löwdin overlaps: {nzeros} != 2."
779            );
780
781            let ps = (0..structure_constraint.implicit_factor()?)
782                .flat_map(|_| {
783                    lowdin_paired_coefficientss.iter().flat_map(|lpc| {
784                        lpc.zero_indices()
785                            .iter()
786                            .map(|mbar| calc_unweighted_codensity_matrix(lpc, *mbar))
787                    })
788                })
789                .collect::<Result<Vec<_>, _>>()?;
790            ensure!(
791                ps.len() == 2,
792                "Unexpected number of unweighted codensity matrices ({}) for two zero overlaps.",
793                ps.len()
794            );
795            let p_mbar = ps.first().ok_or_else(|| {
796                format_err!("Unable to retrieve the first computed unweighted codensity matrix.")
797            })?;
798            let p_nbar = ps.last().ok_or_else(|| {
799                format_err!("Unable to retrieve the second computed unweighted codensity matrix.")
800            })?;
801
802            match (o2_opt, get_jk_opt) {
803                (Some(o2), None) => {
804                    // i = μ, j = μ', k = ν, l = ν'
805                    let j_term_1 = einsum("ikjl,ji,lk->", &[o2, &p_mbar.view(), &p_nbar.view()])
806                        .map_err(|err| format_err!(err))?
807                        .into_dimensionality::<Ix0>()?
808                        .into_iter()
809                        .next()
810                        .ok_or_else(|| {
811                            format_err!("Unable to extract the result of the einsum contraction.")
812                        })?;
813                    let j_term_2 = einsum("ikjl,ji,lk->", &[o2, &p_nbar.view(), &p_mbar.view()])
814                        .map_err(|err| format_err!(err))?
815                        .into_dimensionality::<Ix0>()?
816                        .into_iter()
817                        .next()
818                        .ok_or_else(|| {
819                            format_err!("Unable to extract the result of the einsum contraction.")
820                        })?;
821
822                    let (k_term_1, k_term_2) = if lowdin_paired_coefficientss
823                        .iter()
824                        .any(|lpc| lpc.n_lowdin_zeros() == 2)
825                    {
826                        let k_term_1 =
827                            einsum("ikjl,li,jk->", &[o2, &p_mbar.view(), &p_nbar.view()])
828                                .map_err(|err| format_err!(err))?
829                                .into_dimensionality::<Ix0>()?
830                                .into_iter()
831                                .next()
832                                .ok_or_else(|| {
833                                    format_err!(
834                                        "Unable to extract the result of the einsum contraction."
835                                    )
836                                })?;
837                        let k_term_2 =
838                            einsum("ikjl,li,jk->", &[o2, &p_nbar.view(), &p_mbar.view()])
839                                .map_err(|err| format_err!(err))?
840                                .into_dimensionality::<Ix0>()?
841                                .into_iter()
842                                .next()
843                                .ok_or_else(|| {
844                                    format_err!(
845                                        "Unable to extract the result of the einsum contraction."
846                                    )
847                                })?;
848                        (k_term_1, k_term_2)
849                    } else {
850                        (T::zero(), T::zero())
851                    };
852                    Ok(reduced_ov * (j_term_1 - k_term_1 + j_term_2 - k_term_2)
853                        / (T::one() + T::one()))
854                }
855                (None, Some(get_jk)) => {
856                    // i = μ, j = μ', k = ν, l = ν'
857                    let (j_p_nbar, k_p_nbar) = get_jk(p_nbar)?;
858                    let j_term_1 = einsum("ij,ji->", &[&j_p_nbar.view(), &p_mbar.view()])
859                        .map_err(|err| format_err!(err))?
860                        .into_dimensionality::<Ix0>()?
861                        .into_iter()
862                        .next()
863                        .ok_or_else(|| {
864                            format_err!("Unable to extract the result of the einsum contraction.")
865                        })?;
866                    let (j_p_mbar, k_p_mbar) = get_jk(p_mbar)?;
867                    let j_term_2 = einsum("ij,ji->", &[&j_p_mbar.view(), &p_nbar.view()])
868                        .map_err(|err| format_err!(err))?
869                        .into_dimensionality::<Ix0>()?
870                        .into_iter()
871                        .next()
872                        .ok_or_else(|| {
873                            format_err!("Unable to extract the result of the einsum contraction.")
874                        })?;
875
876                    let (k_term_1, k_term_2) = if lowdin_paired_coefficientss
877                        .iter()
878                        .any(|lpc| lpc.n_lowdin_zeros() == 2)
879                    {
880                        let k_term_1 = einsum("il,li->", &[&k_p_nbar.view(), &p_mbar.view()])
881                            .map_err(|err| format_err!(err))?
882                            .into_dimensionality::<Ix0>()?
883                            .into_iter()
884                            .next()
885                            .ok_or_else(|| {
886                                format_err!(
887                                    "Unable to extract the result of the einsum contraction."
888                                )
889                            })?;
890                        let k_term_2 = einsum("il,li->", &[&k_p_mbar.view(), &p_nbar.view()])
891                            .map_err(|err| format_err!(err))?
892                            .into_dimensionality::<Ix0>()?
893                            .into_iter()
894                            .next()
895                            .ok_or_else(|| {
896                                format_err!(
897                                    "Unable to extract the result of the einsum contraction."
898                                )
899                            })?;
900                        (k_term_1, k_term_2)
901                    } else {
902                        (T::zero(), T::zero())
903                    };
904                    Ok(reduced_ov * (j_term_1 - k_term_1 + j_term_2 - k_term_2)
905                        / (T::one() + T::one()))
906                }
907                _ => Err(format_err!(
908                    "One and only one of `o2_opt` or `get_jk_opt` should be provided."
909                )),
910            }
911        }
912    }
913}
914
915/// Calculates the transition density matrix between two Löwdin-paired determinants.
916///
917/// # Arguments
918///
919/// * `lowdin_paired_coefficientss` - A sequence of pairs of Löwdin-paired coefficients, one for each
920///   subspace determined by the specified structure constraint.
921/// * `structure_constraint` - The structure constraint governing the coefficients.
922///
923/// # Returns
924///
925/// The one-particle matrix element.
926pub fn calc_transition_density_matrix<T, SC>(
927    lowdin_paired_coefficientss: &[LowdinPairedCoefficients<T>],
928    structure_constraint: &SC,
929) -> Result<Array2<T>, anyhow::Error>
930where
931    T: ComplexFloat + ScalarOperand + Product,
932    SC: StructureConstraint,
933{
934    let nzeros_explicit: usize = lowdin_paired_coefficientss
935        .iter()
936        .map(|lpc| lpc.n_lowdin_zeros())
937        .sum();
938    let nzeros = nzeros_explicit * structure_constraint.implicit_factor()?;
939    let nbasis = lowdin_paired_coefficientss[0].nbasis();
940    if nzeros > 1 {
941        Ok(Array2::<T>::zeros((nbasis, nbasis)))
942    } else {
943        let reduced_ov_explicit: T = lowdin_paired_coefficientss
944            .iter()
945            .map(|lpc| lpc.reduced_overlap())
946            .product();
947        let reduced_ov = (0..structure_constraint.implicit_factor()?)
948            .fold(T::one(), |acc, _| acc * reduced_ov_explicit);
949
950        if nzeros == 0 {
951            let nbasis = lowdin_paired_coefficientss[0].nbasis();
952            let w = (0..structure_constraint.implicit_factor()?)
953                .cartesian_product(lowdin_paired_coefficientss.iter())
954                .try_fold(Array2::<T>::zeros((nbasis, nbasis)), |acc, (_, lpc)| {
955                    calc_weighted_codensity_matrix(lpc).map(|w| acc + w)
956                })?;
957            Ok(w.mapv(|v| v * reduced_ov))
958        } else {
959            ensure!(
960                nzeros == 1,
961                "Unexpected number of zero Löwdin overlaps: {nzeros} != 1."
962            );
963            let ps = (0..structure_constraint.implicit_factor()?)
964                .flat_map(|_| {
965                    lowdin_paired_coefficientss.iter().flat_map(|lpc| {
966                        lpc.zero_indices()
967                            .iter()
968                            .map(|mbar| calc_unweighted_codensity_matrix(lpc, *mbar))
969                    })
970                })
971                .collect::<Result<Vec<_>, _>>()?;
972            ensure!(
973                ps.len() == 1,
974                "Unexpected number of unweighted codensity matrices ({}) for one zero overlap.",
975                ps.len()
976            );
977            let p_mbar = ps.first().ok_or_else(|| {
978                format_err!("Unable to retrieve the computed unweighted codensity matrix.")
979            })?;
980            Ok(p_mbar.mapv(|v| v * reduced_ov))
981        }
982    }
983}
984
985/// Performs modified Gram--Schmidt orthonormalisation on a set of column vectors in a matrix with
986/// respect to the complex-symmetric or Hermitian dot product.
987///
988/// # Arguments
989///
990/// * `vmat` - Matrix containing column vectors forming a basis for a subspace.
991/// * `complex_symmetric` - A boolean indicating if the vector dot product is complex-symmetric. If
992///   `false`, the conventional Hermitian dot product is used.
993/// * `thresh` - A threshold for determining self-orthogonal vectors.
994///
995/// # Returns
996///
997/// The orthonormal vectors forming a basis for the same subspace collected as column vectors in a
998/// matrix.
999///
1000/// # Errors
1001///
1002/// Errors when the orthonormalisation procedure fails, which occurs when there is linear dependency
1003/// between the basis vectors and/or when self-orthogonal vectors are encountered.
1004pub fn complex_modified_gram_schmidt<T>(
1005    vmat: &ArrayView2<T>,
1006    complex_symmetric: bool,
1007    thresh: <T as ComplexFloat>::Real,
1008) -> Result<Array2<T>, anyhow::Error>
1009where
1010    T: ComplexFloat + std::fmt::Display + 'static,
1011{
1012    let mut us: Vec<Array1<T>> = Vec::with_capacity(vmat.shape()[1]);
1013    let mut us_sq_norm: Vec<T> = Vec::with_capacity(vmat.shape()[1]);
1014    for (i, vi) in vmat.columns().into_iter().enumerate() {
1015        // u[i] now initialised with v[i]
1016        us.push(vi.to_owned());
1017
1018        // Project ui onto all uj (0 <= j < i)
1019        // This is the 'modified' part of Gram--Schmidt. We project the current (and being updated)
1020        // ui onto uj, rather than projecting vi onto uj. This enhances numerical stability.
1021        for j in 0..i {
1022            let p_uj_ui = if complex_symmetric {
1023                us[j].t().dot(&us[i]) / us_sq_norm[j]
1024            } else {
1025                us[j].t().map(|x| x.conj()).dot(&us[i]) / us_sq_norm[j]
1026            };
1027            us[i] = &us[i] - us[j].map(|&x| x * p_uj_ui);
1028        }
1029
1030        // Evaluate the squared norm of ui which will no longer be changed after this iteration.
1031        // us_sq_norm[i] now available.
1032        let us_sq_norm_i = if complex_symmetric {
1033            us[i].t().dot(&us[i])
1034        } else {
1035            us[i].t().map(|x| x.conj()).dot(&us[i])
1036        };
1037        if us_sq_norm_i.abs() < thresh {
1038            return Err(format_err!("A zero-norm vector found: {}", us[i]));
1039        }
1040        us_sq_norm.push(us_sq_norm_i);
1041    }
1042
1043    // Normalise ui
1044    for i in 0..us.len() {
1045        us[i].mapv_inplace(|x| x / us_sq_norm[i].sqrt());
1046    }
1047
1048    let ortho_check = us.iter().enumerate().all(|(i, ui)| {
1049        us.iter().enumerate().all(|(j, uj)| {
1050            let ov_ij = if complex_symmetric {
1051                ui.dot(uj)
1052            } else {
1053                ui.map(|x| x.conj()).dot(uj)
1054            };
1055            i == j || ov_ij.abs() < thresh
1056        })
1057    });
1058
1059    if ortho_check {
1060        stack(Axis(1), &us.iter().map(|u| u.view()).collect_vec()).map_err(|err| format_err!(err))
1061    } else {
1062        Err(format_err!(
1063            "Post-Gram--Schmidt orthogonality check failed."
1064        ))
1065    }
1066}
1067
1068/// Trait for Löwdin canonical orthogonalisation of a square matrix.
1069pub trait CanonicalOrthogonalisable {
1070    /// Numerical type of the matrix elements.
1071    type NumType;
1072
1073    /// Type of real threshold values.
1074    type RealType;
1075
1076    /// Calculates the Löwdin canonical orthogonalisation matrix $`\mathbf{X}`$ for a square
1077    /// matrix.
1078    ///
1079    /// # Arguments
1080    ///
1081    /// * `complex_symmetric` - Boolean indicating if the orthogonalisation is with respect to the
1082    ///   complex-symmetric inner product.
1083    /// * `preserves_full_rank` - Boolean indicating if a full-rank square matrix should be left
1084    ///   unchanged, thus forcing $`\mathbf{X} = \mathbf{I}`$.
1085    /// * `thresh_offdiag` - Threshold for verifying that the orthogonalised matrix is indeed
1086    ///   orthogonal.
1087    /// * `thresh_zeroov` - Threshold for determining zero eigenvalues of the input square matrix.
1088    /// * `eigenvalue_comparison_mode` - Enumerated type indicating the mode of comparing and
1089    ///   determining zero eigenvalues.
1090    ///
1091    /// # Returns
1092    ///
1093    /// The canonical orthogonalisation result.
1094    fn calc_canonical_orthogonal_matrix(
1095        &self,
1096        complex_symmetric: bool,
1097        preserves_full_rank: bool,
1098        thresh_offdiag: Self::RealType,
1099        thresh_zeroov: Self::RealType,
1100        eigenvalue_comparion_mode: &EigenvalueComparisonMode,
1101    ) -> Result<CanonicalOrthogonalisationResult<Self::NumType>, anyhow::Error>;
1102}
1103
1104/// Structure containing the results of the Löwdin canonical orthogonalisation.
1105pub struct CanonicalOrthogonalisationResult<T> {
1106    /// The eigenvalues of the input matrix.
1107    eigenvalues: Array1<T>,
1108
1109    /// The Löwdin canonical orthogonalisation matrix $`\mathbf{X}`$.
1110    xmat: Array2<T>,
1111
1112    /// The conjugate of the Löwdin canonical orthogonalisation matrix,
1113    /// $`\mathbf{X}^{\dagger\lozenge}`$, where $`\lozenge = \star`$ for complex-symmetric matrices
1114    /// and $`\lozenge = \hat{e}`$ otherwise.
1115    xmat_d: Array2<T>,
1116}
1117
1118impl<T> CanonicalOrthogonalisationResult<T> {
1119    /// Returns the eigenvalues of the input matrix.
1120    pub fn eigenvalues(&'_ self) -> ArrayView1<'_, T> {
1121        self.eigenvalues.view()
1122    }
1123
1124    /// Returns the Löwdin canonical orthogonalisation matrix $`\mathbf{X}`$.
1125    pub fn xmat(&'_ self) -> ArrayView2<'_, T> {
1126        self.xmat.view()
1127    }
1128
1129    /// Returns the conjugate of the Löwdin canonical orthogonalisation matrix,
1130    /// $`\mathbf{X}^{\dagger\lozenge}`$, where $`\lozenge = \star`$ for complex-symmetric matrices
1131    /// and $`\lozenge = \hat{e}`$ otherwise.
1132    pub fn xmat_d(&'_ self) -> ArrayView2<'_, T> {
1133        self.xmat_d.view()
1134    }
1135}
1136
1137#[duplicate_item(
1138    [
1139        dtype_ [ f64 ]
1140    ]
1141    [
1142        dtype_ [ f32 ]
1143    ]
1144)]
1145impl CanonicalOrthogonalisable for ArrayView2<'_, dtype_> {
1146    type NumType = dtype_;
1147
1148    type RealType = dtype_;
1149
1150    fn calc_canonical_orthogonal_matrix(
1151        &self,
1152        _: bool,
1153        preserves_full_rank: bool,
1154        thresh_offdiag: dtype_,
1155        thresh_zeroov: dtype_,
1156        eigenvalue_comparison_mode: &EigenvalueComparisonMode,
1157    ) -> Result<CanonicalOrthogonalisationResult<Self::NumType>, anyhow::Error> {
1158        let smat = self;
1159
1160        // Real, symmetric S
1161        // let max_offdiag_s = *(smat.to_owned() - smat.t()).map(|v| v.abs()).iter()
1162        //         .max_by(|a, b| a.total_cmp(b))
1163        //         .ok_or_else(|| format_err!("Unable to find the maximum absolute value of the overlap symmetric deviation matrix."))?;
1164        // ensure!(
1165        //     max_offdiag_s <= thresh_offdiag,
1166        //     "Overlap matrix is not real-symmetric: ||S - S^T||_∞ = {max_offdiag_s:.3e} > {thresh_offdiag:.3e}."
1167        // );
1168        check_real_matrix_symmetry(
1169            &smat.view(),
1170            thresh_offdiag,
1171            "Real-valued overlap matrix",
1172            "S(ℝ)",
1173        )?;
1174
1175        // S is real-symmetric, so U is orthogonal, i.e. U^T = U^(-1).
1176        let (s_eig, umat) = smat.eigh(UPLO::Lower).map_err(|err| format_err!(err))?;
1177        let mut sorted_s_eig = s_eig.iter().collect_vec();
1178        sorted_s_eig.sort_by(|a, b| match eigenvalue_comparison_mode {
1179            EigenvalueComparisonMode::Modulus => a.abs().total_cmp(&b.abs()),
1180            EigenvalueComparisonMode::Real => a.total_cmp(b),
1181        });
1182        sorted_s_eig.reverse();
1183        log::debug!("Overlap eigenvalues for canonical orthogonalisation (real-symmetric matrix):");
1184        for (i, eig) in s_eig.iter().enumerate() {
1185            log::debug!("  {i}: {eig:+.8e}");
1186        }
1187        log::debug!("");
1188
1189        // Real eigenvalues, but the two comparison modes still differ, because `Real` gives signed
1190        // comparison while `Modulus` does not.
1191        let nonzero_s_indices = s_eig
1192            .iter()
1193            .positions(|x| match eigenvalue_comparison_mode {
1194                EigenvalueComparisonMode::Modulus => x.abs() > thresh_zeroov,
1195                EigenvalueComparisonMode::Real => *x > thresh_zeroov,
1196            })
1197            .collect_vec();
1198        log::debug!(
1199            "Non-zero overlap indices w.r.t. threshold {thresh_zeroov:.8e} ({eigenvalue_comparison_mode}):"
1200        );
1201        log::debug!(
1202            "  {}",
1203            nonzero_s_indices.iter().map(|i| i.to_string()).join(", ")
1204        );
1205
1206        let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
1207        if nonzero_s_eig.iter().any(|v| *v < 0.0) {
1208            return Err(format_err!(
1209                "The matrix has negative eigenvalues and therefore cannot be orthogonalised over the reals."
1210            ));
1211        }
1212        let nonzero_umat = umat.select(Axis(1), &nonzero_s_indices);
1213        let nullity = smat.shape()[0] - nonzero_s_indices.len();
1214        let (xmat, xmat_d) = if nullity == 0 && preserves_full_rank {
1215            (Array2::eye(smat.shape()[0]), Array2::eye(smat.shape()[0]))
1216        } else {
1217            let s_s = Array2::<dtype_>::from_diag(&nonzero_s_eig.mapv(|x| 1.0 / x.sqrt()));
1218            (nonzero_umat.dot(&s_s), s_s.dot(&nonzero_umat.t()))
1219        };
1220        let res = CanonicalOrthogonalisationResult {
1221            eigenvalues: s_eig,
1222            xmat,
1223            xmat_d,
1224        };
1225        Ok(res)
1226    }
1227}
1228
1229impl<T> CanonicalOrthogonalisable for ArrayView2<'_, Complex<T>>
1230where
1231    T: Float + Scalar<Complex = Complex<T>>,
1232    Complex<T>: ComplexFloat<Real = T> + Scalar<Real = T, Complex = Complex<T>> + Lapack,
1233{
1234    type NumType = Complex<T>;
1235
1236    type RealType = T;
1237
1238    fn calc_canonical_orthogonal_matrix(
1239        &self,
1240        complex_symmetric: bool,
1241        preserves_full_rank: bool,
1242        thresh_offdiag: T,
1243        thresh_zeroov: T,
1244        eigenvalue_comparison_mode: &EigenvalueComparisonMode,
1245    ) -> Result<CanonicalOrthogonalisationResult<Self::NumType>, anyhow::Error> {
1246        let smat = self;
1247
1248        check_complex_matrix_symmetry(
1249            &smat.view(),
1250            complex_symmetric,
1251            thresh_offdiag,
1252            "Complex-valued overlap matrix",
1253            "S(ℂ)",
1254        )?;
1255
1256        let (s_eig, umat_nonortho) = smat.eig().map_err(|err| format_err!(err))?;
1257        let mut sorted_s_eig = s_eig.iter().collect_vec();
1258        sorted_s_eig.sort_by(|a, b| match eigenvalue_comparison_mode {
1259            EigenvalueComparisonMode::Modulus => ComplexFloat::abs(**a)
1260                .partial_cmp(&ComplexFloat::abs(**b))
1261                .expect("Unable to compare the moduli of two complex numbers."),
1262            EigenvalueComparisonMode::Real => ComplexFloat::re(**a)
1263                .partial_cmp(&ComplexFloat::re(**b))
1264                .expect("Unable to compare the real parts of two complex numbers."),
1265        });
1266        sorted_s_eig.reverse();
1267        log::debug!(
1268            "Overlap eigenvalues for canonical orthogonalisation ({} matrix):",
1269            if complex_symmetric {
1270                "complex-symmetric"
1271            } else {
1272                "complex-Hermitian"
1273            }
1274        );
1275        for (i, eig) in sorted_s_eig.iter().enumerate() {
1276            log::debug!("  {i}: {eig:+.8e}");
1277        }
1278        log::debug!("");
1279
1280        let nonzero_s_indices = s_eig
1281            .iter()
1282            .positions(|x| match eigenvalue_comparison_mode {
1283                EigenvalueComparisonMode::Modulus => ComplexFloat::abs(*x) > thresh_zeroov,
1284                EigenvalueComparisonMode::Real => ComplexFloat::re(*x) > thresh_zeroov,
1285            })
1286            .collect_vec();
1287        log::debug!(
1288            "Non-zero overlap indices w.r.t. threshold {thresh_zeroov:.8e} ({eigenvalue_comparison_mode}):"
1289        );
1290        log::debug!(
1291            "  {}",
1292            nonzero_s_indices.iter().map(|i| i.to_string()).join(", ")
1293        );
1294        log::debug!("");
1295
1296        let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
1297        let nonzero_umat_nonortho = umat_nonortho.select(Axis(1), &nonzero_s_indices);
1298
1299        // `eig` does not guarantee orthogonality of `nonzero_umat_nonortho`.
1300        // Gram--Schmidt is therefore required.
1301        let nonzero_umat = complex_modified_gram_schmidt(
1302            &nonzero_umat_nonortho.view(),
1303            complex_symmetric,
1304            thresh_zeroov,
1305        )
1306        .map_err(
1307            |_| format_err!("Unable to orthonormalise the linearly-independent eigenvectors of the overlap matrix.")
1308        )?;
1309
1310        let nonzero_s_eig_from_u = if complex_symmetric {
1311            nonzero_umat.t().dot(smat).dot(&nonzero_umat)
1312        } else {
1313            nonzero_umat
1314                .map(|v| v.conj())
1315                .t()
1316                .dot(smat)
1317                .dot(&nonzero_umat)
1318        };
1319
1320        let max_offdiag_s = *(nonzero_s_eig_from_u - Array2::from_diag(&nonzero_s_eig)).mapv(ComplexFloat::abs).iter()
1321                .max_by(|a, b| a.partial_cmp(b).unwrap_or_else(|| panic!("Unable to compare {a} and {b}.")))
1322                .ok_or_else(|| format_err!("Unable to find the maximum absolute value of the overlap symmetric deviation matrix."))?;
1323        ensure!(
1324            max_offdiag_s <= thresh_offdiag,
1325            if complex_symmetric {
1326                "Canonical orthogonalisation has failed: ||U^T.S.U - s||_∞ = {max_offdiag_s:.3e} > {thresh_offdiag:.3e}."
1327            } else {
1328                "Canonical orthogonalisation has failed: ||U^†.S.U - s||_∞ = {max_offdiag_s:.3e} > {thresh_offdiag:.3e}."
1329            }
1330        );
1331
1332        let nullity = smat.shape()[0] - nonzero_s_indices.len();
1333        let (xmat, xmat_d) = if nullity == 0 && preserves_full_rank {
1334            (
1335                Array2::<Complex<T>>::eye(smat.shape()[0]),
1336                Array2::<Complex<T>>::eye(smat.shape()[0]),
1337            )
1338        } else {
1339            let s_s = Array2::<Complex<T>>::from_diag(
1340                &nonzero_s_eig.mapv(|x| Complex::<T>::from(T::one()) / x.sqrt()),
1341            );
1342            let xmat = nonzero_umat.dot(&s_s);
1343            let xmat_d = if complex_symmetric {
1344                // (nonzero_umat.dot(&s_s), s_s.dot(&nonzero_umat.t()))
1345                xmat.t().to_owned()
1346            } else {
1347                xmat.map(|v| v.conj()).t().to_owned()
1348            };
1349            (xmat, xmat_d)
1350        };
1351        let res = CanonicalOrthogonalisationResult {
1352            eigenvalues: s_eig,
1353            xmat,
1354            xmat_d,
1355        };
1356        Ok(res)
1357    }
1358}