qsym2/analysis/
mod.rs

1//! Symmetry analysis via representation and corepresentation theories.
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::ops::Mul;
6
7use anyhow::{self, Context, format_err};
8use duplicate::duplicate_item;
9use itertools::Itertools;
10use log;
11use ndarray::{Array, Array1, Array2, Axis, Dimension, Ix0, Ix2, s};
12use ndarray_einsum::*;
13use ndarray_linalg::{solve::Inverse, types::Lapack};
14use num_complex::{Complex, ComplexFloat};
15use num_traits::{ToPrimitive, Zero};
16#[cfg(feature = "python")]
17use pyo3::prelude::*;
18use rayon::prelude::*;
19use serde::{Deserialize, Serialize};
20
21use crate::chartab::chartab_group::CharacterProperties;
22use crate::chartab::{CharacterTable, DecompositionError, SubspaceDecomposable};
23use crate::group::{GroupProperties, class::ClassProperties};
24use crate::io::format::{log_subtitle, qsym2_output};
25use crate::symmetry::symmetry_group::UnitaryRepresentedSymmetryGroup;
26
27// ======
28// Metric
29// ======
30
31/// Structure to handle various choices for the specification of the metric matrices.
32#[derive(Clone)]
33pub(crate) struct Metric<'a, T, D: Dimension> {
34    /// The  Hermitian atomic-orbital overlap matrix of the underlying basis set used to describe
35    /// the determinant. For restricted, unrestricted, and generalised spin constraints, only the
36    /// spatial overlap matrix is required for the full overlap matrix to be deduced. For
37    /// relativistic SOC spin constraint, the full overlap matrix must be specified.
38    hermitian: &'a Array<T, D>,
39
40    /// The full (w.r.t. the pertinent spin constraint) complex-symmetric atomic-orbital overlap
41    /// matrix of the underlying basis set used to describe the determinant. For restricted,
42    /// unrestricted, and generalised spin constraints, only the spatial overlap matrix is required
43    /// for the full overlap matrix to be deduced. For relativistic SOC spin constraint, the full
44    /// overlap matrix must be specified.
45    complex_symmetric: Option<&'a Array<T, D>>,
46}
47
48// =======
49// Overlap
50// =======
51
52// ----------------
53// Trait definition
54// ----------------
55
56/// Trait for computing the inner product
57/// $`\langle \hat{\iota} \mathbf{v}_i, \mathbf{v}_j \rangle`$ between two linear-space quantities
58/// $`\mathbf{v}_i`$ and $`\mathbf{v}_j`$. The involutory operator $`\hat{\iota}`$ determines
59/// whether the inner product is a sesquilinear form or a bilinear form.
60pub trait Overlap<T, D>
61where
62    T: ComplexFloat + fmt::Debug + Lapack,
63    D: Dimension,
64{
65    /// If `true`, the inner product is bilinear and $`\hat{\iota} = \hat{\kappa}`$. If `false`,
66    /// the inner product is sesquilinear and $`\hat{\iota} = \mathrm{id}`$.
67    fn complex_symmetric(&self) -> bool;
68
69    /// Returns the overlap between `self` and `other`, with respect to a metric `metric` (and
70    /// possibly its complex-symmetric version `metric_h`) of the underlying basis in which `self`
71    /// and `other` are expressed.
72    fn overlap(
73        &self,
74        other: &Self,
75        metric: Option<&Array<T, D>>,
76        metric_h: Option<&Array<T, D>>,
77    ) -> Result<T, anyhow::Error>;
78
79    /// Returns a string describing the mathematical definition of the overlap (*i.e.* the inner
80    /// product) between two quantities of this type.
81    fn overlap_definition(&self) -> String;
82}
83
84// =====
85// Orbit
86// =====
87
88// --------------------------------------
89// Struct definitions and implementations
90// --------------------------------------
91
92/// Lazy iterator for orbits generated by the action of a group on an origin.
93pub struct OrbitIterator<'a, G, I>
94where
95    G: GroupProperties,
96{
97    /// A mutable iterator over the elements of the group. Each element will be applied on the
98    /// origin to yield a corresponding item in the orbit.
99    group_iter: <<G as GroupProperties>::ElementCollection as IntoIterator>::IntoIter,
100
101    /// The origin of the orbit.
102    origin: &'a I,
103
104    /// A function defining the action of each group element on the origin.
105    action: fn(&G::GroupElement, &I) -> Result<I, anyhow::Error>,
106}
107
108impl<'a, G, I> OrbitIterator<'a, G, I>
109where
110    G: GroupProperties,
111{
112    /// Creates and returns a new orbit iterator.
113    ///
114    /// # Arguments
115    ///
116    /// * `group` - A group.
117    /// * `origin` - An origin.
118    /// * `action` - A function or closure defining the action of each group element on the origin.
119    ///
120    /// # Returns
121    ///
122    /// An orbit iterator.
123    pub fn new(
124        group: &G,
125        origin: &'a I,
126        action: fn(&G::GroupElement, &I) -> Result<I, anyhow::Error>,
127    ) -> Self {
128        Self {
129            group_iter: group.elements().clone().into_iter(),
130            origin,
131            action,
132        }
133    }
134}
135
136impl<'a, G, I> Iterator for OrbitIterator<'a, G, I>
137where
138    G: GroupProperties,
139{
140    type Item = Result<I, anyhow::Error>;
141
142    fn next(&mut self) -> Option<Self::Item> {
143        self.group_iter
144            .next()
145            .map(|op| (self.action)(&op, self.origin))
146    }
147}
148
149// ----------------
150// Trait definition
151// ----------------
152
153/// Trait for orbits arising from group actions.
154pub trait Orbit<G, I>
155where
156    G: GroupProperties,
157{
158    /// Type of the iterator over items in the orbit.
159    type OrbitIter: Iterator<Item = Result<I, anyhow::Error>>;
160
161    /// The group generating the orbit.
162    fn group(&self) -> &G;
163
164    /// The origin of the orbit.
165    fn origin(&self) -> &I;
166
167    /// An iterator over items in the orbit arising from the action of the group on the origin.
168    fn iter(&self) -> Self::OrbitIter;
169}
170
171// ========
172// Analysis
173// ========
174
175// ---------------
176// Enum definition
177// ---------------
178
179/// Enumerated type specifying the comparison mode for filtering out orbit overlap
180/// eigenvalues.
181#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
182#[cfg_attr(feature = "python", pyclass(eq, eq_int))]
183pub enum EigenvalueComparisonMode {
184    /// Compares the eigenvalues using only their real parts.
185    Real,
186
187    /// Compares the eigenvalues using their moduli.
188    #[default]
189    Modulus,
190}
191
192impl fmt::Display for EigenvalueComparisonMode {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        match self {
195            EigenvalueComparisonMode::Real => write!(f, "Real part"),
196            EigenvalueComparisonMode::Modulus => write!(f, "Modulus"),
197        }
198    }
199}
200
201// ----------------
202// Trait definition
203// ----------------
204
205// ~~~~~~~~~~~
206// RepAnalysis
207// ~~~~~~~~~~~
208
209/// Trait for representation or corepresentation analysis on an orbit of items spanning a
210/// linear space.
211pub trait RepAnalysis<G, I, T, D>: Orbit<G, I>
212where
213    T: ComplexFloat + Lapack + fmt::Debug,
214    <T as ComplexFloat>::Real: ToPrimitive,
215    G: GroupProperties + ClassProperties + CharacterProperties,
216    G::GroupElement: fmt::Display,
217    G::CharTab: SubspaceDecomposable<T>,
218    D: Dimension,
219    I: Overlap<T, D> + Clone,
220    Self::OrbitIter: Iterator<Item = Result<I, anyhow::Error>>,
221{
222    // ----------------
223    // Required methods
224    // ----------------
225
226    /// Sets the overlap matrix between the items in the orbit.
227    ///
228    /// # Arguments
229    ///
230    /// * `smat` - The overlap matrix between the items in the orbit.
231    fn set_smat(&mut self, smat: Array2<T>);
232
233    /// Returns the overlap matrix between the items in the orbit.
234    #[must_use]
235    fn smat(&self) -> Option<&Array2<T>>;
236
237    /// Returns the transformation matrix $`\mathbf{X}`$ for the overlap matrix $`\mathbf{S}`$
238    /// between the items in the orbit.
239    ///
240    /// The matrix $`\mathbf{X}`$ serves to bring $`\mathbf{S}`$ to full rank, *i.e.*, the matrix
241    /// $`\tilde{\mathbf{S}}`$ defined by
242    ///
243    /// ```math
244    ///     \tilde{\mathbf{S}} = \mathbf{X}^{\dagger\lozenge} \mathbf{S} \mathbf{X}
245    /// ```
246    ///
247    /// is a full-rank matrix.
248    ///
249    /// If the overlap between items is complex-symmetric (see [`Overlap::complex_symmetric`]), then
250    /// $`\lozenge = *`$ is the complex-conjugation operation, otherwise, $`\lozenge`$ is the
251    /// identity.
252    ///
253    /// Depending on how $`\mathbf{X}`$ has been computed, $`\tilde{\mathbf{S}}`$ might also be
254    /// orthogonal. Either way, $`\tilde{\mathbf{S}}`$ is always guaranteed to be of full-rank.
255    #[must_use]
256    fn xmat(&self) -> &Array2<T>;
257
258    /// Returns the norm-preserving scalar map $`f`$ for every element of the generating group
259    /// defined by
260    ///
261    /// ```math
262    ///     \langle \hat{\iota} \mathbf{v}_w, \hat{g}_i \mathbf{v}_x \rangle
263    ///     = f \left( \langle \hat{\iota} \hat{g}_i^{-1} \mathbf{v}_w, \mathbf{v}_x \rangle \right).
264    /// ```
265    ///
266    /// Typically, for $`\hat{\iota} = \mathrm{id}`$, if $`\hat{g}_i`$ is unitary, then $`f`$ is the
267    /// identity, and if $`\hat{g}_i`$ is antiunitary, then $`f`$ is the complex-conjugation
268    /// operation. Either way, the norm of the inner product is preserved.
269    ///
270    /// If the overlap between items is complex-symmetric (*i.e.* $`\hat{\iota} = \hat{\kappa}`$,
271    /// see [`Overlap::complex_symmetric`]), then this map is currently unsupported because it is
272    /// currently unclear if the unitary and antiunitary symmetry operators in QSym² commute with
273    /// $`\hat{\kappa}`$. This thus precludes the use of the Cayley table to speed up the computation
274    /// of the orbit overlap matrix.
275    fn norm_preserving_scalar_map(&self, i: usize) -> Result<fn(T) -> T, anyhow::Error>;
276
277    /// Returns the threshold for integrality checks of irreducible representation or
278    /// corepresentation multiplicities.
279    #[must_use]
280    fn integrality_threshold(&self) -> <T as ComplexFloat>::Real;
281
282    /// Returns the enumerated type specifying the comparison mode for filtering out orbit overlap
283    /// eigenvalues.
284    #[must_use]
285    fn eigenvalue_comparison_mode(&self) -> &EigenvalueComparisonMode;
286
287    // ----------------
288    // Provided methods
289    // ----------------
290
291    /// Calculates and stores the overlap matrix between items in the orbit, with respect to a
292    /// metric of the basis in which these items are expressed.
293    ///
294    /// # Arguments
295    ///
296    /// * `metric` - The metric of the basis in which the orbit items are expressed.
297    /// * `metric_h` - The complex-symmetric metric of the basis in which the orbit items are
298    ///   expressed. This is required if antiunitary operations are involved.
299    /// * `use_cayley_table` - A boolean indicating if the Cayley table of the group should be used
300    ///   to speed up the computation of the overlap matrix.
301    fn calc_smat(
302        &mut self,
303        metric: Option<&Array<T, D>>,
304        metric_h: Option<&Array<T, D>>,
305        use_cayley_table: bool,
306    ) -> Result<&mut Self, anyhow::Error> {
307        let order = self.group().order();
308        let mut smat = Array2::<T>::zeros((order, order));
309        let item_0 = self.origin();
310        if let (Some(ctb), true) = (self.group().cayley_table(), use_cayley_table) {
311            log::debug!(
312                "Cayley table available. Group closure will be used to speed up overlap matrix computation."
313            );
314            let ovs = self
315                .iter()
316                .map(|item_res| {
317                    let item = item_res?;
318                    item.overlap(item_0, metric, metric_h)
319                })
320                .collect::<Result<Vec<_>, _>>()?;
321            for (i, j) in (0..order).cartesian_product(0..order) {
322                let jinv = ctb
323                    .slice(s![.., j])
324                    .iter()
325                    .position(|&x| x == 0)
326                    .ok_or(format_err!(
327                        "Unable to find the inverse of group element `{j}`."
328                    ))?;
329                let jinv_i = ctb[(jinv, i)];
330                smat[(i, j)] = self.norm_preserving_scalar_map(jinv)?(ovs[jinv_i]);
331            }
332        } else {
333            log::debug!(
334                "Cayley table not available or the use of Cayley table not requested. Overlap matrix will be constructed without group-closure speed-up."
335            );
336            for pair in self
337                .iter()
338                .map(|item_res| item_res.map_err(|err| err.to_string()))
339                .enumerate()
340                .combinations_with_replacement(2)
341            {
342                let (w, item_w_res) = &pair[0];
343                let (x, item_x_res) = &pair[1];
344                let item_w = item_w_res
345                    .as_ref()
346                    .map_err(|err| format_err!(err.clone()))
347                    .with_context(|| "One of the items in the orbit is not available")?;
348                let item_x = item_x_res
349                    .as_ref()
350                    .map_err(|err| format_err!(err.clone()))
351                    .with_context(|| "One of the items in the orbit is not available")?;
352                smat[(*w, *x)] = item_w.overlap(item_x, metric, metric_h).map_err(|err| {
353                    log::warn!("{err}");
354                    log::warn!(
355                        "Unable to calculate the overlap between items `{w}` and `{x}` in the orbit."
356                    );
357                    err
358                })?;
359                if *w != *x {
360                    smat[(*x, *w)] = item_x.overlap(item_w, metric, metric_h).map_err(|err| {
361                        log::warn!("{err}");
362                        log::warn!(
363                            "Unable to calculate the overlap between items `{x}` and `{w}` in the orbit."
364                        );
365                        err
366                    })?;
367                }
368            }
369        }
370        if self.origin().complex_symmetric() {
371            self.set_smat((smat.clone() + smat.t().to_owned()).mapv(|x| x / (T::one() + T::one())))
372        } else {
373            self.set_smat(
374                (smat.clone() + smat.t().to_owned().mapv(|x| x.conj()))
375                    .mapv(|x| x / (T::one() + T::one())),
376            )
377        }
378        Ok(self)
379    }
380
381    /// Normalises overlap matrix between items in the orbit such that its diagonal entries are
382    /// unity.
383    ///
384    /// # Errors
385    ///
386    /// Errors if no orbit overlap matrix can be found, of if linear-algebraic errors are
387    /// encountered.
388    fn normalise_smat(&mut self) -> Result<&mut Self, anyhow::Error> {
389        let smat = self
390            .smat()
391            .ok_or(format_err!("No orbit overlap matrix to normalise."))?;
392        let norm = smat.diag().mapv(|x| <T as ComplexFloat>::sqrt(x));
393        let nspatial = norm.len();
394        let norm_col = norm
395            .clone()
396            .into_shape_with_order([nspatial, 1])
397            .map_err(|err| format_err!(err))?;
398        let norm_row = norm
399            .into_shape_with_order([1, nspatial])
400            .map_err(|err| format_err!(err))?;
401        let norm_mat = norm_col.dot(&norm_row);
402        let normalised_smat = smat / norm_mat;
403        self.set_smat(normalised_smat);
404        Ok(self)
405    }
406
407    /// Computes the $`\mathbf{T}(g)`$ matrix for a particular element $`g`$ of the generating
408    /// group.
409    ///
410    /// The elements of this matrix are given by
411    ///
412    /// ```math
413    ///     T_{wx}(g)
414    ///         = \langle \hat{\iota} \hat{g}_w \mathbf{v}_0, \hat{g} \hat{g}_x \mathbf{v}_0 \rangle.
415    /// ```
416    ///
417    /// This means that $`\mathbf{T}(g)`$ is just the orbit overlap matrix $`\mathbf{S}`$ with its
418    /// columns permuted according to the way $`g`$ composites on the elements in the group from
419    /// the left.
420    ///
421    /// # Arguments
422    ///
423    /// * `op` - The element $`g`$ in the generating group.
424    ///
425    /// # Returns
426    ///
427    /// The matrix $`\mathbf{T}(g)`$.
428    fn calc_tmat(&self, op: &G::GroupElement) -> Result<Array2<T>, anyhow::Error> {
429        let ctb = self
430            .group()
431            .cayley_table()
432            .expect("The Cayley table for the group cannot be found.");
433        let i = self.group().get_index_of(op).unwrap_or_else(|| {
434            panic!("Unable to retrieve the index of element `{op}` in the group.")
435        });
436        let ix = ctb.slice(s![i, ..]).iter().cloned().collect::<Vec<_>>();
437        let twx = self
438            .smat()
439            .ok_or(format_err!("No orbit overlap matrix found."))?
440            .select(Axis(1), &ix);
441        Ok(twx)
442    }
443
444    /// Computes the representation or corepresentation matrix $`\mathbf{D}(g)`$ for a particular
445    /// element $`g`$ in the generating group in the basis of the orbit.
446    ///
447    /// The matrix $`\mathbf{D}(g)`$ is defined by
448    ///
449    /// ```math
450    ///     \hat{g} \mathcal{G} \cdot \mathbf{v}_0 = \mathcal{G} \cdot \mathbf{v}_0 \mathbf{D}(g),
451    /// ```
452    ///
453    /// where $`\mathcal{G} \cdot \mathbf{v}_0`$ is the orbit generated by the action of the group
454    /// $`\mathcal{G}`$ on the origin $`\mathbf{v}_0`$.
455    ///
456    /// # Arguments
457    ///
458    /// * `op` - The element $`g`$ of the generating group.
459    ///
460    /// # Returns
461    ///
462    /// The matrix $`\mathbf{D}(g)`$.
463    fn calc_dmat(&self, op: &G::GroupElement) -> Result<Array2<T>, anyhow::Error> {
464        let complex_symmetric = self.origin().complex_symmetric();
465        let xmath = if complex_symmetric {
466            self.xmat().t().to_owned()
467        } else {
468            self.xmat().t().mapv(|x| x.conj())
469        };
470        let smattilde = xmath
471            .dot(
472                self.smat()
473                    .ok_or(format_err!("No orbit overlap matrix found."))?,
474            )
475            .dot(self.xmat());
476        let smattilde_inv = smattilde
477            .inv()
478            .expect("The inverse of S~ could not be found.");
479        einsum(
480            "ij,jk,kl,lm->im",
481            &[&smattilde_inv, &xmath, &self.calc_tmat(op)?, self.xmat()],
482        )
483        .map_err(|err| format_err!(err))
484        .with_context(|| "Unable to compute the matrix product [(S~)^(-1) X† T X].")?
485        .into_dimensionality::<Ix2>()
486        .map_err(|err| format_err!(err))
487        .with_context(
488            || "Unable to convert the matrix product [(S~)^(-1) X† T X] to two dimensions.",
489        )
490    }
491
492    /// Computes the character of a particular element $`g`$ in the generating group in the basis
493    /// of the orbit.
494    ///
495    /// See [`Self::calc_dmat`] for more information.
496    ///
497    /// # Arguments
498    ///
499    /// * `op` - The element $`g`$ of the generating group.
500    ///
501    /// # Returns
502    ///
503    /// The character $`\chi(g)`$.
504    fn calc_character(&self, op: &G::GroupElement) -> Result<T, anyhow::Error> {
505        let complex_symmetric = self.origin().complex_symmetric();
506        let xmath = if complex_symmetric {
507            self.xmat().t().to_owned()
508        } else {
509            self.xmat().t().mapv(|x| x.conj())
510        };
511        let smattilde = xmath
512            .dot(
513                self.smat()
514                    .ok_or(format_err!("No orbit overlap matrix found."))?,
515            )
516            .dot(self.xmat());
517        let smattilde_inv = smattilde
518            .inv()
519            .expect("The inverse of S~ could not be found.");
520        let chi = einsum(
521            "ij,jk,kl,li",
522            &[&smattilde_inv, &xmath, &self.calc_tmat(op)?, self.xmat()],
523        )
524        .map_err(|err| format_err!(err))
525        .with_context(|| "Unable to compute the trace of the matrix product [(S~)^(-1) X† T X].")?
526        .into_dimensionality::<Ix0>()
527        .map_err(|err| format_err!(err))
528        .with_context(|| "Unable to convert the trace of the matrix product [(S~)^(-1) X† T X] to zero dimensions.")?;
529        chi.into_iter().next().ok_or(format_err!(
530            "Unable to extract the character from the representation matrix."
531        ))
532    }
533
534    /// Computes the characters of the elements in a conjugacy-class transversal of the generating
535    /// group in the basis of the orbit.
536    ///
537    /// See [`Self::calc_dmat`]  and [`Self::calc_character`] for more information.
538    ///
539    /// # Returns
540    ///
541    /// The conjugacy class symbols and the corresponding characters.
542    fn calc_characters(
543        &self,
544    ) -> Result<Vec<(<G as ClassProperties>::ClassSymbol, T)>, anyhow::Error> {
545        let complex_symmetric = self.origin().complex_symmetric();
546        let xmath = if complex_symmetric {
547            self.xmat().t().to_owned()
548        } else {
549            self.xmat().t().mapv(|x| x.conj())
550        };
551        let smattilde = xmath
552            .dot(
553                self.smat()
554                    .ok_or(format_err!("No orbit overlap matrix found."))?,
555            )
556            .dot(self.xmat());
557        let smattilde_inv = smattilde
558            .inv()
559            .expect("The inverse of S~ could not be found.");
560        (0..self.group().class_number()).map(|cc_i| {
561            let cc = self.group().get_cc_symbol_of_index(cc_i).unwrap();
562            let op = self.group().get_cc_transversal(cc_i).unwrap();
563            let chi = einsum(
564                "ij,jk,kl,li",
565                &[&smattilde_inv, &xmath, &self.calc_tmat(&op)?, self.xmat()],
566            )
567            .map_err(|err| format_err!(err))
568            .with_context(|| "Unable to compute the trace of the matrix product [(S~)^(-1) X† T X].")?
569            .into_dimensionality::<Ix0>()
570            .map_err(|err| format_err!(err))
571            .with_context(|| "Unable to convert the trace of the matrix product [(S~)^(-1) X† T X] to zero dimensions.")?;
572            let chi_val = chi.into_iter().next().ok_or(format_err!(
573                "Unable to extract the character from the representation matrix."
574            ))?;
575            Ok((cc, chi_val))
576        }).collect::<Result<Vec<_>, _>>()
577    }
578
579    /// Reduces the representation or corepresentation spanned by the items in the orbit to a
580    /// direct sum of the irreducible representations or corepresentations of the generating group.
581    ///
582    /// # Returns
583    ///
584    /// The decomposed result.
585    ///
586    /// # Errors
587    ///
588    /// Errors if the decomposition fails, *e.g.* because one or more calculated multiplicities
589    /// are non-integral.
590    fn analyse_rep(
591        &self,
592    ) -> Result<
593        <<G as CharacterProperties>::CharTab as SubspaceDecomposable<T>>::Decomposition,
594        DecompositionError,
595    > {
596        let chis = self
597            .calc_characters()
598            .map_err(|err| DecompositionError(err.to_string()))?;
599        self.group().character_table().reduce_characters(
600            &chis.iter().map(|(cc, chi)| (cc, *chi)).collect::<Vec<_>>(),
601            self.integrality_threshold(),
602        )
603    }
604
605    /// Converts a slice of tuples of class symbols and characters to a nicely formatted table.
606    ///
607    /// # Arguments
608    ///
609    /// * `chis` - A slice of tuples of class symbols and characters.
610    /// * `integrality_threshold` - Threshold for ascertaining the integrality of characters.
611    ///
612    /// # Returns
613    ///
614    /// A string of a nicely formatted table.
615    fn characters_to_string(
616        &self,
617        chis: &[(<G as ClassProperties>::ClassSymbol, T)],
618        integrality_threshold: <T as ComplexFloat>::Real,
619    ) -> String
620    where
621        T: ComplexFloat + Lapack + fmt::Debug,
622        <T as ComplexFloat>::Real: ToPrimitive,
623    {
624        let ndigits = (-integrality_threshold.log10()).to_usize().unwrap_or(6) + 1;
625        let (ccs, characters): (Vec<_>, Vec<_>) = chis
626            .iter()
627            .map(|(cc, chi)| (cc.to_string(), format!("{chi:+.ndigits$}")))
628            .unzip();
629        let cc_width = ccs
630            .iter()
631            .map(|cc| cc.chars().count())
632            .max()
633            .unwrap_or(5)
634            .max(5);
635        let characters_width = characters
636            .iter()
637            .map(|chi| chi.chars().count())
638            .max()
639            .unwrap_or(9)
640            .max(9);
641
642        let divider = "┈".repeat(cc_width + characters_width + 4);
643        let header = format!(" {:<cc_width$}  {:<}", "Class", "Character");
644        let body = Itertools::intersperse(
645            chis.iter()
646                .map(|(cc, chi)| format!(" {:<cc_width$}  {:<+.ndigits$}", cc.to_string(), chi)),
647            "\n".to_string(),
648        )
649        .collect::<String>();
650
651        Itertools::intersperse(
652            [divider.clone(), header, divider.clone(), body, divider].into_iter(),
653            "\n".to_string(),
654        )
655        .collect::<String>()
656    }
657}
658
659// ~~~~~~~~~~~~~~~~~~~~~~~
660// ProjectionDecomposition
661// ~~~~~~~~~~~~~~~~~~~~~~~
662
663/// Trait for decomposing the origin of an orbit into the irreducible components of the generating
664/// group using the projection operator.
665pub trait ProjectionDecomposition<G, I, T, D>: RepAnalysis<G, I, T, D>
666where
667    T: ComplexFloat + Lapack + fmt::Debug,
668    <T as ComplexFloat>::Real: ToPrimitive,
669    G: GroupProperties + ClassProperties + CharacterProperties + Sync + Send,
670    <G as CharacterProperties>::RowSymbol: Sync + Send,
671    G::GroupElement: fmt::Display,
672    G::CharTab: SubspaceDecomposable<T>,
673    D: Dimension,
674    I: Overlap<T, D> + Clone,
675    Self: Sync + Send,
676    Self::OrbitIter: Iterator<Item = Result<I, anyhow::Error>>,
677    for<'a> Complex<f64>: Mul<&'a T, Output = Complex<f64>>,
678{
679    /// Calculates the irreducible compositions of the origin $`\mathbf{v}`$ of the orbit using the
680    /// projection operator:
681    ///
682    /// ```math
683    /// p_{\Gamma} =
684    ///     \frac{\dim \Gamma}{\lvert \mathcal{G} \rvert}
685    ///     \sum_{i=1}^{\lvert \mathcal{G} \rvert}
686    ///         \chi_{\Gamma}^{*}(\hat{g}_i)
687    ///         \braket{ \mathbf{v} | \hat{g}_i \mathbf{v} }
688    /// ```
689    ///
690    /// At the moment, this only works for unitary irreducible representations because of the
691    /// presence of the characters $`\chi_{\Gamma}(\hat{g}_i)`$ which can only be tabulated for
692    /// unitary operations.
693    ///
694    /// # Returns
695    ///
696    /// A vector of tuples, each of which contains the irreducible row label and the corresponding
697    /// projection value.
698    #[allow(clippy::type_complexity)]
699    fn calc_projection_compositions(
700        &self,
701    ) -> Result<
702        Vec<(
703            <<G as CharacterProperties>::CharTab as CharacterTable>::RowSymbol,
704            Complex<f64>,
705        )>,
706        anyhow::Error,
707    > {
708        self.group()
709            .character_table()
710            .get_all_rows()
711            .iter()
712            .map(|row_label| {
713                let group = self.group();
714                let chartab = group.character_table();
715                let id_class = group
716                    .get_cc_symbol_of_index(0)
717                    .ok_or(format_err!("Unable to retrieve the identity class."))?;
718                let dim = chartab
719                    .get_character(row_label, &id_class).complex_value();
720                let group_order = group.order().to_f64().ok_or(
721                    DecompositionError("The group order cannot be converted to `f64`.".to_string())
722                )?;
723                let projection: Complex<f64> = (0..group.order())
724                    .into_par_iter()
725                    .try_fold(Complex::<f64>::zero, |acc, i| {
726                        let cc_i = group
727                            .get_cc_of_element_index(i)
728                            .ok_or(format_err!("Unable to retrieve the conjugacy class index of element index {i}."))?;
729                        let cc = group
730                            .get_cc_symbol_of_index(cc_i)
731                            .ok_or(format_err!("Unable to retrieve the conjugacy class symbol of conjugacy class index {cc_i}."))?;
732                        let chi_i_star = group.character_table().get_character(row_label, &cc).complex_conjugate();
733                        let s_0i = self
734                            .smat()
735                            .ok_or(format_err!("No orbit overlap matrix found."))?
736                            .get((0, i))
737                            .ok_or(format_err!("Unable to retrieve the overlap matrix element with index `(0, {i})`."))?;
738                        Ok::<_, anyhow::Error>(acc + chi_i_star.complex_value() * s_0i)
739                    })
740                    .try_reduce(Complex::<f64>::zero, |a, s| Ok(a + s))?;
741                #[allow(clippy::op_ref)]
742                Ok::<_, anyhow::Error>((row_label.clone(), &dim * projection / group_order))
743            }).collect::<Result<Vec<_>, anyhow::Error>>()
744    }
745
746    /// Converts a slice of tuples of irreducible row symbols and projection values to a nicely
747    /// formatted table.
748    ///
749    /// # Arguments
750    ///
751    /// * `projections` - A slice of tuples of irreducible row symbols and projection values.
752    /// * `integrality_threshold` - Threshold for ascertaining the integrality of projection values.
753    ///
754    /// # Returns
755    ///
756    /// A string of a nicely formatted table.
757    fn projections_to_string(
758        &self,
759        projections: &[(
760            <<G as CharacterProperties>::CharTab as CharacterTable>::RowSymbol,
761            Complex<f64>,
762        )],
763        integrality_threshold: <T as ComplexFloat>::Real,
764    ) -> String {
765        let ndigits = (-integrality_threshold.log10()).to_usize().unwrap_or(6) + 1;
766        let (row_labels, projection_values): (Vec<_>, Vec<_>) = projections
767            .iter()
768            .map(|(row, proj)| (row.to_string(), format!("{proj:+.ndigits$}")))
769            .unzip();
770        let row_width = row_labels
771            .iter()
772            .map(|row| row.chars().count())
773            .max()
774            .unwrap_or(9)
775            .max(9);
776        let proj_width = projection_values
777            .iter()
778            .map(|proj| proj.chars().count())
779            .max()
780            .unwrap_or(10)
781            .max(10);
782
783        let divider = "┈".repeat(row_width + proj_width + 4);
784        let header = format!(" {:<row_width$}  {:<}", "Component", "Projection");
785        let body = Itertools::intersperse(
786            projections.iter().map(|(row, proj)| {
787                format!(" {:<row_width$}  {:<+.ndigits$}", row.to_string(), proj)
788            }),
789            "\n".to_string(),
790        )
791        .collect::<String>();
792
793        Itertools::intersperse(
794            [divider.clone(), header, divider.clone(), body, divider].into_iter(),
795            "\n".to_string(),
796        )
797        .collect::<String>()
798    }
799}
800
801// Partial blanket implementation for orbits generated by unitary-represented symmetry groups over
802// real or complex numbers.
803#[duplicate_item(
804    duplicate!{
805        [ dtype_nested; [f64]; [Complex<f64>] ]
806        [
807            gtype_ [ UnitaryRepresentedSymmetryGroup ]
808            dtype_ [ dtype_nested ]
809        ]
810    }
811)]
812impl<O, I, D> ProjectionDecomposition<gtype_, I, dtype_, D> for O
813where
814    O: RepAnalysis<gtype_, I, dtype_, D>,
815    D: Dimension,
816    I: Overlap<dtype_, D> + Clone,
817    Self: Sync + Send,
818    Self::OrbitIter: Iterator<Item = Result<I, anyhow::Error>>,
819{
820}
821
822// =================
823// Macro definitions
824// =================
825
826macro_rules! fn_calc_xmat_real {
827    ( $(#[$meta:meta])* $vis:vis $func:ident ) => {
828        $(#[$meta])*
829        $vis fn $func(&mut self, preserves_full_rank: bool) -> Result<&mut Self, anyhow::Error> {
830            // Real, symmetric S
831            let thresh = self.linear_independence_threshold;
832            let smat = self
833                .smat
834                .as_ref()
835                .ok_or(format_err!("No overlap matrix found for this orbit."))?;
836            use ndarray_linalg::norm::Norm;
837            if (smat.to_owned() - smat.t()).norm_l2() > thresh {
838                Err(format_err!("Overlap matrix is not symmetric."))
839            } else {
840                let (s_eig, umat) = smat.eigh(UPLO::Lower).map_err(|err| format_err!(err))?;
841                let nonzero_s_indices = match self.eigenvalue_comparison_mode {
842                    EigenvalueComparisonMode::Modulus => {
843                        s_eig.iter().positions(|x| x.abs() > thresh).collect_vec()
844                    }
845                    EigenvalueComparisonMode::Real => {
846                        s_eig.iter().positions(|x| *x > thresh).collect_vec()
847                    }
848                };
849                let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
850                let nonzero_umat = umat.select(Axis(1), &nonzero_s_indices);
851                let nullity = smat.shape()[0] - nonzero_s_indices.len();
852                let xmat = if nullity == 0 && preserves_full_rank {
853                    Array2::eye(smat.shape()[0])
854                } else {
855                    let s_s = Array2::<f64>::from_diag(&nonzero_s_eig.mapv(|x| 1.0 / x.sqrt()));
856                    nonzero_umat.dot(&s_s)
857                };
858                self.smat_eigvals = Some(s_eig);
859                self.xmat = Some(xmat);
860                Ok(self)
861            }
862        }
863    }
864}
865
866macro_rules! fn_calc_xmat_complex {
867    ( $(#[$meta:meta])* $vis:vis $func:ident ) => {
868        $(#[$meta])*
869        $vis fn $func(&mut self, preserves_full_rank: bool) -> Result<&mut Self, anyhow::Error> {
870            // Complex S, symmetric or Hermitian
871            let thresh = self.linear_independence_threshold;
872            let smat = self
873                .smat
874                .as_ref()
875                .ok_or(format_err!("No overlap matrix found for this orbit."))?;
876            let (s_eig, umat_nonortho) = smat.eig().map_err(|err| format_err!(err))?;
877
878            let nonzero_s_indices = match self.eigenvalue_comparison_mode {
879                EigenvalueComparisonMode::Modulus => s_eig
880                    .iter()
881                    .positions(|x| ComplexFloat::abs(*x) > thresh)
882                    .collect_vec(),
883                EigenvalueComparisonMode::Real => {
884                    if s_eig
885                        .iter()
886                        .any(|x| Float::abs(ComplexFloat::im(*x)) > thresh)
887                    {
888                        log::warn!("Comparing eigenvalues using the real parts, but not all eigenvalues are real.");
889                    }
890                    s_eig
891                        .iter()
892                        .positions(|x| ComplexFloat::re(*x) > thresh)
893                        .collect_vec()
894                }
895            };
896            let nonzero_s_eig = s_eig.select(Axis(0), &nonzero_s_indices);
897            let nonzero_umat_nonortho = umat_nonortho.select(Axis(1), &nonzero_s_indices);
898
899            // `eig` does not guarantee orthogonality of `nonzero_umat_nonortho`.
900            // Gram--Schmidt is therefore required.
901            let nonzero_umat = complex_modified_gram_schmidt(
902                &nonzero_umat_nonortho,
903                self.origin.complex_symmetric(),
904                thresh,
905            )
906            .map_err(
907                |_| format_err!("Unable to Gram--Schmidt orthonormalise the linearly-independent eigenvectors of the overlap matrix.")
908            )?;
909
910            let nullity = smat.shape()[0] - nonzero_s_indices.len();
911            let xmat = if nullity == 0 && preserves_full_rank {
912                Array2::<Complex<T>>::eye(smat.shape()[0])
913            } else {
914                let s_s = Array2::<Complex<T>>::from_diag(
915                    &nonzero_s_eig.mapv(|x| Complex::<T>::from(T::one()) / x.sqrt()),
916                );
917                nonzero_umat.dot(&s_s)
918            };
919            self.smat_eigvals = Some(s_eig);
920            self.xmat = Some(xmat);
921            Ok(self)
922        }
923    }
924}
925
926pub(crate) use fn_calc_xmat_complex;
927pub(crate) use fn_calc_xmat_real;
928
929// =================
930// Utility functions
931// =================
932
933/// Logs overlap eigenvalues nicely and indicates where the threshold has been crossed.
934///
935/// # Arguments
936///
937/// * `eigvals` - The eigenvalues.
938/// * `thresh` - The cut-off threshold to be marked out.
939pub(crate) fn log_overlap_eigenvalues<T>(
940    title: &str,
941    eigvals: &Array1<T>,
942    thresh: <T as ComplexFloat>::Real,
943    eigenvalue_comparison_mode: &EigenvalueComparisonMode,
944) where
945    T: std::fmt::LowerExp + ComplexFloat,
946    <T as ComplexFloat>::Real: std::fmt::LowerExp,
947{
948    let mut eigvals_sorted = eigvals.iter().collect::<Vec<_>>();
949    match eigenvalue_comparison_mode {
950        EigenvalueComparisonMode::Modulus => {
951            eigvals_sorted.sort_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap());
952        }
953        EigenvalueComparisonMode::Real => {
954            eigvals_sorted.sort_by(|a, b| a.re().partial_cmp(&b.re()).unwrap());
955        }
956    }
957    eigvals_sorted.reverse();
958    let eigvals_str = eigvals_sorted
959        .iter()
960        .map(|v| format!("{v:+.3e}"))
961        .collect::<Vec<_>>();
962    log_subtitle(title);
963    qsym2_output!("");
964
965    match eigenvalue_comparison_mode {
966        EigenvalueComparisonMode::Modulus => {
967            qsym2_output!("Eigenvalues are sorted in decreasing order of their moduli.");
968        }
969        EigenvalueComparisonMode::Real => {
970            qsym2_output!("Eigenvalues are sorted in decreasing order of their real parts.");
971        }
972    }
973    let count_length = usize::try_from(eigvals.len().ilog10() + 2).unwrap_or(2);
974    let eigval_length = eigvals_str
975        .iter()
976        .map(|v| v.chars().count())
977        .max()
978        .unwrap_or(20);
979    qsym2_output!("{}", "┈".repeat(count_length + 3 + eigval_length));
980    qsym2_output!("{:>count_length$}  Eigenvalue", "#");
981    qsym2_output!("{}", "┈".repeat(count_length + 3 + eigval_length));
982    let mut write_thresh = false;
983    for (i, eigval) in eigvals_str.iter().enumerate() {
984        let cmp = match eigenvalue_comparison_mode {
985            EigenvalueComparisonMode::Modulus => {
986                eigvals_sorted[i].abs().partial_cmp(&thresh).expect(
987                    "Unable to compare the modulus of an eigenvalue with the specified threshold.",
988                )
989            }
990            EigenvalueComparisonMode::Real => eigvals_sorted[i].re().partial_cmp(&thresh).expect(
991                "Unable to compare the real part of an eigenvalue with the specified threshold.",
992            ),
993        };
994        if cmp == Ordering::Less && !write_thresh {
995            let comparison_mode_str = match eigenvalue_comparison_mode {
996                EigenvalueComparisonMode::Modulus => "modulus-based",
997                EigenvalueComparisonMode::Real => "real-part-based",
998            };
999            qsym2_output!(
1000                "{} <-- linear independence threshold ({comparison_mode_str}): {:+.3e}",
1001                "-".repeat(count_length + 3 + eigval_length),
1002                thresh
1003            );
1004            write_thresh = true;
1005        }
1006        qsym2_output!("{i:>count_length$}  {eigval}",);
1007    }
1008    qsym2_output!("{}", "┈".repeat(count_length + 3 + eigval_length));
1009}