Skip to main content

qsym2/chartab/
chartab_group.rs

1//! Traits enabling groups to construct and possess character tables.
2
3use std::fmt;
4use std::hash::Hash;
5use std::ops::Mul;
6use std::str::FromStr;
7
8use approx;
9use log;
10use ndarray::{Array1, Array2, Zip, array, s};
11use num::integer::lcm;
12use num_modular::{ModularInteger, MontgomeryInt};
13use num_ord::NumOrd;
14use num_traits::{Inv, One, Pow, ToPrimitive, Zero};
15use primes::is_prime;
16use rayon::prelude::*;
17use serde::{Serialize, de::DeserializeOwned};
18
19use crate::chartab::character::Character;
20use crate::chartab::chartab_symbols::{
21    CollectionSymbol, LinearSpaceSymbol, ReducibleLinearSpaceSymbol,
22};
23use crate::chartab::modular_linalg::{
24    modular_eig, split_2d_space, split_space, weighted_hermitian_inprod,
25};
26use crate::chartab::reducedint::{IntoLinAlgReducedInt, LinAlgMontgomeryInt};
27use crate::chartab::unityroot::UnityRoot;
28use crate::chartab::{CharacterTable, CorepCharacterTable, RepCharacterTable};
29use crate::group::class::ClassProperties;
30use crate::group::{
31    FiniteOrder, GroupProperties, HasUnitarySubgroup, MagneticRepresentedGroup,
32    UnitaryRepresentedGroup,
33};
34
35// =================
36// Trait definitions
37// =================
38
39/// Trait to indicate the presence of character properties in a group and enable access to the
40/// character table of the group.
41pub trait CharacterProperties: ClassProperties
42where
43    Self::RowSymbol: LinearSpaceSymbol,
44    Self::CharTab: CharacterTable<RowSymbol = Self::RowSymbol, ColSymbol = Self::ClassSymbol>,
45{
46    /// Type of the row-labelling symbols in the associated character table.
47    type RowSymbol;
48
49    /// Type of the associated character table whose row-labelling symbol type is constrained to be
50    /// the same as [`Self::RowSymbol`].
51    type CharTab;
52
53    /// Returns a shared reference to the character table of this group.
54    fn character_table(&self) -> &Self::CharTab;
55
56    /// Returns a boolean indicating if this character table contains irreducible representations
57    /// of a unitary-represented group.
58    ///
59    /// If `false`, then some elements in the group are not unitary-represented and one has
60    /// corepresentations instead of representations.
61    fn unitary_represented(&self) -> bool;
62}
63
64/// Trait for the ability to construct an irrep character table for the group.
65///
66/// This trait comes with a default implementation of character table calculation based on the
67/// Burnside--Dixon algorithm with Schneider optimisation.
68pub trait IrrepCharTabConstruction:
69    CharacterProperties<
70    CharTab = RepCharacterTable<
71        <Self as CharacterProperties>::RowSymbol,
72        <Self as ClassProperties>::ClassSymbol,
73    >,
74>
75where
76    Self: Sync,
77    Self::GroupElement: Mul<Output = Self::GroupElement>
78        + Hash
79        + Eq
80        + Clone
81        + Sync
82        + fmt::Debug
83        + FiniteOrder<Int = u32>
84        + Pow<i32, Output = Self::GroupElement>,
85{
86    /// Sets the irrep character table internally.
87    fn set_irrep_character_table(&mut self, chartab: Self::CharTab);
88
89    /// Constructs the irrep character table for this group using the Burnside--Dixon algorithm
90    /// with Schneider optimisation.
91    ///
92    /// # References
93    ///
94    /// * Dixon, J. D. High speed computation of group characters. *Numerische Mathematik* **10**, 446–450 (1967).
95    /// * Schneider, G. J. A. Dixon’s character table algorithm revisited. *Journal of Symbolic Computation* **9**, 601–606 (1990).
96    /// * Grove, L. C. Groups and Characters. (John Wiley & Sons, Inc., 1997).
97    ///
98    /// # Panics
99    ///
100    /// Panics if the Frobenius--Schur indicator for any resulted irrep takes on unexpected values
101    /// and thus implies that the computed irrep is invalid.
102    #[allow(clippy::too_many_lines)]
103    fn construct_irrep_character_table(&mut self) {
104        // Variable definitions
105        // --------------------
106        // m: LCM of the orders of the elements in the group (i.e. the group
107        //    exponent)
108        // p: A prime greater than 2*sqrt(|G|) and m | (p - 1), which is
109        //    guaranteed to exist by Dirichlet's theorem. p is guaranteed to be
110        //    odd.
111        // z: An integer having multiplicative order m when viewed as an
112        //    element of Z*p, i.e. z^m ≡ 1 (mod p) but z^n !≡ 1 (mod p) for all
113        //    0 <= n < m.
114
115        log::debug!("=============================================");
116        log::debug!("Construction of irrep character table begins.");
117        log::debug!("      *** Burnside--Dixon algorithm ***      ");
118        log::debug!("      ** with Schneider optimisation **      ");
119        log::debug!("=============================================");
120
121        // Identify a suitable finite field
122        let m = (0..self.class_number())
123            .map(|i| {
124                self.get_cc_transversal(i)
125                    .unwrap_or_else(|| panic!("No representative of class index `{i}` found."))
126                    .order()
127            })
128            .reduce(lcm)
129            .expect("Unable to find the LCM for the orders of the elements in this group.");
130        let zeta = UnityRoot::new(1, m);
131        log::debug!("Found group exponent m = {}.", m);
132        log::debug!("Chosen primitive unity root ζ = {}.", zeta);
133
134        let rf64 = (2.0
135            * self
136                .order()
137                .to_f64()
138                .unwrap_or_else(|| panic!("Unable to convert `{}` to `f64`.", self.order()))
139                .sqrt()
140            / (f64::from(m)))
141        .ceil();
142        assert!(rf64.is_sign_positive());
143        assert!(rf64 <= f64::from(u32::MAX));
144        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
145        let mut r = rf64 as u32;
146        if r == 0 {
147            r = 1;
148        };
149        let mut p = r * m + 1;
150        while !is_prime(u64::from(p)) {
151            log::debug!("Trying {}: not prime.", p);
152            r += 1;
153            p = r * m + 1;
154        }
155        log::debug!("Found r = {}.", r);
156        log::debug!("Found prime number p = r * m + 1 = {}.", p);
157        log::debug!("All arithmetic will now be carried out in GF({}).", p);
158
159        let modp = MontgomeryInt::<u32>::new(1, &p).linalg();
160        // p is prime, so there is guaranteed a z < p such that z^m ≡ 1 (mod p).
161        let mut i = 1u32;
162        while modp.convert(i).multiplicative_order().unwrap_or_else(|| {
163            panic!(
164                "Unable to find multiplicative order for `{}`",
165                modp.convert(i)
166            )
167        }) != m
168            && i < p
169        {
170            i += 1;
171        }
172        let z = modp.convert(i);
173        assert_eq!(
174            z.multiplicative_order()
175                .unwrap_or_else(|| panic!("Unable to find multiplicative order for `{z}`.")),
176            m
177        );
178        log::debug!("Found integer z = {} with multiplicative order {}.", z, m);
179
180        // Diagonalise class matrices
181        let class_sizes: Vec<_> = (0..self.class_number())
182            .map(|i| {
183                self.class_size(i)
184                    .expect("Not all class sizes can be found.")
185            })
186            .collect();
187        log::debug!("Class sizes: {:?}", class_sizes);
188        let sq_indices: Vec<usize> = (0..self.class_number())
189            .map(|i| {
190                let ele = self
191                    .get_cc_transversal(i)
192                    .unwrap_or_else(|| panic!("No representative of class index `{i}` found."));
193                let elep2 = ele.pow(2);
194                let elep2_i = self
195                    .get_index_of(&elep2)
196                    .unwrap_or_else(|| panic!("Element {elep2:?} not found."));
197                self.get_cc_of_element_index(elep2_i)
198                    .unwrap_or_else(|| panic!("Conjugacy class for element {elep2:?} not found."))
199            })
200            .collect();
201        let inverse_conjugacy_classes = Some(
202            (0..self.class_number())
203                .map(|i| {
204                    self.get_inverse_cc(i)
205                        .expect("Not all class inverses could be obtained.")
206                })
207                .collect::<Vec<_>>(),
208        );
209        log::debug!(
210            "Inverse conjugacy classes: {:?}",
211            inverse_conjugacy_classes.as_ref().unwrap()
212        );
213
214        let mut eigvecs_1d: Vec<Array1<LinAlgMontgomeryInt<u32>>> = vec![];
215        let mut success = false;
216        let mut rotate_count = 0;
217        let rs_original = (1..self.class_number()).collect::<Vec<_>>();
218
219        while !success && rotate_count < self.class_number() {
220            let mut rs = rs_original.clone();
221            rs.rotate_left(rotate_count);
222            log::debug!("Class matrix consideration order: {rs:?}");
223            rotate_count += 1;
224            let mut r_iter = rs.into_iter();
225            eigvecs_1d = vec![];
226
227            if self.class_number() == 1 {
228                eigvecs_1d.push(array![modp.convert(1)]);
229                success = true;
230            } else {
231                let mut degenerate_subspaces: Vec<Vec<Array1<LinAlgMontgomeryInt<u32>>>> = vec![];
232                let ctb = self.cayley_table();
233                let r = r_iter
234                    .next()
235                    .expect("Unable to obtain any `r` indices for class matrices.");
236                log::debug!("Considering class matrix N{r}...");
237                let nmat_1 = self.class_matrix(ctb, r).map(|&i| {
238                    modp.convert(
239                        u32::try_from(i)
240                            .unwrap_or_else(|_| panic!("Unable to convert `{i}` to `u32`.")),
241                    )
242                });
243                let eigs_1 = modular_eig(&nmat_1).unwrap_or_else(|err| {
244                    log::error!("{err}");
245                    panic!("{err}");
246                });
247                eigvecs_1d.extend(eigs_1.values().filter_map(|eigvecs| {
248                    if eigvecs.len() == 1 {
249                        Some(eigvecs[0].clone())
250                    } else {
251                        None
252                    }
253                }));
254                degenerate_subspaces
255                    .extend(eigs_1.values().filter(|eigvecs| eigvecs.len() > 1).cloned());
256
257                while !degenerate_subspaces.is_empty() {
258                    log::debug!(
259                        "Number of 1-D eigenvectors found: {} / {}.",
260                        eigvecs_1d.len(),
261                        self.class_number()
262                    );
263                    log::debug!(
264                        "Number of degenerate subspaces found: {}.",
265                        degenerate_subspaces.len(),
266                    );
267
268                    let mut degenerate_2d_subspaces = degenerate_subspaces
269                        .iter()
270                        .filter(|subspace| subspace.len() == 2)
271                        .cloned()
272                        .collect::<Vec<_>>();
273                    if !degenerate_2d_subspaces.is_empty() {
274                        degenerate_subspaces.retain(|subspace| subspace.len() > 2);
275                        log::debug!(
276                            "Number of 2-D degenerate subspaces found: {}",
277                            degenerate_2d_subspaces.len()
278                        );
279                        log::debug!(
280                            "Schneider's greedy algorithm for splitting 2-D spaces will be attempted."
281                        );
282                        while let Some(subspace) = degenerate_2d_subspaces.pop() {
283                            if let Ok(subsubspaces) = split_2d_space(
284                                &subspace,
285                                &class_sizes,
286                                &sq_indices,
287                                inverse_conjugacy_classes.as_ref(),
288                            ) {
289                                eigvecs_1d.extend(subsubspaces.iter().filter_map(|subsubspace| {
290                                    if subsubspace.len() == 1 {
291                                        Some(subsubspace[0].clone())
292                                    } else {
293                                        None
294                                    }
295                                }));
296                                log::debug!(
297                                    "2-D subspace index {} successfully split.",
298                                    degenerate_2d_subspaces.len()
299                                );
300                            } else {
301                                log::debug!(
302                                    "2-D subspace index {} cannot be split greedily.",
303                                    degenerate_2d_subspaces.len()
304                                );
305                                log::debug!(
306                                    "Stashing this 2-D subspace for splitting with class matrices..."
307                                );
308                                degenerate_subspaces.push(subspace);
309                            }
310                        }
311                    }
312
313                    if !degenerate_subspaces.is_empty() {
314                        if let Some(r) = r_iter.next() {
315                            log::debug!("Considering class matrix N{}...", r);
316                            let nmat_r = self.class_matrix(ctb, r).map(|&i| {
317                                modp.convert(u32::try_from(i).unwrap_or_else(|_| {
318                                    panic!("Unable to convert `{i}` to `u32`.")
319                                }))
320                            });
321
322                            let mut remaining_degenerate_subspaces: Vec<
323                                Vec<Array1<LinAlgMontgomeryInt<u32>>>,
324                            > = vec![];
325                            while let Some(subspace) = degenerate_subspaces.pop() {
326                                if let Ok(subsubspaces) = split_space(
327                                    &nmat_r,
328                                    &subspace,
329                                    &class_sizes,
330                                    inverse_conjugacy_classes.as_ref(),
331                                ) {
332                                    eigvecs_1d.extend(subsubspaces.iter().filter_map(
333                                        |subsubspace| {
334                                            if subsubspace.len() == 1 {
335                                                Some(subsubspace[0].clone())
336                                            } else {
337                                                None
338                                            }
339                                        },
340                                    ));
341                                    remaining_degenerate_subspaces.extend(
342                                        subsubspaces
343                                            .iter()
344                                            .filter(|subsubspace| subsubspace.len() > 1)
345                                            .cloned(),
346                                    );
347                                } else {
348                                    log::warn!(
349                                        "Class matrix N{} failed to split degenerate subspace {}.",
350                                        r,
351                                        degenerate_subspaces.len()
352                                    );
353                                    log::warn!(
354                                        "Stashing this subspace for the next class matrices..."
355                                    );
356                                    remaining_degenerate_subspaces.push(subspace);
357                                }
358                            }
359                            degenerate_subspaces = remaining_degenerate_subspaces;
360                        } else {
361                            log::warn!(
362                                "Class matrices exhausted before degenerate subspaces are fully resolved."
363                            );
364                            log::warn!(
365                                "Restarting the solver using a different class matrix order..."
366                            );
367                            break;
368                        }
369                    }
370                }
371            }
372            if eigvecs_1d.len() == self.class_number() {
373                success = true;
374            }
375        }
376
377        if eigvecs_1d.len() != self.class_number() {
378            log::error!(
379                "Degenerate subspaces failed to be fully resolved for all cyclic permutations of class matrices N1, ..., N{}.",
380                self.class_number()
381            );
382        }
383        assert_eq!(
384            eigvecs_1d.len(),
385            self.class_number(),
386            "Degenerate subspaces failed to be fully resolved."
387        );
388        log::debug!(
389            "Successfully found {} / {} one-dimensional eigenvectors for the class matrices.",
390            eigvecs_1d.len(),
391            self.class_number()
392        );
393        for (i, vec) in eigvecs_1d.iter().enumerate() {
394            log::debug!("Eigenvector {}: {}", i, vec);
395        }
396
397        // Lift characters back to the complex field
398        log::debug!("Lifting characters from GF({p}) back to the complex field...",);
399
400        let chars: Vec<_> = eigvecs_1d
401            .par_iter()
402            .flat_map(|vec_i| {
403                let vec_i_inprod = weighted_hermitian_inprod(
404                    (vec_i, vec_i),
405                    &class_sizes,
406                    inverse_conjugacy_classes.as_ref(),
407                );
408                let dim_i = (1..=p.div_euclid(2))
409                    .map(|d| vec_i_inprod.convert(d))
410                    .find(|d_modp| {
411                        Some(vec_i_inprod) == d_modp.square().inv()
412                    })
413                    .unwrap_or_else(|| {
414                        log::error!("Unable to deduce the irrep dimensionality from ⟨θvi, θvi⟩ = {vec_i_inprod} where vi = {vec_i}.");
415                        panic!("Unable to deduce the irrep dimensionality from ⟨θvi, θvi⟩ = {vec_i_inprod} where vi = {vec_i}.");
416                    });
417                log::debug!("⟨θvi, θvi⟩ = {vec_i_inprod} yields irrep dimensionality {}.", dim_i.residue());
418
419                let tchar_i =
420                    Zip::from(vec_i)
421                        .and(class_sizes.as_slice())
422                        .par_map_collect(|&v, &k| {
423                            v * dim_i
424                                / modp.convert(u32::try_from(k).unwrap_or_else(|_| {
425                                    panic!("Unable to convert `{k}` to `u32`.")
426                                }))
427                        });
428                let char_i: Vec<_> = (0..self.class_number())
429                    .into_par_iter()
430                    .map(|cc_i| {
431                        let x = self
432                            .get_cc_transversal(cc_i)
433                            .unwrap_or_else(|| {
434                                panic!("Representative of class index `{cc_i}` not found.")
435                            });
436                        let k = x.order();
437                        let xi = zeta.clone().pow(
438                            i32::try_from(m.checked_div_euclid(k).unwrap_or_else(|| {
439                                panic!("`{m}` cannot be Euclid-divided by `{k}`.")
440                            }))
441                            .unwrap_or_else(|_| {
442                                panic!(
443                                    "The Euclid division `{m} / {k}` cannot be converted to `i32`."
444                                )
445                            }),
446                        );
447                        let char_ij_terms: Vec<_> = (0..k)
448                            .into_par_iter()
449                            .map(|s| {
450                                let mu_s = (0..k).fold(modp.convert(0), |acc, l| {
451                                    let x_l =
452                                        x.clone().pow(i32::try_from(l).unwrap_or_else(|_| {
453                                            panic!("Unable to convert `{l}` to `i32`.")
454                                        }));
455                                    let x_l_idx = self
456                                        .get_index_of(&x_l)
457                                        .unwrap_or_else(|| panic!("Element {x_l:?} not found."));
458                                    let x_l_class_idx =
459                                        self.get_cc_of_element_index(x_l_idx).unwrap_or_else(|| {
460                                            panic!("Element `{x_l_idx}` does not have a conjugacy class.")
461                                        });
462                                    let tchar_i_x_l = tchar_i[x_l_class_idx];
463                                    acc + tchar_i_x_l
464                                        * z.pow(
465                                            s * l
466                                                * m.checked_div_euclid(k).unwrap_or_else(|| {
467                                                    panic!(
468                                                        "`{m}` cannot be Euclid-divided by `{k}`."
469                                                    )
470                                                }),
471                                        )
472                                        .inv()
473                                        .expect("Unable to find the modular inverse of z^(slm//k).")
474                                }) / k;
475                                (
476                                    xi.pow(i32::try_from(s).unwrap_or_else(|_| {
477                                        panic!("Unable to convert `{s}` to `i32`.")
478                                    })),
479                                    usize::try_from(mu_s.residue()).unwrap_or_else(|_| {
480                                        panic!("Unable to convert `{}` to `usize`.", mu_s.residue())
481                                    }),
482                                )
483                            })
484                            .collect();
485                        // We do not wish to simplify the character here, even if it can be
486                        // simplified (e.g. (E8)^2 + (E8)^6 can be simplified to 0). This is so
487                        // that the full unity-root-term-structure can be used in the ordering of
488                        // irreps.
489                        Character::new(&char_ij_terms)
490                    })
491                    .collect();
492                char_i
493            })
494            .collect();
495
496        let char_arr = Array2::from_shape_vec((self.class_number(), self.class_number()), chars)
497            .expect("Unable to construct the two-dimensional array of characters.");
498        log::debug!("Lifting characters from GF({p}) back to the complex field... Done.",);
499
500        let default_irrep_symbols = char_arr
501            .rows()
502            .into_iter()
503            .enumerate()
504            .map(|(irrep_i, _)| {
505                Self::RowSymbol::from_str(&format!("||Λ|_({irrep_i})|"))
506                    .ok()
507                    .expect("Unable to construct default irrep symbols.")
508            })
509            .collect::<Vec<_>>();
510        let default_principal_classes = vec![
511            self.get_cc_symbol_of_index(0)
512                .expect("No conjugacy class symbols found."),
513        ];
514
515        log::debug!("Computing the Frobenius--Schur indicators in GF({p})...");
516        let group_order = class_sizes.iter().sum::<usize>();
517        let group_order_u32 = u32::try_from(group_order).unwrap_or_else(|_| {
518            panic!("Unable to convert the group order {group_order} to `u32`.")
519        });
520        log::debug!("Indices of squared conjugacy classes: {sq_indices:?}");
521        let frobenius_schur_indicators: Vec<i8> = eigvecs_1d
522            .par_iter()
523            .map(|vec_i| {
524                let vec_i_inprod = weighted_hermitian_inprod(
525                    (vec_i, vec_i),
526                    &class_sizes,
527                    inverse_conjugacy_classes.as_ref(),
528                );
529                let dim_i = (1..=p.div_euclid(2))
530                    .map(|d| vec_i_inprod.convert(d))
531                    .find(|d_modp| {
532                        Some(vec_i_inprod) == d_modp.square().inv()
533                    })
534                    .unwrap_or_else(|| {
535                        log::error!("Unable to deduce the irrep dimensionality from ⟨θvi, θvi⟩ = {vec_i_inprod} where vi = {vec_i}.");
536                        panic!("Unable to deduce the irrep dimensionality from ⟨θvi, θvi⟩ = {vec_i_inprod} where vi = {vec_i}.");
537                    });
538
539                let tchar_i =
540                    Zip::from(vec_i)
541                        .and(class_sizes.as_slice())
542                        .par_map_collect(|&v, &k| {
543                            v * dim_i
544                                / modp.convert(u32::try_from(k).unwrap_or_else(|_| {
545                                    panic!("Unable to convert `{k}` to `u32`.")
546                                }))
547                        });
548                let fs_i = sq_indices
549                    .iter()
550                    .zip(class_sizes.iter())
551                    .fold(modp.convert(0), |acc, (&sq_idx, &k)| {
552                        let k_u32 = u32::try_from(k).unwrap_or_else(|_| {
553                            panic!("Unable to convert the class size {k} to `u32`.");
554                        });
555                        acc + modp.convert(k_u32) * tchar_i[sq_idx]
556                    }) / modp.convert(group_order_u32);
557
558                if fs_i.is_one() {
559                    1i8
560                } else if Zero::is_zero(&fs_i) {
561                    0i8
562                } else if fs_i == modp.convert(modp.modulus() - 1) {
563                    -1i8
564                } else {
565                    log::error!("Invalid Frobenius -- Schur indicator: `{fs_i}`.");
566                    panic!("Invalid Frobenius -- Schur indicator: `{fs_i}`.");
567                }
568            }).collect();
569        log::debug!("Computing the Frobenius--Schur indicators in GF({p})... Done.");
570
571        let chartab_name = if let Some(finite_name) = self.finite_subgroup_name().as_ref() {
572            format!("{} > {finite_name}", self.name())
573        } else {
574            self.name()
575        };
576        let ccsyms = (0..self.class_number())
577            .map(|i| {
578                self.get_cc_symbol_of_index(i)
579                    .unwrap_or_else(|| {
580                        let rep = self
581                            .get_cc_transversal(i)
582                            .unwrap_or_else(||
583                                panic!("Unable to obtain a representative for conjugacy class `{i}`.")
584                            );
585                        panic!("Class symbol for conjugacy class `{i}` with representative element `{rep:?}` cannot be found.")
586                    })
587            })
588            .collect::<Vec<_>>();
589        self.set_irrep_character_table(RepCharacterTable::new(
590            chartab_name.as_str(),
591            &default_irrep_symbols,
592            &ccsyms,
593            &default_principal_classes,
594            char_arr,
595            &frobenius_schur_indicators,
596        ));
597
598        log::debug!("===========================================");
599        log::debug!("     *** Burnside--Dixon algorithm ***     ");
600        log::debug!("     ** with Schneider optimisation **     ");
601        log::debug!("Construction of irrep character table ends.");
602        log::debug!("===========================================");
603    }
604}
605
606/// Trait for the ability to construct an ircorep character table for the group.
607///
608/// This trait comes with a default implementation of ircorep character table calculation based on
609/// the irreps of the unitary subgroup.
610pub trait IrcorepCharTabConstruction: HasUnitarySubgroup + CharacterProperties<
611    CharTab = CorepCharacterTable<
612        <Self as CharacterProperties>::RowSymbol,
613        <<Self as HasUnitarySubgroup>::UnitarySubgroup as CharacterProperties>::CharTab,
614    >
615>
616where
617    Self: ClassProperties<ClassSymbol = <<Self as HasUnitarySubgroup>::UnitarySubgroup as ClassProperties>::ClassSymbol>,
618    Self::RowSymbol: ReducibleLinearSpaceSymbol<
619        Subspace = <
620            <Self as HasUnitarySubgroup>::UnitarySubgroup as CharacterProperties
621        >::RowSymbol
622    >,
623    <<Self as HasUnitarySubgroup>::UnitarySubgroup as ClassProperties>::ClassSymbol: Serialize + DeserializeOwned,
624{
625    /// Sets the ircorep character table internally.
626    fn set_ircorep_character_table(&mut self, chartab: Self::CharTab);
627
628    /// Constructs the ircorep character table for this group.
629    ///
630    /// For each irrep in the unitary subgroup, the type of the ircorep it induces is determined
631    /// using the Dimmock--Wheeler character test, then the ircorep's characters in the
632    /// unitary-represented part of the full group are determined to give a square character table.
633    ///
634    /// # References
635    ///
636    /// * Bradley, C. J. & Davies, B. L. Magnetic Groups and Their Corepresentations. *Rev. Mod. Phys.* **40**, 359–379 (1968).
637    /// * Newmarch, J. D. & Golding, R. M. The character table for the corepresentations of magnetic groups. *J. Math. Phys.* **23**, 695–704 (1982).
638    /// * Newmarch, J. D. Some character theory for groups of linear and antilinear operators. *J. Math. Phys.* **24**, 742–756 (1983).
639    ///
640    /// # Panics
641    ///
642    /// Panics if any calculated ircoreps are found to be invalid.
643    fn construct_ircorep_character_table(&mut self) {
644        log::debug!("===============================================");
645        log::debug!("Construction of ircorep character table begins.");
646        log::debug!("===============================================");
647
648        if self.unitary_subgroup().order() == self.order() {
649            log::debug!(
650                "The unitary subgroup order and the full group order are both {}. This is not a magnetic group.", self.order()
651            );
652            // This is not a magnetic group. There is nothing to do.
653            return;
654        }
655
656        debug_assert_eq!(self.order() % 2, 0);
657        debug_assert_eq!(self.order().div_euclid(2), self.unitary_subgroup().order());
658        let unitary_order: i32 = self
659            .order()
660            .div_euclid(2)
661            .try_into()
662            .expect("Unable to convert the unitary group order to `i32`.");
663        let unitary_chartab = self.unitary_subgroup().character_table();
664
665        let mag_ctb = self.cayley_table().expect("Cayley table not found for the magnetic group.");
666
667        let mag = &self;
668        let uni = self.unitary_subgroup();
669        let mag_ccsyms = (0..mag.class_number()) .map(|i| {
670            mag.get_cc_symbol_of_index(i).expect("Unable to retrieve all class symbols of the magnetic group.")
671        })
672        .collect::<Vec<_>>();
673        let uni_ccsyms = (0..uni.class_number()).map(|i| {
674            uni.get_cc_symbol_of_index(i).expect("Unable to retrieve all class symbols of the unitary subgroup.")
675        }).collect::<Vec<_>>();
676        let (a0_mag_idx, _) = self
677            .elements()
678            .clone()
679            .into_iter()
680            .enumerate()
681            .find(|(_, op)| self.check_elem_antiunitary(op))
682            .expect("No antiunitary elements found in the magnetic group.");
683
684        let mut remaining_irreps = unitary_chartab.get_all_rows();
685        remaining_irreps.reverse();
686
687        let mut ircoreps_ins: Vec<(Self::RowSymbol, u8)> = Vec::new();
688        while let Some(irrep) = remaining_irreps.pop() {
689            log::debug!("Considering irrep {irrep} of the unitary subgroup...");
690            let char_sum = self
691                .elements()
692                .clone()
693                .into_iter()
694                .enumerate()
695                .filter(|(_, op)| self.check_elem_antiunitary(op))
696                .fold(Character::zero(), |acc, (a_mag_idx, _)| {
697                    let a2_mag_idx = mag_ctb[(a_mag_idx, a_mag_idx)];
698                    let a2 = self.get_index(a2_mag_idx).unwrap_or_else(|| {
699                        panic!("Element index `{a2_mag_idx}` not found in the magnetic group.")
700                    });
701                    let a2_uni_idx = self.unitary_subgroup().get_index_of(&a2).unwrap_or_else(|| {
702                        panic!("Element `{a2:?}` not found in the unitary subgroup.")
703                    });
704                    let a2_uni_class = &uni_ccsyms[
705                        uni.get_cc_of_element_index(a2_uni_idx).unwrap_or_else(|| {
706                            panic!("Conjugacy class for `{a2:?}` not found in the unitary subgroup.")
707                        })
708                    ];
709                    acc + unitary_chartab.get_character(&irrep, a2_uni_class)
710                })
711                .simplify();
712            log::debug!("  Dimmock--Wheeler indicator for {irrep}: {char_sum}");
713            let char_sum_c128 = char_sum.complex_value();
714            approx::assert_relative_eq!(
715                char_sum_c128.im,
716                0.0,
717                max_relative = char_sum.threshold()
718                    * unitary_order
719                        .to_f64()
720                        .expect("Unable to convert the unitary order to `f64`.")
721                        .sqrt(),
722                epsilon = char_sum.threshold()
723                    * unitary_order
724                        .to_f64()
725                        .expect("Unable to convert the unitary order to `f64`.")
726                        .sqrt(),
727            );
728            approx::assert_relative_eq!(
729                char_sum_c128.re,
730                char_sum_c128.re.round(),
731                max_relative = char_sum.threshold()
732                    * unitary_order
733                        .to_f64()
734                        .expect("Unable to convert the unitary order to `f64`.")
735                        .sqrt(),
736                epsilon = char_sum.threshold()
737                    * unitary_order
738                        .to_f64()
739                        .expect("Unable to convert the unitary order to `f64`.")
740                        .sqrt(),
741            );
742            let char_sum = char_sum_c128.re.round();
743
744            let (intertwining_number, ircorep) = if NumOrd(char_sum) == NumOrd(unitary_order) {
745                // Irreducible corepresentation type a
746                // Δ(u) is equivalent to Δ*[a^(-1)ua].
747                // Δ(u) is contained once in the induced irreducible corepresentation.
748                log::debug!(
749                    "  Ircorep induced by {irrep} is of type (a) with intertwining number 1."
750                );
751                (1u8, Self::RowSymbol::from_subspaces(&[(irrep, 1)]))
752            } else if NumOrd(char_sum) == NumOrd(-unitary_order) {
753                // Irreducible corepresentation type b
754                // Δ(u) is equivalent to Δ*[a^(-1)ua].
755                // Δ(u) is contained twice in the induced irreducible corepresentation.
756                log::debug!(
757                    "  Ircorep induced by {irrep} is of type (b) with intertwining number 4."
758                );
759                (4u8, Self::RowSymbol::from_subspaces(&[(irrep, 2)]))
760            } else if NumOrd(char_sum) == NumOrd(0i8) {
761                // Irreducible corepresentation type c
762                // Δ(u) is inequivalent to Δ*[a^(-1)ua].
763                // Δ(u) and Δ*[a^(-1)ua] are contained the induced irreducible corepresentation.
764                let irrep_conj_chars: Vec<Character> = unitary_chartab
765                    .get_all_cols()
766                    .iter()
767                    .enumerate()
768                    .map(|(cc_idx, cc)| {
769                        let u_cc = self
770                            .unitary_subgroup()
771                            .get_cc_index(cc_idx)
772                            .unwrap_or_else(|| panic!("Conjugacy class `{cc}` not found."));
773                        let u_unitary_idx = u_cc
774                            .iter()
775                            .next()
776                            .unwrap_or_else(|| panic!("No unitary elements found for conjugacy class `{cc}`."));
777                        let u = self.unitary_subgroup()
778                            .get_index(*u_unitary_idx)
779                            .unwrap_or_else(|| panic!("Unitary element with index `{u_unitary_idx}` cannot be retrieved."));
780                        let u_mag_idx = self
781                            .get_index_of(&u)
782                            .unwrap_or_else(|| panic!("Unable to retrieve the index of unitary element `{u:?}` in the magnetic group."));
783                        let ua0_mag_idx = mag_ctb[(u_mag_idx, a0_mag_idx)];
784                        let mag_ctb_a0x = mag_ctb.slice(s![a0_mag_idx, ..]);
785                        let a0invua0_mag_idx = mag_ctb_a0x.iter().position(|&x| x == ua0_mag_idx).unwrap_or_else(|| {
786                            panic!("No element `{ua0_mag_idx}` can be found in row `{a0_mag_idx}` of the magnetic group Cayley table.")
787                        });
788                        let a0invua0 = self
789                            .get_index(a0invua0_mag_idx)
790                            .unwrap_or_else(|| {
791                                panic!("Unable to retrieve element with index `{a0invua0_mag_idx}` in the magnetic group.")
792                            });
793                        let a0invua0_unitary_idx = self.unitary_subgroup()
794                            .get_index_of(&a0invua0)
795                            .unwrap_or_else(|| {
796                                panic!("Unable to retrieve the index of element `{a0invua0:?}` in the unitary subgroup.")
797                            });
798                        let a0invua0_unitary_class = &uni_ccsyms[
799                            uni.get_cc_of_element_index(a0invua0_unitary_idx).unwrap_or_else(|| {
800                                panic!("Unable to retrieve the class for `{a0invua0:?}` in the unitary subgroup.")
801                            })
802                        ];
803                        unitary_chartab.get_character(&irrep, a0invua0_unitary_class).complex_conjugate()
804                }).collect();
805                let all_irreps = unitary_chartab.get_all_rows();
806                let (_, conj_irrep) = all_irreps
807                    .iter()
808                    .enumerate()
809                    .find(|(irrep_idx, _)| {
810                        unitary_chartab.array().row(*irrep_idx).to_vec() == irrep_conj_chars
811                    })
812                    .unwrap_or_else(|| panic!("Conjugate irrep for {irrep} not found."));
813                assert!(remaining_irreps.shift_remove(conj_irrep));
814
815                log::debug!("  The Wigner-conjugate irrep of {irrep} is {conj_irrep}.");
816                log::debug!("  Ircorep induced by {irrep} and {conj_irrep} is of type (c) with intertwining number 2.");
817                (
818                    2u8,
819                    Self::RowSymbol::from_subspaces(&[(irrep, 1), (conj_irrep.to_owned(), 1)]),
820                )
821            } else {
822                log::error!(
823                    "Unexpected `char_sum`: {char_sum}. This can only be ±{unitary_order} or 0."
824                );
825                panic!("Unexpected `char_sum`: {char_sum}. This can only be ±{unitary_order} or 0.")
826            };
827            ircoreps_ins.push((ircorep, intertwining_number));
828        }
829
830        let mut char_arr: Array2<Character> =
831            Array2::zeros((ircoreps_ins.len(), self.class_number()));
832        for (i, (ircorep, intertwining_number)) in ircoreps_ins.iter().enumerate() {
833            for cc_idx in 0..mag.class_number() {
834                let mag_cc = &mag_ccsyms[cc_idx];
835                let mag_cc_rep = mag_cc.representative().unwrap_or_else(|| {
836                    panic!(
837                        "No representative element found for magnetic conjugacy class {mag_cc}."
838                    );
839                });
840                let mag_cc_uni_idx = self
841                    .unitary_subgroup()
842                    .get_index_of(mag_cc_rep)
843                    .unwrap_or_else(|| {
844                        panic!(
845                            "Index for element {mag_cc_rep:?} not found in the unitary subgroup."
846                        );
847                    });
848                let uni_cc = &uni_ccsyms[
849                    uni.get_cc_of_element_index(mag_cc_uni_idx).unwrap_or_else(|| {
850                        panic!("Unable to find the conjugacy class of element {mag_cc_rep:?} in the unitary subgroup.");
851                    })
852                ];
853
854                char_arr[(i, cc_idx)] = ircorep
855                    .subspaces()
856                    .iter()
857                    .fold(Character::zero(), |acc, (irrep, _)| {
858                        acc + unitary_chartab.get_character(irrep, uni_cc)
859                    });
860                if *intertwining_number == 4 {
861                    // Irreducible corepresentation type b
862                    // The inducing irrep appears twice.
863                    char_arr[(i, cc_idx)] *= 2;
864                }
865            }
866        }
867
868        let principal_classes = (0..mag.class_number())
869            .filter_map(|i| {
870                let mag_cc = &mag_ccsyms[i];
871                let mag_cc_rep = mag_cc.representative().unwrap_or_else(|| {
872                    panic!("No representative element found for magnetic conjugacy class {mag_cc}.");
873                });
874                let mag_cc_uni_idx = self.unitary_subgroup().get_index_of(mag_cc_rep).unwrap_or_else(|| {
875                    panic!("Index for element {mag_cc_rep:?} not found in the unitary subgroup.");
876                });
877                let uni_cc = uni.get_cc_symbol_of_index(
878                    uni.get_cc_of_element_index(mag_cc_uni_idx).unwrap_or_else(|| {
879                        panic!("Unable to find the conjugacy class of element {mag_cc_rep:?} in the unitary subgroup.");
880                    })
881                ).unwrap_or_else(|| {
882                    panic!("Unable to find the conjugacy class symbol of element {mag_cc_rep:?} in the unitary subgroup.");
883                });
884                if unitary_chartab.get_principal_cols().contains(&uni_cc) {
885                    Some(mag_cc.clone())
886                } else {
887                    None
888                }
889            }).collect::<Vec<_>>();
890
891        let (ircoreps, ins): (Vec<Self::RowSymbol>, Vec<u8>) = ircoreps_ins.into_iter().unzip();
892
893        let chartab_name = if let Some(finite_name) = self.finite_subgroup_name().as_ref() {
894            format!("{} > {finite_name}", self.name())
895        } else {
896            self.name()
897        };
898        self.set_ircorep_character_table(Self::CharTab::new(
899            chartab_name.as_str(),
900            unitary_chartab.clone(),
901            &ircoreps,
902            &mag_ccsyms,
903            &principal_classes,
904            char_arr,
905            &ins,
906        ));
907
908        log::debug!("=============================================");
909        log::debug!("Construction of ircorep character table ends.");
910        log::debug!("=============================================");
911    }
912}
913
914// =====================
915// Trait implementations
916// =====================
917
918// ---------------------------------------------
919// UnitaryRepresentedGroup trait implementations
920// ---------------------------------------------
921
922impl<T, RowSymbol, ColSymbol> CharacterProperties
923    for UnitaryRepresentedGroup<T, RowSymbol, ColSymbol>
924where
925    RowSymbol: LinearSpaceSymbol + Sync,
926    ColSymbol: CollectionSymbol<CollectionElement = T> + Sync,
927    T: Mul<Output = T>
928        + Inv<Output = T>
929        + Hash
930        + Eq
931        + Clone
932        + Sync
933        + fmt::Debug
934        + FiniteOrder<Int = u32>
935        + Pow<i32, Output = T>,
936    for<'a, 'b> &'b T: Mul<&'a T, Output = T>,
937{
938    type RowSymbol = RowSymbol;
939    type CharTab = RepCharacterTable<RowSymbol, ColSymbol>;
940
941    fn character_table(&self) -> &Self::CharTab {
942        self.irrep_character_table
943            .as_ref()
944            .expect("Irrep character table not found for this group.")
945    }
946
947    fn unitary_represented(&self) -> bool {
948        true
949    }
950}
951
952impl<T, RowSymbol, ColSymbol> IrrepCharTabConstruction
953    for UnitaryRepresentedGroup<T, RowSymbol, ColSymbol>
954where
955    RowSymbol: LinearSpaceSymbol + Sync,
956    ColSymbol: CollectionSymbol<CollectionElement = T> + Sync,
957    T: Mul<Output = T>
958        + Inv<Output = T>
959        + Hash
960        + Eq
961        + Clone
962        + Sync
963        + fmt::Debug
964        + FiniteOrder<Int = u32>
965        + Pow<i32, Output = T>,
966    for<'a, 'b> &'b T: Mul<&'a T, Output = T>,
967{
968    fn set_irrep_character_table(&mut self, chartab: Self::CharTab) {
969        self.irrep_character_table = Some(chartab)
970    }
971}
972
973// ----------------------------------------------
974// MagneticRepresentedGroup trait implementations
975// ----------------------------------------------
976
977impl<T, RowSymbol, UG> CharacterProperties for MagneticRepresentedGroup<T, UG, RowSymbol>
978where
979    RowSymbol: ReducibleLinearSpaceSymbol<Subspace = UG::RowSymbol> + Serialize + DeserializeOwned,
980    T: Mul<Output = T>
981        + Inv<Output = T>
982        + Hash
983        + Eq
984        + Clone
985        + Sync
986        + fmt::Debug
987        + FiniteOrder<Int = u32>
988        + Pow<i32, Output = T>,
989    for<'a, 'b> &'b T: Mul<&'a T, Output = T>,
990    UG: Clone
991        + GroupProperties<GroupElement = T>
992        + ClassProperties<GroupElement = T>
993        + CharacterProperties,
994    <UG as ClassProperties>::ClassSymbol: Serialize + DeserializeOwned,
995    <UG as CharacterProperties>::CharTab: Serialize + DeserializeOwned,
996    CorepCharacterTable<RowSymbol, <UG as CharacterProperties>::CharTab>:
997        Serialize + DeserializeOwned,
998{
999    type RowSymbol = RowSymbol;
1000    type CharTab = CorepCharacterTable<Self::RowSymbol, UG::CharTab>;
1001
1002    fn character_table(&self) -> &Self::CharTab {
1003        self.ircorep_character_table
1004            .as_ref()
1005            .expect("Ircorep character table not found for this group.")
1006    }
1007
1008    fn unitary_represented(&self) -> bool {
1009        false
1010    }
1011}
1012
1013impl<T, RowSymbol, UG> IrcorepCharTabConstruction for MagneticRepresentedGroup<T, UG, RowSymbol>
1014where
1015    RowSymbol: ReducibleLinearSpaceSymbol<Subspace = UG::RowSymbol> + Serialize + DeserializeOwned,
1016    T: Mul<Output = T>
1017        + Inv<Output = T>
1018        + Hash
1019        + Eq
1020        + Clone
1021        + Sync
1022        + fmt::Debug
1023        + FiniteOrder<Int = u32>
1024        + Pow<i32, Output = T>,
1025    for<'a, 'b> &'b T: Mul<&'a T, Output = T>,
1026    UG: Clone
1027        + GroupProperties<GroupElement = T>
1028        + ClassProperties<GroupElement = T>
1029        + CharacterProperties,
1030    <UG as ClassProperties>::ClassSymbol: Serialize + DeserializeOwned,
1031    <UG as CharacterProperties>::CharTab: Serialize + DeserializeOwned,
1032    CorepCharacterTable<RowSymbol, <UG as CharacterProperties>::CharTab>:
1033        Serialize + DeserializeOwned,
1034{
1035    fn set_ircorep_character_table(&mut self, chartab: Self::CharTab) {
1036        self.ircorep_character_table = Some(chartab);
1037    }
1038}