Skip to main content

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