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