qsym2/symmetry/
symmetry_group.rs

1//! Abstract groups of symmetry operations.
2
3use anyhow::{self, ensure};
4use counter::Counter;
5use indexmap::IndexMap;
6use itertools::Itertools;
7use lazy_static::lazy_static;
8use nalgebra::Vector3;
9use ordered_float::OrderedFloat;
10use regex::Regex;
11
12use crate::auxiliary::geometry::{self, PositiveHemisphere};
13use crate::chartab::chartab_group::{
14    CharacterProperties, IrcorepCharTabConstruction, IrrepCharTabConstruction,
15};
16use crate::chartab::chartab_symbols::CollectionSymbol;
17use crate::chartab::{CharacterTable, RepCharacterTable};
18use crate::group::class::ClassProperties;
19use crate::group::{GroupProperties, GroupType, MagneticRepresentedGroup, UnitaryRepresentedGroup};
20use crate::symmetry::symmetry_core::Symmetry;
21use crate::symmetry::symmetry_element::symmetry_operation::{
22    sort_operations, SpecialSymmetryTransformation,
23};
24use crate::symmetry::symmetry_element::{SymmetryOperation, SIG};
25use crate::symmetry::symmetry_symbols::{
26    deduce_mulliken_irrep_symbols, deduce_principal_classes, sort_irreps, MullikenIrcorepSymbol,
27    MullikenIrrepSymbol, SymmetryClassSymbol, FORCED_C3_PRINCIPAL_GROUPS,
28};
29
30#[cfg(test)]
31#[path = "symmetry_group_tests.rs"]
32mod symmetry_group_tests;
33
34#[cfg(test)]
35#[path = "symmetry_chartab_tests.rs"]
36mod symmetry_chartab_tests;
37
38lazy_static! {
39    static ref SQUARE_BRACKETED_GROUP_NAME_RE: Regex =
40        Regex::new(r"u\[(.+)\]").expect("Regex pattern invalid.");
41}
42
43// ======================
44// Type alias definitions
45// ======================
46
47/// Type for symmetry groups in which all elements are represented as mathematical unitary
48/// operators. These groups admit unitary irreducible representations.
49pub type UnitaryRepresentedSymmetryGroup = UnitaryRepresentedGroup<
50    SymmetryOperation,
51    MullikenIrrepSymbol,
52    SymmetryClassSymbol<SymmetryOperation>,
53>;
54
55/// Type for symmetry groups in which half of the elements are represented as mathematical unitary
56/// operators, and the other half as mathematical antiunitary operators. These groups admit
57/// irreducible corepresentations.
58pub type MagneticRepresentedSymmetryGroup = MagneticRepresentedGroup<
59    SymmetryOperation,
60    UnitaryRepresentedSymmetryGroup,
61    MullikenIrcorepSymbol,
62>;
63
64// =================
65// Trait definitions
66// =================
67
68/// Trait defining behaviours of symmetry groups.
69pub trait SymmetryGroupProperties:
70    ClassProperties<
71        GroupElement = SymmetryOperation,
72        ClassSymbol = SymmetryClassSymbol<SymmetryOperation>,
73    > + CharacterProperties
74    + Sized
75{
76    // ----------------
77    // Required methods
78    // ----------------
79
80    /// Constructs a group from molecular symmetry *elements* (not operations).
81    ///
82    /// # Arguments
83    ///
84    /// * `sym` - A molecular symmetry structure containing the symmetry *elements*.
85    /// * `infinite_order_to_finite` - Interpret infinite-order generating
86    /// elements as finite-order generating elements to create a finite subgroup
87    /// of an otherwise infinite group.
88    ///
89    /// # Returns
90    ///
91    /// A finite group of symmetry operations.
92    fn from_molecular_symmetry(
93        sym: &Symmetry,
94        infinite_order_to_finite: Option<u32>,
95    ) -> Result<Self, anyhow::Error>;
96
97    /// Converts a symmetry group to its corresponding double group.
98    ///
99    /// # Returns
100    ///
101    /// The corresponding double group.
102    fn to_double_group(&self) -> Result<Self, anyhow::Error>;
103
104    /// Reorders and relabels the rows and columns of the constructed character table using
105    /// symmetry-specific rules and conventions.
106    ///
107    /// The default implementation of this method is to do nothing. Specific trait implementations
108    /// can override this to provide specific ways to canonicalise character tables.
109    fn canonicalise_character_table(&mut self) {}
110
111    // ----------------
112    // Provided methods
113    // ----------------
114
115    /// Deduces the group name in Schönflies notation of a finite subgroup of an infinite molecular
116    /// symmetry group.
117    fn deduce_finite_group_name(&mut self) -> String {
118        let (full_name, full_order, grey, double) = if let Some((_, [grp_name])) =
119            SQUARE_BRACKETED_GROUP_NAME_RE
120                .captures(&self.name())
121                .map(|caps| caps.extract())
122        {
123            // u[group] means taking the unitary halving subgroup of 'group', i.e. 'group' is a grey
124            // group.
125            // So, we shall deduce the finite subgroup of 'group' first, and then take the unitary
126            // halving subgroup of that.
127            (
128                grp_name.to_string(),
129                self.order() * 2,
130                true,
131                grp_name.contains('*'),
132            )
133        } else {
134            (self.name(), self.order(), false, self.name().contains('*'))
135        };
136        let finite_group = if full_name.contains('∞') {
137            // C∞, C∞h, C∞v, S∞, D∞, D∞h, D∞d, or the corresponding grey groups
138            if full_name.as_bytes()[0] == b'D' {
139                if matches!(
140                    full_name
141                        .as_bytes()
142                        .iter()
143                        .last()
144                        .expect("The last character in the group name cannot be retrieved."),
145                    b'h' | b'd'
146                ) {
147                    if full_name.contains('θ') {
148                        assert_eq!(full_order % 8, 0);
149                        full_name.replace('∞', format!("{}", full_order / 8).as_str())
150                    } else {
151                        assert_eq!(full_order % 4, 0);
152                        full_name.replace('∞', format!("{}", full_order / 4).as_str())
153                    }
154                } else if full_name.contains('θ') {
155                    assert_eq!(full_order % 4, 0);
156                    full_name.replace('∞', format!("{}", full_order / 4).as_str())
157                } else {
158                    assert_eq!(full_order % 2, 0);
159                    full_name.replace('∞', format!("{}", full_order / 2).as_str())
160                }
161            } else {
162                assert!(matches!(full_name.as_bytes()[0], b'C' | b'S'));
163                if matches!(
164                    full_name
165                        .as_bytes()
166                        .iter()
167                        .last()
168                        .expect("The last character in the group name cannot be retrieved."),
169                    b'h' | b'v'
170                ) {
171                    if full_name.contains('θ') {
172                        assert_eq!(
173                            full_order % 4,
174                            0,
175                            "Unexpected order {} for group {full_name}.",
176                            full_order
177                        );
178                    } else {
179                        assert_eq!(
180                            full_order % 2,
181                            0,
182                            "Unexpected order {} for group {full_name}.",
183                            full_order
184                        );
185                    }
186                    if full_order > 2 {
187                        if full_name.contains('θ') {
188                            full_name.replace('∞', format!("{}", full_order / 4).as_str())
189                        } else {
190                            full_name.replace('∞', format!("{}", full_order / 2).as_str())
191                        }
192                    } else {
193                        assert_eq!(full_name.as_bytes()[0], b'C');
194                        "Cs".to_string()
195                    }
196                } else {
197                    full_name.replace('∞', format!("{}", full_order).as_str())
198                }
199            }
200        } else if full_name.contains("O(3)") {
201            // O(3) or the corresponding grey group
202            match (full_order, double) {
203                (8, false) => "D2h".to_string(),
204                (16, true) => "D2h*".to_string(),
205                (16, false) => "D2h + θ·D2h".to_string(),
206                (32, true) => "(D2h + θ·D2h)*".to_string(),
207                (48, false) => "Oh".to_string(),
208                (96, true) => "Oh*".to_string(),
209                (96, false) => "Oh + θ·Oh".to_string(),
210                (192, true) => "(Oh + θ·Oh)*".to_string(),
211                _ => panic!("Unsupported number of group elements ({full_order}) for a finite group of {full_name}."),
212            }
213        } else {
214            // This is already a finite group.
215            full_name
216        };
217        if grey {
218            format!("u[{finite_group}]")
219        } else {
220            finite_group
221        }
222    }
223
224    /// Returns `true` if all elements in this group are unitary.
225    fn all_unitary(&self) -> bool {
226        self.elements()
227            .clone()
228            .into_iter()
229            .all(|op| !op.contains_time_reversal())
230    }
231
232    /// Returns `true` if all elements in this group are in $`\mathsf{SU}'(2)`$ or `false` if they
233    /// are all in $`\mathsf{O}(3)`$.
234    ///
235    /// # Panics
236    ///
237    /// Panics if mixed $`\mathsf{SU}'(2)`$ and $`\mathsf{O}(3)`$ elements are found.
238    fn is_double_group(&self) -> bool {
239        if self.elements().clone().into_iter().all(|op| op.is_su2()) {
240            true
241        } else if self.elements().clone().into_iter().all(|op| !op.is_su2()) {
242            false
243        } else {
244            panic!("Mixed SU(2) and SO(3) proper rotations are not allowed.");
245        }
246    }
247
248    /// Determines whether this group is an ordinary (double) group, a magnetic grey (double)
249    /// group, or a magnetic black-and-white (double) group.
250    fn group_type(&self) -> GroupType {
251        let double = self.is_double_group();
252        if self.all_unitary() {
253            GroupType::Ordinary(double)
254        } else if self
255            .elements()
256            .clone()
257            .into_iter()
258            .any(|op| op.is_time_reversal())
259        {
260            GroupType::MagneticGrey(double)
261        } else {
262            GroupType::MagneticBlackWhite(double)
263        }
264    }
265
266    /// Returns the conjugacy class symbols in this group based on molecular symmetry.
267    fn class_symbols_from_symmetry(&mut self) -> Vec<SymmetryClassSymbol<SymmetryOperation>> {
268        log::debug!("Assigning class symbols from symmetry operations...");
269        let mut undashed_class_symbols: Counter<SymmetryClassSymbol<SymmetryOperation>, usize> =
270            Counter::new();
271
272        let symmetry_class_symbols = (0..self.class_number())
273            .map(|i| {
274                let old_symbol = self.get_cc_symbol_of_index(i).unwrap_or_else(|| {
275                    panic!("No symmetry symbol for class index `{i}` can be found.")
276                });
277                let rep_ele_index = *self
278                    .get_cc_index(i)
279                    .unwrap_or_else(|| panic!("No conjugacy class index `{i}` can be found."))
280                    .iter()
281                    .min_by_key(|&&j| {
282                        let op = self
283                            .get_index(j)
284                            .unwrap_or_else(|| {
285                                panic!("Element with index {j} cannot be retrieved.")
286                            })
287                            .to_symmetry_element();
288                        (
289                            op.is_su2_class_1(), // prioritise class 0
290                            !geometry::check_standard_positive_pole(
291                                &op.proper_rotation_pole(),
292                                op.threshold(),
293                            ), // prioritise positive rotation
294                            op.proper_fraction()
295                                .map(|frac| {
296                                    *frac.numer().expect(
297                                        "The numerator of the proper fraction cannot be retrieved.",
298                                    )
299                                })
300                                .or_else(|| op.raw_proper_power().map(|pp| pp.unsigned_abs()))
301                                .expect(
302                                    "No angle information for the proper rotation can be found.",
303                                ), // prioritise small rotation angle
304                        )
305                    })
306                    .expect("Unable to obtain a representative element index.");
307                let rep_ele = self.get_index(rep_ele_index).unwrap_or_else(|| {
308                    panic!("Unable to retrieve group element with index `{rep_ele_index}`.")
309                });
310
311                let su2 = if rep_ele.is_su2_class_1() {
312                    "(QΣ)"
313                } else if rep_ele.is_su2() {
314                    "(Σ)"
315                } else {
316                    ""
317                };
318                if rep_ele.is_identity() {
319                    // E(Σ) and E(QΣ) cannot be in the same conjugacy class.
320                    let id_sym = SymmetryClassSymbol::new(
321                        format!("1||E{su2}||").as_str(),
322                        Some(vec![rep_ele]),
323                    )
324                    .unwrap_or_else(|_| {
325                        panic!("Unable to construct a class symbol from `1||E{su2}||`.")
326                    });
327                    assert!(undashed_class_symbols.insert(id_sym.clone(), 1).is_none());
328                    id_sym
329                } else if rep_ele.is_inversion() {
330                    // i(Σ) and i(QΣ) cannot be in the same conjugacy class.
331                    let inv_sym = SymmetryClassSymbol::new(
332                        format!("1||i{su2}||").as_str(),
333                        Some(vec![rep_ele]),
334                    )
335                    .unwrap_or_else(|_| {
336                        panic!("Unable to construct a class symbol from `1||i{su2}||`.")
337                    });
338                    assert!(undashed_class_symbols.insert(inv_sym.clone(), 1).is_none());
339                    inv_sym
340                } else if rep_ele.is_time_reversal() {
341                    // θ(Σ) and θ(QΣ) cannot be in the same conjugacy class.
342                    let trev_sym = SymmetryClassSymbol::new(
343                        format!("1||θ{su2}||").as_str(),
344                        Some(vec![rep_ele]),
345                    )
346                    .unwrap_or_else(|_| {
347                        panic!("Unable to construct a class symbol from `1||θ{su2}||`.")
348                    });
349                    assert!(undashed_class_symbols.insert(trev_sym.clone(), 1).is_none());
350                    trev_sym
351                } else {
352                    let (mult, main_symbol, reps) = if rep_ele.is_su2() && !rep_ele.is_su2_class_1()
353                    {
354                        // This class might contain both class-0 and class-1 elements.
355                        let class_1_count = self
356                            .get_cc_index(i)
357                            .unwrap_or_else(|| {
358                                panic!("No conjugacy class index `{i}` can be found.")
359                            })
360                            .iter()
361                            .filter(|&j| {
362                                let op = self.get_index(*j).unwrap_or_else(|| {
363                                    panic!("Element with index {j} cannot be retrieved.")
364                                });
365                                op.is_su2_class_1()
366                            })
367                            .count();
368                        if old_symbol.size().rem_euclid(2) == 0
369                            && class_1_count == old_symbol.size().div_euclid(2)
370                        {
371                            // Both class-0 and class-1 elements occur in equal number. We show one
372                            // of each and halve the multiplicity.
373                            let class_1_rep_ele = self
374                                .get_cc_index(i)
375                                .unwrap_or_else(|| {
376                                    panic!("No conjugacy class index `{i}` can be found.")
377                                })
378                                .iter()
379                                .find_map(|&j| {
380                                    let op = self.get_index(j).unwrap_or_else(|| {
381                                        panic!("Element with index {j} cannot be retrieved.")
382                                    });
383                                    if op.is_su2_class_1() {
384                                        Some(op)
385                                    } else {
386                                        None
387                                    }
388                                })
389                                .expect("Unable to find a class-1 element in this class.");
390                            (
391                                old_symbol.size().div_euclid(2),
392                                format!(
393                                    "{}, {}",
394                                    rep_ele.get_abbreviated_symbol(),
395                                    class_1_rep_ele.get_abbreviated_symbol()
396                                ),
397                                vec![rep_ele, class_1_rep_ele],
398                            )
399                        } else if class_1_count > 0 {
400                            // Both class-0 and class-1 elements occur, but not in equal numbers.
401                            // We show all of them and set the multiplicity to 1.
402                            let ops = self
403                                .get_cc_index(i)
404                                .unwrap_or_else(|| {
405                                    panic!("No conjugacy class index `{i}` can be found.")
406                                })
407                                .iter()
408                                .map(|&j| {
409                                    self.get_index(j).unwrap_or_else(|| {
410                                        panic!("Element with index {j} cannot be retrieved.")
411                                    })
412                                })
413                                .collect_vec();
414                            (
415                                1,
416                                ops.iter().map(|op| op.get_abbreviated_symbol()).join(", "),
417                                ops,
418                            )
419                        } else {
420                            // Only class-0 elements occur.
421                            (
422                                old_symbol.size(),
423                                rep_ele.get_abbreviated_symbol(),
424                                vec![rep_ele],
425                            )
426                        }
427                    } else {
428                        // Only class-1 elements occur, or no SU2 elements at all.
429                        (
430                            old_symbol.size(),
431                            rep_ele.get_abbreviated_symbol(),
432                            vec![rep_ele],
433                        )
434                    };
435                    let undashed_sym =
436                        SymmetryClassSymbol::new(format!("1||{main_symbol}||").as_str(), None)
437                            .unwrap_or_else(|_| {
438                                panic!(
439                                    "Unable to construct a coarse class symbol from `1||{main_symbol}||`"
440                                )
441                            });
442                    undashed_class_symbols
443                        .entry(undashed_sym.clone())
444                        .and_modify(|counter| *counter += 1)
445                        .or_insert(1);
446                    let dash = undashed_class_symbols
447                        .get(&undashed_sym)
448                        .map(|&counter| "'".repeat(counter - 1))
449                        .unwrap();
450
451                    SymmetryClassSymbol::new(
452                        format!("{mult}||{main_symbol}|^({dash})|",).as_str(),
453                        Some(reps),
454                    )
455                    .unwrap_or_else(|_| {
456                        panic!(
457                            "Unable to construct a class symbol from `{mult}||{main_symbol}|^({dash})|`"
458                        )
459                    })
460                }
461            })
462            .collect::<Vec<_>>();
463        log::debug!("Assigning class symbols from symmetry operations... Done.");
464        symmetry_class_symbols
465    }
466}
467
468// =====================
469// Trait implementations
470// =====================
471
472// -----------------------
473// UnitaryRepresentedGroup
474// -----------------------
475
476impl SymmetryGroupProperties
477    for UnitaryRepresentedGroup<
478        SymmetryOperation,
479        MullikenIrrepSymbol,
480        SymmetryClassSymbol<SymmetryOperation>,
481    >
482{
483    /// Constructs a unitary-represented group from molecular symmetry *elements* (not operations).
484    ///
485    /// # Arguments
486    ///
487    /// * `sym` - A molecular symmetry struct.
488    /// * `infinite_order_to_finite` - Interpret infinite-order generating
489    /// elements as finite-order generating elements to create a finite subgroup
490    /// of an otherwise infinite group.
491    ///
492    /// # Returns
493    ///
494    /// A unitary-represented group of the symmetry operations generated by `sym`.
495    #[allow(clippy::too_many_lines)]
496    fn from_molecular_symmetry(
497        sym: &Symmetry,
498        infinite_order_to_finite: Option<u32>,
499    ) -> Result<Self, anyhow::Error> {
500        let group_name = sym
501            .group_name
502            .as_ref()
503            .expect("No point groups found.")
504            .clone();
505
506        let handles_infinite_group = if sym.is_infinite() {
507            assert!(
508                infinite_order_to_finite.is_some(),
509                "No finite orders specified for an infinite-order group."
510            );
511            infinite_order_to_finite
512        } else {
513            None
514        };
515
516        let sorted_operations = sym.generate_all_operations(infinite_order_to_finite);
517
518        let mut group = Self::new(group_name.as_str(), sorted_operations)?;
519        if handles_infinite_group.is_some() {
520            let finite_subgroup_name = group.deduce_finite_group_name();
521            group.set_finite_subgroup_name(Some(finite_subgroup_name));
522        }
523        let symbols = group.class_symbols_from_symmetry();
524        group.set_class_symbols(&symbols);
525        group.construct_irrep_character_table();
526        group.canonicalise_character_table();
527        Ok(group)
528    }
529
530    /// Constructs the double group of this unitary-represented group.
531    ///
532    /// # Returns
533    ///
534    /// The unitary-represented double group.
535    fn to_double_group(&self) -> Result<Self, anyhow::Error> {
536        log::debug!(
537            "Constructing the double group for unitary-represented {}...",
538            self.name()
539        );
540
541        // Check for classes of multiple C2 axes.
542        let poshem = find_positive_hemisphere(self);
543
544        if let Some(pos_hem) = poshem.as_ref() {
545            log::debug!("New positive hemisphere:");
546            log::debug!("{pos_hem}");
547        }
548
549        let mut su2_operations = self
550            .elements()
551            .clone()
552            .into_iter()
553            .map(|op| {
554                let mut su2_op = op.to_su2_class_0();
555                su2_op.set_positive_hemisphere(poshem.as_ref());
556                su2_op
557            })
558            .collect_vec();
559        let q_identity = SymmetryOperation::from_quaternion(
560            (-1.0, -Vector3::z()),
561            true,
562            su2_operations[0].generating_element.threshold(),
563            1,
564            false,
565            true,
566            poshem,
567        );
568        let su2_1_operations = su2_operations
569            .iter()
570            .map(|op| {
571                let mut q_op = op * &q_identity;
572                if !q_op.is_proper() {
573                    q_op = q_op.convert_to_improper_kind(&SIG);
574                }
575
576                // Multiplying by q_identity does not change subscript/superscript information
577                // such as inversion parity or mirror plane type.
578                q_op.generating_element.additional_subscript =
579                    op.generating_element.additional_subscript.clone();
580                q_op.generating_element.additional_superscript =
581                    op.generating_element.additional_superscript.clone();
582                q_op
583            })
584            .collect_vec();
585        su2_operations.extend(su2_1_operations.into_iter());
586        sort_operations(&mut su2_operations);
587
588        let group_name = if self.name().contains('+') {
589            format!("({})*", self.name())
590        } else {
591            self.name() + "*"
592        };
593        let finite_group_name = self.finite_subgroup_name().map(|name| {
594            if name.contains('+') {
595                format!("({name})*")
596            } else {
597                name.clone() + "*"
598            }
599        });
600        let mut group = Self::new(group_name.as_str(), su2_operations)?;
601        group.set_finite_subgroup_name(finite_group_name);
602        let symbols = group.class_symbols_from_symmetry();
603        group.set_class_symbols(&symbols);
604        group.construct_irrep_character_table();
605        group.canonicalise_character_table();
606        Ok(group)
607    }
608
609    /// Reorders and relabels the rows and columns of the constructed character table using
610    /// Mulliken conventions for the irreducible representations.
611    fn canonicalise_character_table(&mut self) {
612        let old_chartab = self.character_table();
613        let class_symbols: IndexMap<_, _> = (0..self.class_number())
614            .map(|i| (self.get_cc_symbol_of_index(i).unwrap(), i))
615            .collect();
616
617        let su2_0 = if self.is_double_group() { "(Σ)" } else { "" };
618        let i_cc = SymmetryClassSymbol::new(format!("1||i{su2_0}||").as_str(), None)
619            .unwrap_or_else(|_| panic!("Unable to construct a class symbol from `1||i{su2_0}||`."));
620        let s_cc = SymmetryClassSymbol::new(format!("1||σh{su2_0}||").as_str(), None)
621            .unwrap_or_else(|_| {
622                panic!("Unable to construct a class symbol from `1||σh{su2_0}||`.")
623            });
624        let s2_cc = SymmetryClassSymbol::new("1||σh(Σ), σh(QΣ)||".to_string().as_str(), None)
625            .unwrap_or_else(|_| {
626                panic!("Unable to construct a class symbol from `1||σh(Σ), σh(QΣ)||`.")
627            });
628        let ts_cc = SymmetryClassSymbol::new(format!("1||θ·σh{su2_0}||").as_str(), None)
629            .unwrap_or_else(|_| panic!("Unable to construct class symbol `1||θ·σh{su2_0}||`."));
630
631        let force_principal = if FORCED_C3_PRINCIPAL_GROUPS.contains(&self.name())
632            || FORCED_C3_PRINCIPAL_GROUPS.contains(
633                self.finite_subgroup_name()
634                    .unwrap_or(&String::new())
635                    .as_str(),
636            ) {
637            let c3_cc: SymmetryClassSymbol<SymmetryOperation> =
638                SymmetryClassSymbol::new(format!("8||C3{su2_0}||").as_str(), None).unwrap_or_else(
639                    |_| panic!("Unable to construct a class symbol from `8||C3{su2_0}||`."),
640                );
641            log::debug!(
642                "Group is {}. Principal-axis classes will be forced to be {}. This is to obtain non-standard Mulliken symbols that are in line with conventions in the literature.",
643                self.name(),
644                c3_cc
645            );
646            Some(c3_cc)
647        } else {
648            None
649        };
650
651        let principal_classes = if force_principal.is_some() {
652            deduce_principal_classes(
653                &class_symbols,
654                None::<fn(&SymmetryClassSymbol<SymmetryOperation>) -> bool>,
655                force_principal,
656            )
657        } else if class_symbols.contains_key(&i_cc) {
658            log::debug!(
659                "Inversion centre exists. Principal-axis classes will be forced to be proper."
660            );
661            deduce_principal_classes(
662                &class_symbols,
663                Some(|cc: &SymmetryClassSymbol<SymmetryOperation>| {
664                    cc.is_proper() && !cc.contains_time_reversal()
665                }),
666                None,
667            )
668        } else if class_symbols.contains_key(&s_cc) || class_symbols.contains_key(&s2_cc) {
669            log::debug!(
670                "Horizontal mirror plane exists. Principal-axis classes will be forced to be proper."
671            );
672            deduce_principal_classes(
673                &class_symbols,
674                Some(|cc: &SymmetryClassSymbol<SymmetryOperation>| {
675                    cc.is_proper() && !cc.contains_time_reversal()
676                }),
677                None,
678            )
679        } else if class_symbols.contains_key(&ts_cc) {
680            log::debug!(
681                "Time-reversed horizontal mirror plane exists. Principal-axis classes will be forced to be proper."
682            );
683            deduce_principal_classes(
684                &class_symbols,
685                Some(|cc: &SymmetryClassSymbol<SymmetryOperation>| {
686                    cc.is_proper() && !cc.contains_time_reversal()
687                }),
688                None,
689            )
690        } else if !self
691            .elements()
692            .iter()
693            .all(|op| !op.contains_time_reversal())
694        {
695            log::debug!(
696                "Antiunitary elements exist without any inversion centres or horizonal mirror planes. Principal-axis classes will be forced to be unitary."
697            );
698            deduce_principal_classes(
699                &class_symbols,
700                Some(|cc: &SymmetryClassSymbol<SymmetryOperation>| !cc.contains_time_reversal()),
701                None,
702            )
703        } else {
704            deduce_principal_classes(
705                &class_symbols,
706                None::<fn(&SymmetryClassSymbol<SymmetryOperation>) -> bool>,
707                None,
708            )
709        };
710
711        let (char_arr, sorted_fs) = sort_irreps(
712            &old_chartab.array().view(),
713            &old_chartab.frobenius_schurs.values().copied().collect_vec(),
714            &class_symbols,
715            &principal_classes,
716        );
717
718        let ordered_irreps =
719            deduce_mulliken_irrep_symbols(&char_arr.view(), &class_symbols, &principal_classes);
720
721        self.irrep_character_table = Some(RepCharacterTable::new(
722            &old_chartab.name,
723            &ordered_irreps,
724            &class_symbols.keys().cloned().collect::<Vec<_>>(),
725            &principal_classes,
726            char_arr,
727            &sorted_fs,
728        ));
729    }
730}
731
732// ------------------------
733// MagneticRepresentedGroup
734// ------------------------
735
736impl SymmetryGroupProperties
737    for MagneticRepresentedGroup<
738        SymmetryOperation,
739        UnitaryRepresentedGroup<
740            SymmetryOperation,
741            MullikenIrrepSymbol,
742            SymmetryClassSymbol<SymmetryOperation>,
743        >,
744        MullikenIrcorepSymbol,
745    >
746{
747    /// Constructs a magnetic-represented group from molecular symmetry *elements* (not operations).
748    ///
749    /// # Arguments
750    ///
751    /// * `sym` - A molecular symmetry struct.
752    /// * `infinite_order_to_finite` - Interpret infinite-order generating
753    /// elements as finite-order generating elements to create a finite subgroup
754    /// of an otherwise infinite group.
755    ///
756    /// # Returns
757    ///
758    /// A magnetic-represented group of the symmetry operations generated by `sym`.
759    ///
760    /// # Panics
761    ///
762    /// Panics if `sym` generates no antiunitary operations.
763    #[allow(clippy::too_many_lines)]
764    fn from_molecular_symmetry(
765        sym: &Symmetry,
766        infinite_order_to_finite: Option<u32>,
767    ) -> Result<Self, anyhow::Error> {
768        let group_name = sym
769            .group_name
770            .as_ref()
771            .expect("No point groups found.")
772            .clone();
773
774        let handles_infinite_group = if sym.is_infinite() {
775            assert!(
776                infinite_order_to_finite.is_some(),
777                "No finite orders specified for an infinite-order group."
778            );
779            infinite_order_to_finite
780        } else {
781            None
782        };
783
784        let sorted_operations = sym.generate_all_operations(infinite_order_to_finite);
785
786        ensure!(
787            sorted_operations
788                .iter()
789                .any(SpecialSymmetryTransformation::contains_time_reversal),
790            "A magnetic-represented group is requested, but no antiunitary operations can be found. \
791            Ensure that time reversal is considered during symmetry-group detection."
792        );
793
794        log::debug!("Constructing the unitary subgroup for the magnetic group...");
795        let unitary_operations = sorted_operations
796            .iter()
797            .filter_map(|op| {
798                if op.contains_time_reversal() {
799                    None
800                } else {
801                    Some(op.clone())
802                }
803            })
804            .collect::<Vec<_>>();
805        let mut unitary_subgroup = UnitaryRepresentedGroup::<
806            SymmetryOperation,
807            MullikenIrrepSymbol,
808            SymmetryClassSymbol<SymmetryOperation>,
809        >::new(&format!("u[{group_name}]"), unitary_operations)?;
810        let uni_symbols = unitary_subgroup.class_symbols_from_symmetry();
811        unitary_subgroup.set_class_symbols(&uni_symbols);
812        if handles_infinite_group.is_some() {
813            let finite_subgroup_name = unitary_subgroup.deduce_finite_group_name();
814            unitary_subgroup.set_finite_subgroup_name(Some(finite_subgroup_name));
815        }
816        unitary_subgroup.construct_irrep_character_table();
817        unitary_subgroup.canonicalise_character_table();
818        log::debug!("Constructing the unitary subgroup for the magnetic group... Done.");
819
820        log::debug!("Constructing the magnetic group...");
821        let mut group = Self::new(group_name.as_str(), sorted_operations, unitary_subgroup)?;
822        if handles_infinite_group.is_some() {
823            let finite_subgroup_name = group.deduce_finite_group_name();
824            group.set_finite_subgroup_name(Some(finite_subgroup_name));
825        }
826        let symbols = group.class_symbols_from_symmetry();
827        group.set_class_symbols(&symbols);
828        group.construct_ircorep_character_table();
829        log::debug!("Constructing the magnetic group... Done.");
830        Ok(group)
831    }
832
833    /// Constructs the double group of this magnetic-represented group.
834    ///
835    /// Note that the unitary subgroup of the magnetic-represented double group is not necessarily
836    /// the same as the double group of the unitary subgroup. This difference can manifest when
837    /// there are binary rotations or reflections and the positive hemisphere of the
838    /// magnetic-represented group might be different from the positive hemisphere of the unitary
839    /// subgroup.
840    ///
841    /// # Returns
842    ///
843    /// The magnetic-represented double group.
844    fn to_double_group(&self) -> Result<Self, anyhow::Error> {
845        log::debug!(
846            "Constructing the double group for magnetic-represented {}...",
847            self.name()
848        );
849
850        // Check for classes of multiple C2 axes.
851        let poshem = find_positive_hemisphere(self);
852
853        if let Some(pos_hem) = poshem.as_ref() {
854            log::debug!("New positive hemisphere:");
855            log::debug!("{pos_hem}");
856        }
857
858        let mut su2_operations = self
859            .elements()
860            .clone()
861            .into_iter()
862            .map(|op| {
863                let mut su2_op = op.to_su2_class_0();
864                su2_op.set_positive_hemisphere(poshem.as_ref());
865                su2_op
866            })
867            .collect_vec();
868        let q_identity = SymmetryOperation::from_quaternion(
869            (-1.0, -Vector3::z()),
870            true,
871            su2_operations[0].generating_element.threshold(),
872            1,
873            false,
874            true,
875            poshem,
876        );
877        let su2_1_operations = su2_operations
878            .iter()
879            .map(|op| {
880                let mut q_op = op * &q_identity;
881                if !q_op.is_proper() {
882                    q_op = q_op.convert_to_improper_kind(&SIG);
883                }
884
885                // Multiplying by q_identity does not change subscript/superscript information
886                // such as inversion parity or mirror plane type.
887                q_op.generating_element.additional_subscript =
888                    op.generating_element.additional_subscript.clone();
889                q_op.generating_element.additional_superscript =
890                    op.generating_element.additional_superscript.clone();
891                q_op
892            })
893            .collect_vec();
894        su2_operations.extend(su2_1_operations.into_iter());
895        sort_operations(&mut su2_operations);
896
897        let group_name = if self.name().contains('+') {
898            format!("({})*", self.name())
899        } else {
900            self.name() + "*"
901        };
902        let finite_group_name = self.finite_subgroup_name().map(|name| {
903            if name.contains('+') {
904                format!("({name})*")
905            } else {
906                name.clone() + "*"
907            }
908        });
909
910        log::debug!("Constructing the double unitary subgroup for the magnetic double group...");
911        let unitary_su2_operations = su2_operations
912            .iter()
913            .filter_map(|op| {
914                if op.contains_time_reversal() {
915                    None
916                } else {
917                    Some(op.clone())
918                }
919            })
920            .collect::<Vec<_>>();
921        let mut double_unitary_subgroup =
922            UnitaryRepresentedGroup::<
923                SymmetryOperation,
924                MullikenIrrepSymbol,
925                SymmetryClassSymbol<SymmetryOperation>,
926            >::new(&format!("u[{group_name}]"), unitary_su2_operations)?;
927        let uni_symbols = double_unitary_subgroup.class_symbols_from_symmetry();
928        double_unitary_subgroup.set_class_symbols(&uni_symbols);
929        if double_unitary_subgroup.name().contains('∞')
930            || double_unitary_subgroup.name().contains("O(3)")
931        {
932            let finite_subgroup_name = double_unitary_subgroup.deduce_finite_group_name();
933            double_unitary_subgroup.set_finite_subgroup_name(Some(finite_subgroup_name));
934        }
935        double_unitary_subgroup.construct_irrep_character_table();
936        double_unitary_subgroup.canonicalise_character_table();
937        log::debug!(
938            "Constructing the double unitary subgroup for the magnetic double group... Done."
939        );
940
941        let mut group = Self::new(group_name.as_str(), su2_operations, double_unitary_subgroup)?;
942        group.set_finite_subgroup_name(finite_group_name);
943        let symbols = group.class_symbols_from_symmetry();
944        group.set_class_symbols(&symbols);
945        group.construct_ircorep_character_table();
946        group.canonicalise_character_table();
947        Ok(group)
948    }
949}
950
951// =================
952// Utility functions
953// =================
954
955/// Finds the custom positive hemisphere for a group such that any classes of odd non-coaxial binary
956/// rotations or reflections have all of their elements have the poles in the positive hemisphere.
957fn find_positive_hemisphere<G>(group: &G) -> Option<PositiveHemisphere>
958where
959    G: GroupProperties<GroupElement = SymmetryOperation>
960        + ClassProperties<ClassSymbol = SymmetryClassSymbol<SymmetryOperation>>,
961{
962    log::debug!("Checking for classes of odd non-coaxial binary rotations or reflections...");
963    let poshem = (0..group.class_number()).find_map(|cc_i| {
964        let cc_symbol = group
965            .get_cc_symbol_of_index(cc_i)
966            .expect("Unable to retrive a conjugacy class symbol.");
967        if cc_symbol.is_spatial_binary_rotation() || cc_symbol.is_spatial_reflection() {
968            let cc = group
969                .get_cc_index(cc_i)
970                .expect("Unable to retrieve a conjugacy class.");
971            if cc.len() > 1 && cc.len().rem_euclid(2) == 1 {
972                let mut all_c2s = cc
973                    .iter()
974                    .map(|&op_i| {
975                        group.get_index(op_i)
976                            .expect("Unable to retrieve a group operation.")
977                    })
978                    .collect_vec();
979                all_c2s.sort_by_key(|c2| {
980                    let (axis_closeness, closest_axis) = c2.generating_element.closeness_to_cartesian_axes();
981                    (OrderedFloat(axis_closeness), closest_axis)
982                });
983                let c2x = all_c2s.first().expect("Unable to retrieve the last C2 operation.");
984                let c20 = all_c2s.last().expect("Unable to retrieve the first C2 operation.");
985                let z_basis = geometry::get_standard_positive_pole(
986                    &c2x
987                        .generating_element
988                        .raw_axis()
989                        .cross(c20.generating_element.raw_axis()),
990                    c2x.generating_element.threshold(),
991                );
992                let x_basis = *c2x.generating_element.raw_axis();
993                log::debug!("Found a class of odd non-coaxial binary rotations or reflections:");
994                for c2 in all_c2s.iter() {
995                    log::debug!("  {c2}");
996                }
997                log::debug!("Adjusting the positive hemisphere to encompass all class-0 binary-rotation or reflection poles...");
998                Some(PositiveHemisphere::new_spherical_disjoint_equatorial_arcs(
999                    z_basis,
1000                    x_basis,
1001                    cc.len(),
1002                ))
1003            } else {
1004                None
1005            }
1006        } else {
1007            None
1008        }
1009    });
1010    if poshem.is_none() {
1011        log::debug!("No classes of odd non-coaxial binary rotations or reflections found.");
1012    }
1013    log::debug!("Checking for classes of odd non-coaxial binary rotations or reflections... Done.");
1014    poshem
1015}