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