Skip to main content

qsym2/auxiliary/
misc.rs

1//! Miscellaneous items.
2
3use std::collections::hash_map::DefaultHasher;
4use std::error::Error;
5use std::fmt::{self, LowerExp};
6use std::hash::{Hash, Hasher};
7
8use anyhow::{ensure, format_err};
9use itertools::{Itertools, MultiProduct};
10use log;
11use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis, Ix2, LinalgScalar, stack};
12use ndarray_einsum::einsum;
13use num::{Float, One};
14use num_complex::ComplexFloat;
15use num_traits::float::TotalOrder;
16
17use crate::analysis::EigenvalueComparisonMode;
18use crate::io::format::qsym2_warn;
19
20/// Trait to enable floating point numbers to be hashed.
21pub trait HashableFloat {
22    /// Returns a float rounded after being multiplied by a factor.
23    ///
24    /// Let $`x`$ be a float, $`k`$ a factor, and $`[\cdot]`$ denote the
25    /// rounding-to-integer operation. This function yields $`[x \times k] / k`$.
26    ///
27    /// Arguments
28    ///
29    /// * `threshold` - The inverse $`k^{-1}`$ of the factor $`k`$ used in the
30    ///   rounding of the float.
31    ///
32    /// Returns
33    ///
34    /// The rounded float.
35    #[must_use]
36    fn round_factor(self, threshold: Self) -> Self;
37
38    /// Returns the mantissa-exponent-sign triplet for a float.
39    ///
40    /// Reference: <https://stackoverflow.com/questions/39638363/how-can-i-use-a-hashmap-with-f64-as-key-in-rust>
41    ///
42    /// # Arguments
43    ///
44    /// * `val` - A floating point number.
45    ///
46    /// # Returns
47    ///
48    /// The corresponding mantissa-exponent-sign triplet.
49    fn integer_decode(self) -> (u64, i16, i8);
50}
51
52impl HashableFloat for f64 {
53    fn round_factor(self, factor: f64) -> Self {
54        (self / factor).round() * factor + 0.0
55    }
56
57    fn integer_decode(self) -> (u64, i16, i8) {
58        let bits: u64 = self.to_bits();
59        let sign: i8 = if bits >> 63 == 0 { 1 } else { -1 };
60        let mut exponent: i16 = ((bits >> 52) & 0x7ff) as i16;
61        let mantissa = if exponent == 0 {
62            (bits & 0x000f_ffff_ffff_ffff) << 1
63        } else {
64            (bits & 0x000f_ffff_ffff_ffff) | 0x0010_0000_0000_0000
65        };
66
67        exponent -= 1023 + 52;
68        (mantissa, exponent, sign)
69    }
70}
71
72/// Returns the hash value of a hashable struct.
73///
74/// Arguments
75///
76/// * `t` - A struct of a hashable type.
77///
78/// Returns
79///
80/// The hash value.
81pub fn calculate_hash<T: Hash>(t: &T) -> u64 {
82    let mut s = DefaultHasher::new();
83    t.hash(&mut s);
84    s.finish()
85}
86
87/// Trait for performing repeated products of iterators.
88pub trait ProductRepeat: Iterator + Clone
89where
90    Self::Item: Clone,
91{
92    /// Rust implementation of Python's `itertools.product()` with repetition.
93    ///
94    /// From <https://stackoverflow.com/a/68231315>.
95    ///
96    /// # Arguments
97    ///
98    /// * `repeat` - Number of repetitions of the given iterator.
99    ///
100    /// # Returns
101    ///
102    /// A [`MultiProduct`] iterator.
103    fn product_repeat(self, repeat: usize) -> MultiProduct<Self> {
104        std::iter::repeat_n(self, repeat).multi_cartesian_product()
105    }
106}
107
108impl<T: Iterator + Clone> ProductRepeat for T where T::Item: Clone {}
109
110// =============
111// Gram--Schmidt
112// =============
113
114/// Error during Gram--Schmidt orthogonalisation.
115#[derive(Debug, Clone)]
116pub struct GramSchmidtError<'a, T> {
117    pub mat: Option<&'a Array2<T>>,
118    pub vecs: Option<&'a [Array1<T>]>,
119}
120
121impl<'a, T: fmt::Display + fmt::Debug> fmt::Display for GramSchmidtError<'a, T> {
122    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
123        writeln!(f, "Unable to perform Gram--Schmidt orthogonalisation on:",)?;
124        if let Some(mat) = self.mat {
125            writeln!(f, "{mat}")?;
126        } else if let Some(vecs) = self.vecs {
127            for vec in vecs {
128                writeln!(f, "{vec}")?;
129            }
130        } else {
131            writeln!(f, "Unspecified basis vectors for Gram--Schmidt.")?;
132        }
133        Ok(())
134    }
135}
136
137impl<'a, T: fmt::Display + fmt::Debug> Error for GramSchmidtError<'a, T> {}
138
139/// Performs modified Gram--Schmidt orthonormalisation on a set of column vectors in a matrix with
140/// respect to the complex-symmetric or Hermitian dot product.
141///
142/// # Arguments
143///
144/// * `vmat` - Matrix containing column vectors forming a basis for a subspace.
145/// * `complex_symmetric` - A boolean indicating if the vector dot product is complex-symmetric. If
146///   `false`, the conventional Hermitian dot product is used.
147/// * `thresh` - A threshold for determining self-orthogonal vectors.
148///
149/// # Returns
150///
151/// The orthonormal vectors forming a basis for the same subspace collected as column vectors in a
152/// matrix.
153///
154/// # Errors
155///
156/// Errors when the orthonormalisation procedure fails, which occurs when there is linear dependency
157/// between the basis vectors and/or when self-orthogonal vectors are encountered.
158pub fn complex_modified_gram_schmidt<T>(
159    vmat: &'_ Array2<T>,
160    complex_symmetric: bool,
161    thresh: T::Real,
162) -> Result<Array2<T>, GramSchmidtError<'_, T>>
163where
164    T: ComplexFloat + fmt::Display + 'static,
165{
166    let mut us: Vec<Array1<T>> = Vec::with_capacity(vmat.shape()[1]);
167    let mut us_sq_norm: Vec<T> = Vec::with_capacity(vmat.shape()[1]);
168    for (i, vi) in vmat.columns().into_iter().enumerate() {
169        // u[i] now initialised with v[i]
170        us.push(vi.to_owned());
171
172        // Project ui onto all uj (0 <= j < i)
173        // This is the 'modified' part of Gram--Schmidt. We project the current (and being updated)
174        // ui onto uj, rather than projecting vi onto uj. This enhances numerical stability.
175        for j in 0..i {
176            let p_uj_ui = if complex_symmetric {
177                us[j].t().dot(&us[i]) / us_sq_norm[j]
178            } else {
179                us[j].t().map(|x| x.conj()).dot(&us[i]) / us_sq_norm[j]
180            };
181            us[i] = &us[i] - us[j].map(|&x| x * p_uj_ui);
182        }
183
184        // Evaluate the squared norm of ui which will no longer be changed after this iteration.
185        // us_sq_norm[i] now available.
186        let us_sq_norm_i = if complex_symmetric {
187            us[i].t().dot(&us[i])
188        } else {
189            us[i].t().map(|x| x.conj()).dot(&us[i])
190        };
191        if us_sq_norm_i.abs() < thresh {
192            log::error!("A zero-norm vector found: {}", us[i]);
193            return Err(GramSchmidtError {
194                mat: Some(vmat),
195                vecs: None,
196            });
197        }
198        us_sq_norm.push(us_sq_norm_i);
199    }
200
201    // Normalise ui
202    for i in 0..us.len() {
203        us[i].mapv_inplace(|x| x / us_sq_norm[i].sqrt());
204    }
205
206    let ortho_check = us.iter().enumerate().all(|(i, ui)| {
207        us.iter().enumerate().all(|(j, uj)| {
208            let ov_ij = if complex_symmetric {
209                ui.dot(uj)
210            } else {
211                ui.map(|x| x.conj()).dot(uj)
212            };
213            i == j || ov_ij.abs() < thresh
214        })
215    });
216
217    if ortho_check {
218        stack(Axis(1), &us.iter().map(|u| u.view()).collect_vec()).map_err(|err| {
219            log::error!("{}", err);
220            GramSchmidtError {
221                mat: Some(vmat),
222                vecs: None,
223            }
224        })
225    } else {
226        Err(GramSchmidtError {
227            mat: Some(vmat),
228            vecs: None,
229        })
230    }
231}
232
233// ====================================
234// Linear-algebraic auxiliary functions
235// ====================================
236
237/// Sorts the eigenvalues and the corresponding eigenvectors.
238///
239/// # Arguments
240///
241/// * `eigvals` - The eigenvalues.
242/// * `eigvecs` - The corresponding eigenvectors.
243/// * `eigenvalue_comparison_mode` - Eigenvalue comparison mode.
244///
245/// # Returns
246///
247/// A tuple containing thw sorted eigenvalues and eigenvectors.
248pub(crate) fn sort_eigenvalues_eigenvectors<T: ComplexFloat>(
249    eigvals: &ArrayView1<T>,
250    eigvecs: &ArrayView2<T>,
251    eigenvalue_comparison_mode: &EigenvalueComparisonMode,
252) -> (Array1<T>, Array2<T>) {
253    let mut indices = (0..eigvals.len()).collect_vec();
254    match eigenvalue_comparison_mode {
255        EigenvalueComparisonMode::Modulus => {
256            indices.sort_by(|i, j| {
257                ComplexFloat::abs(eigvals[*i])
258                    .partial_cmp(&ComplexFloat::abs(eigvals[*j]))
259                    .unwrap()
260            });
261        }
262        EigenvalueComparisonMode::Real => {
263            indices.sort_by(|i, j| eigvals[*i].re().partial_cmp(&eigvals[*j].re()).unwrap());
264        }
265    }
266    let eigvals_sorted = eigvals.select(Axis(0), &indices);
267    let eigvecs_sorted = eigvecs.select(Axis(1), &indices);
268    (eigvals_sorted, eigvecs_sorted)
269}
270
271/// Regularises the eigenvectors such that the first entry of each of them has a positive real
272/// part, or a positive imaginary part if the real part is zero.
273///
274/// # Arguments
275///
276/// * `eigvecs` - The eigenvectors to be regularised.
277/// * `thresh` - Threshold for determining if a real number is zero.
278///
279/// # Returns
280///
281/// The regularised eigenvectors.
282pub(crate) fn regularise_eigenvectors<T>(eigvecs: &ArrayView2<T>, thresh: T::Real) -> Array2<T>
283where
284    T: ComplexFloat + One,
285    T::Real: Float,
286{
287    let eigvecs_sgn = stack!(
288        Axis(0),
289        eigvecs
290            .row(0)
291            .map(|v| {
292                if Float::abs(ComplexFloat::re(*v)) > thresh {
293                    T::from(v.re().signum()).expect("Unable to convert a signum to the right type.")
294                } else if Float::abs(ComplexFloat::im(*v)) > thresh {
295                    T::from(v.im().signum()).expect("Unable to convert a signum to the right type.")
296                } else {
297                    T::one()
298                }
299            })
300            .view()
301    );
302    eigvecs * eigvecs_sgn
303}
304
305/// Normalises the real eigenvectors with respect to a metric.
306///
307/// # Arguments
308///
309/// * `eigvecs` - The eigenvectors to be normalised.
310/// * `smat` - The metric.
311/// * `thresh` - Threshold for verifying the orthogonality of the eigenvectors.
312///
313/// # Returns
314///
315/// The normalised eigenvectors.
316pub(crate) fn normalise_eigenvectors_real<T>(
317    eigvecs: &ArrayView2<T>,
318    smat: &ArrayView2<T>,
319    thresh: T,
320) -> Result<Array2<T>, anyhow::Error>
321where
322    T: LinalgScalar + Float + std::fmt::LowerExp,
323{
324    let sq_norm = einsum("ji,jk,kl->il", &[eigvecs, smat, eigvecs])
325        .map_err(|err| format_err!(err))?
326        .into_dimensionality::<Ix2>()
327        .map_err(|err| format_err!(err))?;
328    let max_diff = (&sq_norm - &Array2::from_diag(&sq_norm.diag()))
329        .iter()
330        .map(|x| x.abs())
331        .max_by(|x, y| {
332            x.partial_cmp(y)
333                .expect("Unable to compare two `abs` values.")
334        })
335        .ok_or_else(|| {
336            format_err!(
337                "Unable to determine the maximum off-diagonal element of the C^T.S.C matrix."
338            )
339        })?;
340
341    ensure!(
342        max_diff <= thresh,
343        "The C^T.S.C matrix is not a diagonal matrix: the maximum absolute value of the off-diagonal elements is {max_diff:.3e} > {thresh:.3e}."
344    );
345    ensure!(
346        sq_norm.diag().iter().all(|v| *v > T::zero()),
347        "Some eigenvectors have negative squared norms and cannot be normalised over the reals."
348    );
349    let eigvecs_normalised = eigvecs / sq_norm.diag().map(|v| v.sqrt());
350    Ok(eigvecs_normalised)
351}
352
353/// Normalises the complex eigenvectors with respect to a metric.
354///
355/// # Arguments
356///
357/// * `eigvecs` - The eigenvectors to be normalised.
358/// * `smat` - The metric.
359/// * `complex_symmetric` - Boolean indicating if the inner product is complex-symmetric or not.
360/// * `thresh` - Optioanl threshold for verifying the orthogonality of the eigenvectors. If `None`,
361///   orthogonality will not be verified.
362///
363/// # Returns
364///
365/// The normalised eigenvectors.
366pub(crate) fn normalise_eigenvectors_complex<T>(
367    eigvecs: &ArrayView2<T>,
368    smat: &ArrayView2<T>,
369    complex_symmetric: bool,
370    thresh: Option<T::Real>,
371) -> Result<Array2<T>, anyhow::Error>
372where
373    T: LinalgScalar + ComplexFloat + std::fmt::Display + std::fmt::LowerExp,
374    T::Real: Float + std::fmt::LowerExp,
375{
376    let sq_norm = if complex_symmetric {
377        einsum("ji,jk,kl->il", &[eigvecs, smat, eigvecs])
378            .map_err(|err| format_err!(err))?
379            .into_dimensionality::<Ix2>()
380            .map_err(|err| format_err!(err))?
381    } else {
382        einsum(
383            "ji,jk,kl->il",
384            &[&eigvecs.map(|v| v.conj()).view(), smat, eigvecs],
385        )
386        .map_err(|err| format_err!(err))?
387        .into_dimensionality::<Ix2>()
388        .map_err(|err| format_err!(err))?
389    };
390
391    if let Some(thr) = thresh {
392        let max_diff = (&sq_norm - &Array2::from_diag(&sq_norm.diag()))
393            .iter()
394            .map(|x| ComplexFloat::abs(*x))
395            .max_by(|x, y| {
396                x.partial_cmp(y)
397                    .expect("Unable to compare two `abs` values.")
398            })
399            .ok_or_else(|| {
400                if complex_symmetric {
401                    format_err!(
402                        "Unable to determine the maximum off-diagonal element of the C^T.S.C matrix."
403                    )
404                } else {
405                    format_err!(
406                        "Unable to determine the maximum off-diagonal element of the C^†.S.C matrix."
407                    )
408                }
409            })?;
410
411        if complex_symmetric {
412            log::debug!("C^T.S.C:\n  {sq_norm:+.8e}");
413            ensure!(
414                max_diff <= thr,
415                "The C^T.S.C matrix is not a diagonal matrix: the maximum absolute value of the off-diagonal elements is {max_diff:.3e} > {thr:.3e}."
416            )
417        } else {
418            log::debug!("C^†.S.C:\n  {sq_norm:+.8e}");
419            ensure!(
420                max_diff <= thr,
421                "The C^†.S.C matrix is not a diagonal matrix: the maximum absolute value of the off-diagonal elements is {max_diff:.3e} > {thr:.3e}."
422            )
423        };
424    }
425    let eigvecs_normalised = eigvecs / sq_norm.diag().map(|v| v.sqrt());
426    Ok(eigvecs_normalised)
427}
428
429/// Checks for complex-symmetric or complex-Hermitian symmetry of a complex square matrix.
430///
431/// # Arguments
432///
433/// * `mat` - The complex square matrix to be checked.
434/// * `complex_symmetric` - Boolean indicating if complex-symmetry is to be checked instead of
435///   complex-Hermiticity.
436/// * `thresh_offdiag` - Threshold for checking.
437/// * `matname` - Name of the matrix.
438/// * `matsymbol` - Symbol of the matrix.
439pub(crate) fn check_complex_matrix_symmetry<T>(
440    mat: &ArrayView2<T>,
441    complex_symmetric: bool,
442    thresh_offdiag: <T as ComplexFloat>::Real,
443    matname: &str,
444    matsymbol: &str,
445) -> Result<(), anyhow::Error>
446where
447    T: LinalgScalar + ComplexFloat + std::fmt::Display + std::fmt::LowerExp,
448    <T as ComplexFloat>::Real: Float + std::fmt::LowerExp + std::fmt::Display,
449{
450    if complex_symmetric {
451        let deviation = mat.to_owned() - mat.t();
452        let (pos, &max_offdiag) = deviation
453            .mapv(ComplexFloat::abs)
454            .iter()
455            .enumerate()
456            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or_else(|| panic!("Unable to compare {a} and {b}.")))
457            .ok_or_else(|| format_err!("Unable to find the maximum absolute value of the {matname} complex-symmetric deviation matrix."))?;
458        let (pos_i, pos_j) = (pos.div_euclid(mat.ncols()), pos.rem_euclid(mat.ncols()));
459        log::debug!("{matname} matrix:\n  {mat:+.3e}");
460        log::debug!("{matname} matrix complex-symmetric deviation:\n  {deviation:+.3e}",);
461        qsym2_warn!("{matname} matrix complex-symmetric deviation:\n  {deviation:+.3e}",);
462        ensure!(
463            max_offdiag <= thresh_offdiag,
464            "The {matname} matrix is not complex-symmetric: ||{matsymbol} - ({matsymbol})^T||_∞ = {max_offdiag:.3e} > {thresh_offdiag:.3e} at ({pos_i}, {pos_j})."
465        );
466    } else {
467        let deviation = mat.to_owned() - mat.map(|v| v.conj()).t();
468        let (pos, &max_offdiag) = deviation
469                .mapv(ComplexFloat::abs)
470                .iter()
471                .enumerate()
472                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or_else(|| panic!("Unable to compare {a} and {b}.")))
473                .ok_or_else(|| format_err!("Unable to find the maximum absolute value of the {matname} complex-Hermitian deviation matrix."))?;
474        let (pos_i, pos_j) = (pos.div_euclid(mat.ncols()), pos.rem_euclid(mat.ncols()));
475        log::debug!("{matname} matrix:\n  {mat:+.3e}");
476        log::debug!("{matname} matrix complex-Hermitian deviation:\n  {deviation:+.3e}",);
477        qsym2_warn!("{matname} matrix complex-Hermitian deviation:\n  {deviation:+.3e}",);
478        ensure!(
479            max_offdiag <= thresh_offdiag,
480            "The {matname} matrix is not complex-Hermitian: ||{matsymbol} - ({matsymbol})^†||_∞ = {max_offdiag:.3e} > {thresh_offdiag:.3e} at ({pos_i}, {pos_j})."
481        );
482    }
483    Ok(())
484}
485
486/// Checks for real-symmetric symmetry of a complex square matrix.
487///
488/// # Arguments
489///
490/// * `mat` - The real square matrix to be checked.
491/// * `thresh_offdiag` - Threshold for checking.
492/// * `matname` - Name of the matrix.
493/// * `matsymbol` - Symbol of the matrix.
494pub(crate) fn check_real_matrix_symmetry<T>(
495    mat: &ArrayView2<T>,
496    thresh_offdiag: T,
497    matname: &str,
498    matsymbol: &str,
499) -> Result<(), anyhow::Error>
500where
501    T: LowerExp + Clone + LinalgScalar + Float + TotalOrder,
502{
503    let deviation = mat.to_owned() - mat.t();
504    let (pos, &max_offdiag) = deviation
505        .map(|v| v.abs())
506        .iter()
507        .enumerate()
508        .max_by(|(_, a), (_, b)| a.total_cmp(b))
509        .ok_or_else(|| format_err!("Unable to find the maximum absolute value of the {matname} real-symmetric deviation matrix."))?;
510    log::debug!("{matname} matrix:\n  {mat:+.3e}");
511    log::debug!("{matname} matrix real-symmetric deviation:\n  {deviation:+.3e}",);
512    qsym2_warn!("{matname} matrix real-symmetric deviation:\n  {deviation:+.3e}",);
513    let (pos_i, pos_j) = (pos.div_euclid(mat.ncols()), pos.rem_euclid(mat.ncols()));
514    ensure!(
515        max_offdiag <= thresh_offdiag,
516        "{matname} matrix is not real-symmetric: ||{matsymbol} - ({matsymbol})^T||_∞ = {max_offdiag:.3e} > {thresh_offdiag:.3e} at ({pos_i}, {pos_j})."
517    );
518    Ok(())
519}