qsym2/chartab/
mod.rs

1//! Symbolic computation and management of character tables.
2
3use std::cmp::max;
4use std::error::Error;
5use std::fmt;
6use std::iter;
7use std::ops::Mul;
8
9use derive_builder::Builder;
10use indexmap::{IndexMap, IndexSet};
11use ndarray::{Array2, ArrayView1};
12use num_complex::{Complex, ComplexFloat};
13use num_traits::{ToPrimitive, Zero};
14use rayon::prelude::*;
15use serde::{de::DeserializeOwned, Deserialize, Serialize};
16
17use crate::chartab::character::Character;
18use crate::chartab::chartab_symbols::{
19    CollectionSymbol, DecomposedSymbol, LinearSpaceSymbol, ReducibleLinearSpaceSymbol,
20    FROBENIUS_SCHUR_SYMBOLS,
21};
22
23pub mod character;
24pub mod chartab_group;
25pub mod chartab_symbols;
26pub(crate) mod modular_linalg;
27pub(crate) mod reducedint;
28pub mod unityroot;
29
30// =================
31// Trait definitions
32// =================
33
34/// Trait to contain essential methods for a character table.
35pub trait CharacterTable: Clone
36where
37    Self::RowSymbol: LinearSpaceSymbol,
38    Self::ColSymbol: CollectionSymbol,
39{
40    /// The type for the row-labelling symbols.
41    type RowSymbol;
42
43    /// The type for the column-labelling symbols.
44    type ColSymbol;
45
46    /// Retrieves the character of a particular irreducible representation in a particular
47    /// conjugacy class.
48    ///
49    /// # Arguments
50    ///
51    /// * `irrep` - A Mulliken irreducible representation symbol.
52    /// * `class` - A conjugacy class symbol.
53    ///
54    /// # Returns
55    ///
56    /// The required character.
57    ///
58    /// # Panics
59    ///
60    /// Panics if the specified `irrep` or `class` cannot be found.
61    fn get_character(&self, irrep: &Self::RowSymbol, class: &Self::ColSymbol) -> &Character;
62
63    /// Retrieves the characters of all columns in a particular row.
64    ///
65    /// # Arguments
66    ///
67    /// * `row` - A row-labelling symbol.
68    ///
69    /// # Returns
70    ///
71    /// The required characters.
72    fn get_row(&self, row: &Self::RowSymbol) -> ArrayView1<Character>;
73
74    /// Retrieves the characters of all rows in a particular column.
75    ///
76    /// # Arguments
77    ///
78    /// * `col` - A column-labelling symbol.
79    ///
80    /// # Returns
81    ///
82    /// The required characters.
83    fn get_col(&self, col: &Self::ColSymbol) -> ArrayView1<Character>;
84
85    /// Retrieves the symbols of all rows in the character table.
86    fn get_all_rows(&self) -> IndexSet<Self::RowSymbol>;
87
88    /// Retrieves the symbols of all columns in the character table.
89    fn get_all_cols(&self) -> IndexSet<Self::ColSymbol>;
90
91    /// Returns a shared reference to the underlying array of the character table.
92    fn array(&self) -> &Array2<Character>;
93
94    /// Retrieves the order of the group.
95    fn get_order(&self) -> usize;
96
97    /// Returns the principal columns of the character table.
98    fn get_principal_cols(&self) -> &IndexSet<Self::ColSymbol>;
99
100    /// Prints a nicely formatted character table.
101    ///
102    /// # Arguments
103    ///
104    /// * `compact` - Flag indicating if the columns are compact with unequal widths or expanded
105    /// with all equal widths.
106    /// * `numerical` - An option containing a non-negative integer specifying the number of decimal
107    /// places for the numerical forms of the characters. If `None`, the characters will be shown
108    /// as exact algebraic forms.
109    ///
110    /// # Returns
111    ///
112    /// A formatted string containing the character table in a printable form.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error when encountering any issue formatting the character table.
117    fn write_nice_table(
118        &self,
119        f: &mut fmt::Formatter,
120        compact: bool,
121        numerical: Option<usize>,
122    ) -> fmt::Result;
123}
124
125/// Trait for character tables that support decomposing a space into its irreducible subspaces
126/// using characters.
127pub trait SubspaceDecomposable<T>: CharacterTable
128where
129    T: ComplexFloat,
130    <T as ComplexFloat>::Real: ToPrimitive,
131    Self::Decomposition: ReducibleLinearSpaceSymbol<Subspace = Self::RowSymbol>,
132{
133    /// The type for the decomposed result.
134    type Decomposition;
135
136    /// Reduces a space into subspaces using its characters under the conjugacy classes of the
137    /// character table.
138    ///
139    /// # Arguments
140    ///
141    /// * `characters` - A slice of characters for conjugacy classes.
142    /// * `thresh` - Threshold for determining non-zero imaginary parts of multiplicities.
143    ///
144    /// # Returns
145    ///
146    /// The decomposition result.
147    fn reduce_characters(
148        &self,
149        characters: &[(&Self::ColSymbol, T)],
150        thresh: T::Real,
151    ) -> Result<Self::Decomposition, DecompositionError>;
152}
153
154#[derive(Debug, Clone)]
155pub struct DecompositionError(pub String);
156
157impl fmt::Display for DecompositionError {
158    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
159        write!(f, "Subspace decomposition error: {}", self.0)
160    }
161}
162
163impl Error for DecompositionError {}
164
165// ======================================
166// Struct definitions and implementations
167// ======================================
168
169// -----------------
170// RepCharacterTable
171// -----------------
172
173/// Structure to manage character tables of irreducible representations.
174#[derive(Builder, Clone, Serialize, Deserialize)]
175pub struct RepCharacterTable<RowSymbol, ColSymbol>
176where
177    RowSymbol: LinearSpaceSymbol,
178    ColSymbol: CollectionSymbol,
179{
180    /// The name given to the character table.
181    pub name: String,
182
183    /// The irreducible representations of the group and their row indices in the character
184    /// table.
185    pub(crate) irreps: IndexMap<RowSymbol, usize>,
186
187    /// The conjugacy classes of the group and their column indices in the character table.
188    pub(crate) classes: IndexMap<ColSymbol, usize>,
189
190    /// The principal conjugacy classes of the group.
191    principal_classes: IndexSet<ColSymbol>,
192
193    /// The characters of the irreducible representations in this group.
194    pub(crate) characters: Array2<Character>,
195
196    /// The Frobenius--Schur indicators for the irreducible representations in this group.
197    pub(crate) frobenius_schurs: IndexMap<RowSymbol, i8>,
198}
199
200impl<RowSymbol, ColSymbol> RepCharacterTable<RowSymbol, ColSymbol>
201where
202    RowSymbol: LinearSpaceSymbol,
203    ColSymbol: CollectionSymbol,
204{
205    /// Returns a builder to construct a `RepCharacterTable`.
206    fn builder() -> RepCharacterTableBuilder<RowSymbol, ColSymbol> {
207        RepCharacterTableBuilder::default()
208    }
209
210    /// Constructs a new character table of irreducible representations.
211    ///
212    /// # Arguments
213    ///
214    /// * `name` - A name given to the character table.
215    /// * `irreps` - A slice of Mulliken irreducible representation symbols in the right order.
216    /// * `classes` - A slice of conjugacy class symbols in the right order.
217    /// * `principal_classes` - A slice of the principal classes used in determining the irrep
218    /// symbols.
219    /// * `char_arr` - A two-dimensional array of characters.
220    /// * `frobenius_schurs` - A slice of Frobenius--Schur indicators for the irreps.
221    ///
222    /// # Returns
223    ///
224    /// A character table.
225    ///
226    /// # Panics
227    ///
228    /// Panics if the character table cannot be constructed.
229    pub(crate) fn new(
230        name: &str,
231        irreps: &[RowSymbol],
232        classes: &[ColSymbol],
233        principal_classes: &[ColSymbol],
234        char_arr: Array2<Character>,
235        frobenius_schurs: &[i8],
236    ) -> Self {
237        assert_eq!(irreps.len(), char_arr.dim().0);
238        assert_eq!(frobenius_schurs.len(), char_arr.dim().0);
239        assert_eq!(classes.len(), char_arr.dim().1);
240        assert_eq!(char_arr.dim().0, char_arr.dim().1);
241
242        let irreps_indexmap: IndexMap<RowSymbol, usize> = irreps
243            .iter()
244            .cloned()
245            .enumerate()
246            .map(|(i, irrep)| (irrep, i))
247            .collect();
248
249        let classes_indexmap: IndexMap<ColSymbol, usize> = classes
250            .iter()
251            .cloned()
252            .enumerate()
253            .map(|(i, class)| (class, i))
254            .collect();
255
256        let principal_classes_indexset: IndexSet<ColSymbol> =
257            principal_classes.iter().cloned().collect();
258
259        let frobenius_schurs_indexmap = iter::zip(irreps, frobenius_schurs)
260            .map(|(irrep, &fsi)| (irrep.clone(), fsi))
261            .collect::<IndexMap<_, _>>();
262
263        Self::builder()
264            .name(name.to_string())
265            .irreps(irreps_indexmap)
266            .classes(classes_indexmap)
267            .principal_classes(principal_classes_indexset)
268            .characters(char_arr)
269            .frobenius_schurs(frobenius_schurs_indexmap)
270            .build()
271            .expect("Unable to construct a character table.")
272    }
273}
274
275impl<RowSymbol, ColSymbol> CharacterTable for RepCharacterTable<RowSymbol, ColSymbol>
276where
277    RowSymbol: LinearSpaceSymbol,
278    ColSymbol: CollectionSymbol,
279{
280    type RowSymbol = RowSymbol;
281    type ColSymbol = ColSymbol;
282
283    /// Retrieves the character of a particular irreducible representation in a particular
284    /// conjugacy class.
285    ///
286    /// # Arguments
287    ///
288    /// * `irrep` - A Mulliken irreducible representation symbol.
289    /// * `class` - A conjugacy class symbol.
290    ///
291    /// # Returns
292    ///
293    /// The required character.
294    ///
295    /// # Panics
296    ///
297    /// Panics if the specified `irrep` or `class` cannot be found.
298    fn get_character(&self, irrep: &Self::RowSymbol, class: &Self::ColSymbol) -> &Character {
299        let row = self
300            .irreps
301            .get(irrep)
302            .unwrap_or_else(|| panic!("Irrep `{irrep}` not found."));
303        let col = self
304            .classes
305            .get(class)
306            .unwrap_or_else(|| panic!("Conjugacy class `{class}` not found."));
307        &self.characters[(*row, *col)]
308    }
309
310    /// Retrieves the characters of all conjugacy classes in a particular irreducible
311    /// representation.
312    ///
313    /// # Arguments
314    ///
315    /// * `irrep` - A Mulliken irreducible representation symbol.
316    ///
317    /// # Returns
318    ///
319    /// The required characters.
320    fn get_row(&self, irrep: &Self::RowSymbol) -> ArrayView1<Character> {
321        let row = self
322            .irreps
323            .get(irrep)
324            .unwrap_or_else(|| panic!("Irrep `{irrep}` not found."));
325        self.characters.row(*row)
326    }
327
328    /// Retrieves the characters of all irreducible representations in a particular conjugacy
329    /// class.
330    ///
331    /// # Arguments
332    ///
333    /// * `class` - A conjugacy class symbol.
334    ///
335    /// # Returns
336    ///
337    /// The required characters.
338    fn get_col(&self, class: &Self::ColSymbol) -> ArrayView1<Character> {
339        let col = self
340            .classes
341            .get(class)
342            .unwrap_or_else(|| panic!("Conjugacy class `{class}` not found."));
343        self.characters.column(*col)
344    }
345
346    /// Retrieves the Mulliken symbols of all irreducible representations of the group.
347    fn get_all_rows(&self) -> IndexSet<Self::RowSymbol> {
348        self.irreps.keys().cloned().collect::<IndexSet<_>>()
349    }
350
351    /// Retrieves the symbols of all conjugacy classes of the group.
352    fn get_all_cols(&self) -> IndexSet<Self::ColSymbol> {
353        self.classes.keys().cloned().collect::<IndexSet<_>>()
354    }
355
356    /// Returns a shared reference to the underlying array of the character table.
357    fn array(&self) -> &Array2<Character> {
358        &self.characters
359    }
360
361    /// Retrieves the order of the group.
362    fn get_order(&self) -> usize {
363        self.classes.keys().map(|cc| cc.size()).sum()
364    }
365
366    /// Prints a nicely formatted character table.
367    ///
368    /// # Arguments
369    ///
370    /// * `compact` - Flag indicating if the columns are compact with unequal widths or expanded
371    /// with all equal widths.
372    /// * `numerical` - An option containing a non-negative integer specifying the number of decimal
373    /// places for the numerical forms of the characters. If `None`, the characters will be shown
374    /// as exact algebraic forms.
375    ///
376    /// # Returns
377    ///
378    /// A formatted string containing the character table in a printable form.
379    ///
380    /// # Panics
381    ///
382    /// Panics upon encountering any missing information required for a complete print-out of the
383    /// character table.
384    ///
385    /// # Errors
386    ///
387    /// Errors upon encountering any issue formatting the character table.
388    #[allow(clippy::too_many_lines)]
389    fn write_nice_table(
390        &self,
391        f: &mut fmt::Formatter,
392        compact: bool,
393        numerical: Option<usize>,
394    ) -> fmt::Result {
395        let group_order = self.get_order();
396
397        let name = format!("u {} ({group_order})", self.name);
398        let chars_str = self.characters.map(|character| {
399            if let Some(precision) = numerical {
400                let real_only = self.characters.iter().all(|character| {
401                    approx::relative_eq!(
402                        character.complex_value().im,
403                        0.0,
404                        epsilon = character.threshold(),
405                        max_relative = character.threshold()
406                    )
407                });
408                character.get_numerical(real_only, precision)
409            } else {
410                character.to_string()
411            }
412        });
413        let irreps_str: Vec<_> = self
414            .irreps
415            .keys()
416            .map(std::string::ToString::to_string)
417            .collect();
418        let ccs_str: Vec<_> = self
419            .classes
420            .keys()
421            .map(|cc| {
422                if self.principal_classes.contains(cc) {
423                    format!("◈{cc}")
424                } else {
425                    cc.to_string()
426                }
427            })
428            .collect();
429
430        let first_width = max(
431            irreps_str
432                .iter()
433                .map(|irrep_str| irrep_str.chars().count())
434                .max()
435                .expect("Unable to find the maximum length for the irrep symbols."),
436            name.chars().count(),
437        ) + 1;
438
439        let digit_widths: Vec<_> = if compact {
440            iter::zip(chars_str.columns(), &ccs_str)
441                .map(|(chars_col_str, cc_str)| {
442                    let char_width = chars_col_str
443                        .iter()
444                        .map(|c| c.chars().count())
445                        .max()
446                        .expect("Unable to find the maximum length for the characters.");
447                    let cc_width = cc_str.chars().count();
448                    max(char_width, cc_width) + 1
449                })
450                .collect()
451        } else {
452            let char_width = chars_str
453                .iter()
454                .map(|c| c.chars().count())
455                .max()
456                .expect("Unable to find the maximum length for the characters.");
457            let cc_width = ccs_str
458                .iter()
459                .map(|cc| cc.chars().count())
460                .max()
461                .expect("Unable to find the maximum length for the conjugacy class symbols.");
462            let fixed_width = max(char_width, cc_width) + 1;
463            iter::repeat(fixed_width).take(ccs_str.len()).collect()
464        };
465
466        // Table heading
467        let mut heading = format!(" {name:^first_width$} ┆ FS ║");
468        ccs_str.iter().enumerate().for_each(|(i, cc)| {
469            heading.push_str(&format!("{cc:>width$} │", width = digit_widths[i]));
470        });
471        heading.pop();
472        let tab_width = heading.chars().count();
473        heading = format!(
474            "{}\n{}\n{}\n",
475            "━".repeat(tab_width),
476            heading,
477            "┈".repeat(tab_width),
478        );
479        write!(f, "{heading}")?;
480
481        // Table body
482        let rows =
483            iter::zip(self.irreps.keys(), irreps_str)
484                .enumerate()
485                .map(|(i, (irrep, irrep_str))| {
486                    let ind = self.frobenius_schurs.get(irrep).unwrap_or_else(|| {
487                        panic!(
488                            "Unable to obtain the Frobenius--Schur indicator for irrep `{irrep}`."
489                        )
490                    });
491                    let fs = FROBENIUS_SCHUR_SYMBOLS.get(ind).unwrap_or_else(|| {
492                        panic!("Unknown Frobenius--Schur symbol for indicator {ind}.")
493                    });
494                    let mut line = format!(" {irrep_str:<first_width$} ┆ {fs:>2} ║");
495
496                    let line_chars: String = itertools::Itertools::intersperse(
497                        ccs_str.iter().enumerate().map(|(j, _)| {
498                            format!("{:>width$}", chars_str[[i, j]], width = digit_widths[j])
499                        }),
500                        " │".to_string(),
501                    )
502                    .collect();
503
504                    line.push_str(&line_chars);
505                    line
506                });
507
508        write!(
509            f,
510            "{}",
511            &itertools::Itertools::intersperse(rows, "\n".to_string()).collect::<String>(),
512        )?;
513
514        // Table bottom
515        write!(f, "\n{}\n", &"━".repeat(tab_width))
516    }
517
518    fn get_principal_cols(&self) -> &IndexSet<Self::ColSymbol> {
519        &self.principal_classes
520    }
521}
522
523impl<RowSymbol, ColSymbol, T> SubspaceDecomposable<T> for RepCharacterTable<RowSymbol, ColSymbol>
524where
525    RowSymbol: LinearSpaceSymbol + PartialOrd + Sync + Send,
526    ColSymbol: CollectionSymbol + Sync + Send,
527    T: ComplexFloat + Sync + Send,
528    <T as ComplexFloat>::Real: ToPrimitive + Sync + Send,
529    for<'a> Complex<f64>: Mul<&'a T, Output = Complex<f64>>,
530{
531    type Decomposition = DecomposedSymbol<RowSymbol>;
532
533    /// Reduces a representation into irreducible representations using its characters under the
534    /// conjugacy classes of the character table.
535    ///
536    /// # Arguments
537    ///
538    /// * `characters` - A slice of characters for conjugacy classes.
539    /// * `thresh` - Threshold for determining non-zero imaginary parts of multiplicities.
540    ///
541    /// # Returns
542    ///
543    /// The representation as a direct sum of irreducible representations.
544    fn reduce_characters(
545        &self,
546        characters: &[(&Self::ColSymbol, T)],
547        thresh: T::Real,
548    ) -> Result<Self::Decomposition, DecompositionError> {
549        assert_eq!(characters.len(), self.classes.len());
550        let rep_syms: Result<Vec<Option<(RowSymbol, usize)>>, _> = self
551            .irreps
552            .par_iter()
553            .map(|(irrep_symbol, &i)| {
554                let c = characters
555                    .par_iter()
556                    .try_fold(|| Complex::<f64>::zero(), |acc, (cc_symbol, character)| {
557                        let j = self.classes.get_index_of(*cc_symbol).ok_or(DecompositionError(
558                            format!(
559                                "The conjugacy class `{cc_symbol}` cannot be found in this group."
560                            )
561                        ))?;
562                        Ok(
563                            acc + cc_symbol.size().to_f64().ok_or(DecompositionError(
564                                format!(
565                                    "The size of conjugacy class `{cc_symbol}` cannot be converted to `f64`."
566                                )
567                            ))?
568                                * self.characters[(i, j)].complex_conjugate().complex_value()
569                                * character
570                        )
571                    })
572                    .try_reduce(|| Complex::<f64>::zero(), |a, s| Ok(a + s))? / self.get_order().to_f64().ok_or(
573                        DecompositionError("The group order cannot be converted to `f64`.".to_string())
574                    )?;
575
576                let thresh_f64 = thresh.to_f64().expect("Unable to convert the threshold to `f64`.");
577                if approx::relative_ne!(c.im, 0.0, epsilon = thresh_f64, max_relative = thresh_f64) {
578                    Err(
579                        DecompositionError(
580                            format!(
581                                "Non-negligible imaginary part for irrep multiplicity: {:.3e}",
582                                c.im
583                            )
584                        )
585                    )
586                } else if c.re < -thresh_f64 {
587                    Err(
588                        DecompositionError(
589                            format!(
590                                "Negative irrep multiplicity: {:.3e}",
591                                c.re
592                            )
593                        )
594                    )
595                } else if approx::relative_ne!(
596                    c.re, c.re.round(), epsilon = thresh_f64, max_relative = thresh_f64
597                ) {
598                    let ndigits = (-thresh_f64.log10())
599                        .ceil()
600                        .to_usize()
601                        .expect("Unable to convert the number of digits to `usize`.");
602                    Err(
603                        DecompositionError(
604                            format!(
605                                "Non-integer coefficient: {:.ndigits$e}",
606                                c.re
607                            )
608                        )
609                    )
610                } else {
611                    let mult = c.re.round().to_usize().ok_or(DecompositionError(
612                        format!(
613                            "Unable to convert the rounded coefficient `{}` to `usize`.",
614                            c.re.round()
615                        )
616                    ))?;
617                    if mult != 0 {
618                        Ok(Some((irrep_symbol.clone(), mult)))
619                    } else {
620                        Ok(None)
621                    }
622                }
623        })
624        .collect();
625
626        rep_syms.map(|syms| {
627            DecomposedSymbol::<RowSymbol>::from_subspaces(
628                &syms.into_iter().flatten().collect::<Vec<_>>(),
629            )
630        })
631    }
632}
633
634impl<RowSymbol, ColSymbol> fmt::Display for RepCharacterTable<RowSymbol, ColSymbol>
635where
636    RowSymbol: LinearSpaceSymbol,
637    ColSymbol: CollectionSymbol,
638{
639    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
640        self.write_nice_table(f, true, Some(3))
641    }
642}
643
644impl<RowSymbol, ColSymbol> fmt::Debug for RepCharacterTable<RowSymbol, ColSymbol>
645where
646    RowSymbol: LinearSpaceSymbol,
647    ColSymbol: CollectionSymbol,
648{
649    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
650        self.write_nice_table(f, true, None)
651    }
652}
653
654// -------------------
655// CorepCharacterTable
656// -------------------
657
658/// Structure to manage character tables of irreducible corepresentations of magnetic groups.
659#[derive(Builder, Clone, Serialize, Deserialize)]
660pub struct CorepCharacterTable<RowSymbol, UC>
661where
662    <UC as CharacterTable>::ColSymbol: Serialize + DeserializeOwned,
663    RowSymbol: ReducibleLinearSpaceSymbol,
664    UC: CharacterTable,
665{
666    /// The name given to the character table.
667    pub name: String,
668
669    /// The character table of the irreducible representations of the halving unitary subgroup that
670    /// induce the irreducible corepresentations of the current magnetic group.
671    pub(crate) unitary_character_table: UC,
672
673    /// The irreducible corepresentations of the group and their row indices in the character
674    /// table.
675    pub(crate) ircoreps: IndexMap<RowSymbol, usize>,
676
677    /// The conjugacy classes of the group and their column indices in the character table.
678    classes: IndexMap<UC::ColSymbol, usize>,
679
680    /// The principal conjugacy classes of the group.
681    principal_classes: IndexSet<UC::ColSymbol>,
682
683    /// The characters of the irreducible corepresentations in this group.
684    characters: Array2<Character>,
685
686    /// The intertwining numbers of the irreducible corepresentations.
687    pub(crate) intertwining_numbers: IndexMap<RowSymbol, u8>,
688}
689
690impl<RowSymbol, UC> CorepCharacterTable<RowSymbol, UC>
691where
692    <UC as CharacterTable>::ColSymbol: Serialize + DeserializeOwned,
693    RowSymbol: ReducibleLinearSpaceSymbol,
694    UC: CharacterTable,
695{
696    /// Returns a builder to construct a new [`CorepCharacterTable`].
697    fn builder() -> CorepCharacterTableBuilder<RowSymbol, UC> {
698        CorepCharacterTableBuilder::default()
699    }
700
701    /// Constructs a new character table of irreducible corepresentations.
702    ///
703    /// # Arguments
704    ///
705    /// * `name` - A name given to the character table.
706    /// * `unitary_chartab` - The character table of irreducible representations of the unitary
707    /// halving subgroup, which will be owned by this [`CorepCharacterTable`].
708    /// * `ircoreps` - A slice of Mulliken irreducible corepresentation symbols in the right order.
709    /// * `classes` - A slice of conjugacy class symbols in the right order. These symbols must be
710    /// of the same type as those of the unitary subgroup.
711    /// * `principal_classes` - A slice of the principal classes of the group.
712    /// * `char_arr` - A two-dimensional array of characters,
713    /// * `intertwining_numbers` - A slice of the intertwining numbers of the irreducible
714    /// corepresentations in the right order.
715    ///
716    /// # Returns
717    ///
718    /// A character table.
719    ///
720    /// # Panics
721    ///
722    /// Panics if the character table cannot be constructed.
723    pub(crate) fn new(
724        name: &str,
725        unitary_chartab: UC,
726        ircoreps: &[RowSymbol],
727        classes: &[UC::ColSymbol],
728        principal_classes: &[UC::ColSymbol],
729        char_arr: Array2<Character>,
730        intertwining_numbers: &[u8],
731    ) -> Self {
732        assert_eq!(ircoreps.len(), char_arr.dim().0);
733        assert_eq!(intertwining_numbers.len(), char_arr.dim().0);
734
735        let ircoreps_indexmap: IndexMap<RowSymbol, usize> = ircoreps
736            .iter()
737            .cloned()
738            .enumerate()
739            .map(|(i, ircorep)| (ircorep, i))
740            .collect();
741
742        let classes_indexmap: IndexMap<UC::ColSymbol, usize> = classes
743            .iter()
744            .cloned()
745            .enumerate()
746            .map(|(i, class)| (class, i))
747            .collect();
748
749        let principal_classes_indexset: IndexSet<UC::ColSymbol> =
750            principal_classes.iter().cloned().collect();
751
752        let intertwining_numbers_indexmap = iter::zip(ircoreps, intertwining_numbers)
753            .map(|(ircorep, &ini)| (ircorep.clone(), ini))
754            .collect::<IndexMap<_, _>>();
755
756        Self::builder()
757            .name(name.to_string())
758            .unitary_character_table(unitary_chartab)
759            .ircoreps(ircoreps_indexmap)
760            .classes(classes_indexmap)
761            .principal_classes(principal_classes_indexset)
762            .characters(char_arr)
763            .intertwining_numbers(intertwining_numbers_indexmap)
764            .build()
765            .expect("Unable to construct a character table.")
766    }
767}
768
769impl<RowSymbol, UC> CharacterTable for CorepCharacterTable<RowSymbol, UC>
770where
771    <UC as CharacterTable>::ColSymbol: Serialize + DeserializeOwned,
772    RowSymbol: ReducibleLinearSpaceSymbol,
773    UC: CharacterTable,
774{
775    type RowSymbol = RowSymbol;
776    type ColSymbol = UC::ColSymbol;
777
778    /// Retrieves the character of a particular irreducible corepresentation in a particular
779    /// unitary conjugacy class.
780    ///
781    /// # Arguments
782    ///
783    /// * `ircorep` - A Mulliken irreducible representation symbol.
784    /// * `class` - A unitary conjugacy class symbol.
785    ///
786    /// # Returns
787    ///
788    /// The required character.
789    ///
790    /// # Panics
791    ///
792    /// Panics if the specified `ircorep` or `class` cannot be found.
793    fn get_character(&self, ircorep: &Self::RowSymbol, class: &Self::ColSymbol) -> &Character {
794        let row = self
795            .ircoreps
796            .get(ircorep)
797            .unwrap_or_else(|| panic!("Ircorep `{ircorep}` not found."));
798        let col = self
799            .classes
800            .get(class)
801            .unwrap_or_else(|| panic!("Conjugacy class `{class}` not found."));
802        &self.characters[(*row, *col)]
803    }
804
805    /// Retrieves the characters of all conjugacy classes in a particular irreducible
806    /// corepresentation.
807    ///
808    /// # Arguments
809    ///
810    /// * `ircorep` - A Mulliken irreducible corepresentation symbol.
811    ///
812    /// # Returns
813    ///
814    /// The required characters.
815    fn get_row(&self, ircorep: &Self::RowSymbol) -> ArrayView1<Character> {
816        let row = self
817            .ircoreps
818            .get(ircorep)
819            .unwrap_or_else(|| panic!("Ircorep `{ircorep}` not found."));
820        self.characters.row(*row)
821    }
822
823    /// Retrieves the characters of all irreducible corepresentations in a particular conjugacy
824    /// class.
825    ///
826    /// # Arguments
827    ///
828    /// * `class` - A conjugacy class symbol.
829    ///
830    /// # Returns
831    ///
832    /// The required characters.
833    fn get_col(&self, class: &Self::ColSymbol) -> ArrayView1<Character> {
834        let col = self
835            .classes
836            .get(class)
837            .unwrap_or_else(|| panic!("Conjugacy class `{class}` not found."));
838        self.characters.column(*col)
839    }
840
841    /// Retrieves the Mulliken symbols of all irreducible corepresentations of the group.
842    fn get_all_rows(&self) -> IndexSet<Self::RowSymbol> {
843        self.ircoreps.keys().cloned().collect::<IndexSet<_>>()
844    }
845
846    /// Retrieves the symbols of all conjugacy classes of the group.
847    fn get_all_cols(&self) -> IndexSet<Self::ColSymbol> {
848        self.classes.keys().cloned().collect::<IndexSet<_>>()
849    }
850
851    /// Returns a shared reference to the underlying array of the character table.
852    fn array(&self) -> &Array2<Character> {
853        &self.characters
854    }
855
856    /// Retrieves the order of the group.
857    fn get_order(&self) -> usize {
858        2 * self.unitary_character_table.get_order()
859    }
860
861    /// Prints a nicely formatted character table.
862    ///
863    /// # Arguments
864    ///
865    /// * `compact` - Flag indicating if the columns are compact with unequal widths or expanded
866    /// with all equal widths.
867    /// * `numerical` - An option containing a non-negative integer specifying the number of decimal
868    /// places for the numerical forms of the characters. If `None`, the characters will be shown
869    /// as exact algebraic forms.
870    ///
871    /// # Returns
872    ///
873    /// A formatted string containing the character table in a printable form.
874    ///
875    /// # Panics
876    ///
877    /// Panics upon encountering any missing information required for a complete print-out of the
878    /// character table.
879    ///
880    /// # Errors
881    ///
882    /// Errors upon encountering any issue formatting the character table.
883    #[allow(clippy::too_many_lines)]
884    fn write_nice_table(
885        &self,
886        f: &mut fmt::Formatter,
887        compact: bool,
888        numerical: Option<usize>,
889    ) -> fmt::Result {
890        let group_order = self.get_order();
891
892        let name = format!("m {} ({})", self.name, group_order);
893        let chars_str = self.characters.map(|character| {
894            if let Some(precision) = numerical {
895                let real_only = self.characters.iter().all(|character| {
896                    approx::relative_eq!(
897                        character.complex_value().im,
898                        0.0,
899                        epsilon = character.threshold(),
900                        max_relative = character.threshold()
901                    )
902                });
903                character.get_numerical(real_only, precision)
904            } else {
905                character.to_string()
906            }
907        });
908        let ircoreps_str: Vec<_> = self
909            .ircoreps
910            .keys()
911            .map(std::string::ToString::to_string)
912            .collect();
913        let ccs_str: Vec<_> = self
914            .classes
915            .keys()
916            .map(|cc| {
917                if self.principal_classes.contains(cc) {
918                    format!("◈{cc}")
919                } else {
920                    cc.to_string()
921                }
922            })
923            .collect();
924
925        let first_width = max(
926            ircoreps_str
927                .iter()
928                .map(|ircorep_str| ircorep_str.chars().count())
929                .max()
930                .expect("Unable to find the maximum length for the ircorep symbols."),
931            name.chars().count(),
932        ) + 1;
933
934        let digit_widths: Vec<_> = if compact {
935            iter::zip(chars_str.columns(), &ccs_str)
936                .map(|(chars_col_str, cc_str)| {
937                    let char_width = chars_col_str
938                        .iter()
939                        .map(|c| c.chars().count())
940                        .max()
941                        .expect("Unable to find the maximum length for the characters.");
942                    let cc_width = cc_str.chars().count();
943                    max(char_width, cc_width) + 1
944                })
945                .collect()
946        } else {
947            let char_width = chars_str
948                .iter()
949                .map(|c| c.chars().count())
950                .max()
951                .expect("Unable to find the maximum length for the characters.");
952            let cc_width = ccs_str
953                .iter()
954                .map(|cc| cc.chars().count())
955                .max()
956                .expect("Unable to find the maximum length for the conjugacy class symbols.");
957            let fixed_width = max(char_width, cc_width) + 1;
958            iter::repeat(fixed_width).take(ccs_str.len()).collect()
959        };
960
961        // Table heading
962        let mut heading = format!(" {name:^first_width$} ┆ IN ║");
963        ccs_str.iter().enumerate().for_each(|(i, cc)| {
964            heading.push_str(&format!("{cc:>width$} │", width = digit_widths[i]));
965        });
966        heading.pop();
967        let tab_width = heading.chars().count();
968        heading = format!(
969            "{}\n{}\n{}\n",
970            "━".repeat(tab_width),
971            heading,
972            "┈".repeat(tab_width),
973        );
974        write!(f, "{heading}")?;
975
976        // Table body
977        let rows = iter::zip(self.ircoreps.keys(), ircoreps_str)
978            .enumerate()
979            .map(|(i, (ircorep, ircorep_str))| {
980                let intertwining_number =
981                    self.intertwining_numbers.get(ircorep).unwrap_or_else(|| {
982                        panic!("Unable to obtain the intertwining_number for ircorep `{ircorep}`.")
983                    });
984                let mut line = format!(" {ircorep_str:<first_width$} ┆ {intertwining_number:>2} ║");
985
986                let line_chars: String = itertools::Itertools::intersperse(
987                    ccs_str.iter().enumerate().map(|(j, _)| {
988                        format!("{:>width$}", chars_str[[i, j]], width = digit_widths[j])
989                    }),
990                    " │".to_string(),
991                )
992                .collect();
993
994                line.push_str(&line_chars);
995                line
996            });
997
998        write!(
999            f,
1000            "{}",
1001            &itertools::Itertools::intersperse(rows, "\n".to_string()).collect::<String>(),
1002        )?;
1003
1004        // Table bottom
1005        write!(f, "\n{}\n", &"━".repeat(tab_width))
1006    }
1007
1008    fn get_principal_cols(&self) -> &IndexSet<Self::ColSymbol> {
1009        &self.principal_classes
1010    }
1011}
1012
1013impl<RowSymbol, UC, T> SubspaceDecomposable<T> for CorepCharacterTable<RowSymbol, UC>
1014where
1015    RowSymbol: ReducibleLinearSpaceSymbol + PartialOrd + Sync + Send,
1016    UC: CharacterTable + Sync + Send,
1017    <UC as CharacterTable>::ColSymbol: Serialize + DeserializeOwned + Sync + Send,
1018    T: ComplexFloat + Sync + Send,
1019    <T as ComplexFloat>::Real: ToPrimitive + Sync + Send,
1020    for<'a> Complex<f64>: Mul<&'a T, Output = Complex<f64>>,
1021{
1022    type Decomposition = DecomposedSymbol<RowSymbol>;
1023
1024    /// Reduces a corepresentation into irreducible corepresentations using its characters under the
1025    /// conjugacy classes of the character table.
1026    ///
1027    /// # Arguments
1028    ///
1029    /// * `characters` - A slice of characters for conjugacy classes.
1030    /// * `thresh` - Threshold for determining non-zero imaginary parts of multiplicities.
1031    ///
1032    /// # Returns
1033    ///
1034    /// The corepresentation as a direct sum of irreducible corepresentations.
1035    fn reduce_characters(
1036        &self,
1037        characters: &[(&Self::ColSymbol, T)],
1038        thresh: T::Real,
1039    ) -> Result<Self::Decomposition, DecompositionError> {
1040        assert_eq!(characters.len(), self.classes.len());
1041        let rep_syms: Result<Vec<Option<(RowSymbol, usize)>>, _> = self
1042            .ircoreps
1043            .par_iter()
1044            .map(|(ircorep_symbol, &i)| {
1045                let c = characters
1046                    .par_iter()
1047                    .try_fold(|| Complex::<f64>::zero(), |acc, (cc_symbol, character)| {
1048                        let j = self.classes.get_index_of(*cc_symbol).ok_or(DecompositionError(
1049                            format!(
1050                                "The conjugacy class `{cc_symbol}` cannot be found in this group."
1051                            )
1052                        ))?;
1053                        Ok(
1054                            acc + cc_symbol.size().to_f64().ok_or(DecompositionError(
1055                                format!(
1056                                    "The size of conjugacy class `{cc_symbol}` cannot be converted to `f64`."
1057                                )
1058                            ))?
1059                                * self.characters[(i, j)].complex_conjugate().complex_value()
1060                                * character
1061                        )
1062                    })
1063                    .try_reduce(|| Complex::<f64>::zero(), |a, s| Ok(a + s))? / (self.unitary_character_table.get_order().to_f64().ok_or(
1064                        DecompositionError("The unitary subgroup order cannot be converted to `f64`.".to_string())
1065                    )? * self.intertwining_numbers.get(ircorep_symbol).and_then(|x| x.to_f64()).ok_or(
1066                        DecompositionError(
1067                            format!(
1068                                "The intertwining number of `{ircorep_symbol}` cannot be retrieved and/or converted to `f64`."
1069                            )
1070                        )
1071                    )?);
1072
1073                let thresh_f64 = thresh.to_f64().expect("Unable to convert the threshold to `f64`.");
1074                if approx::relative_ne!(c.im, 0.0, epsilon = thresh_f64, max_relative = thresh_f64) {
1075                    Err(
1076                        DecompositionError(
1077                            format!(
1078                                "Non-negligible imaginary part for ircorep multiplicity: {:.3e}",
1079                                c.im
1080                            )
1081                        )
1082                    )
1083                } else if c.re < -thresh_f64 {
1084                    Err(
1085                        DecompositionError(
1086                            format!(
1087                                "Negative ircorep multiplicity: {:.3e}",
1088                                c.re
1089                            )
1090                        )
1091                    )
1092                } else if approx::relative_ne!(
1093                    c.re, c.re.round(), epsilon = thresh_f64, max_relative = thresh_f64
1094                ) {
1095                    let ndigits = (-thresh_f64.log10())
1096                        .ceil()
1097                        .to_usize()
1098                        .expect("Unable to convert the number of digits to `usize`.");
1099                    Err(
1100                        DecompositionError(
1101                            format!(
1102                                "Non-integer coefficient: {:.ndigits$e}",
1103                                c.re
1104                            )
1105                        )
1106                    )
1107                } else {
1108                    let mult = c.re.round().to_usize().ok_or(DecompositionError(
1109                        format!(
1110                            "Unable to convert the rounded coefficient `{}` to `usize`.",
1111                            c.re.round()
1112                        )
1113                    ))?;
1114                    if mult != 0 {
1115                        Ok(Some((ircorep_symbol.clone(), mult)))
1116                    } else {
1117                        Ok(None)
1118                    }
1119                }
1120        })
1121        .collect();
1122
1123        rep_syms.map(|syms| {
1124            DecomposedSymbol::<RowSymbol>::from_subspaces(
1125                &syms.into_iter().flatten().collect::<Vec<_>>(),
1126            )
1127        })
1128    }
1129}
1130
1131impl<RowSymbol, UC> fmt::Display for CorepCharacterTable<RowSymbol, UC>
1132where
1133    RowSymbol: ReducibleLinearSpaceSymbol,
1134    UC: CharacterTable,
1135    <UC as CharacterTable>::ColSymbol: Serialize + DeserializeOwned,
1136{
1137    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1138        self.write_nice_table(f, true, Some(3))
1139    }
1140}
1141
1142impl<RowSymbol, UC> fmt::Debug for CorepCharacterTable<RowSymbol, UC>
1143where
1144    RowSymbol: ReducibleLinearSpaceSymbol,
1145    UC: CharacterTable,
1146    <UC as CharacterTable>::ColSymbol: Serialize + DeserializeOwned,
1147{
1148    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1149        self.write_nice_table(f, true, None)
1150    }
1151}