qsym2/symmetry/symmetry_core/
mod.rs

1//! Molecular symmetry elements and their detections.
2
3use std::collections::hash_map::Entry::Vacant;
4use std::collections::{HashMap, HashSet};
5use std::error::Error;
6use std::fmt;
7
8use anyhow::{self, ensure, format_err};
9use derive_builder::Builder;
10use indexmap::IndexSet;
11use itertools::Itertools;
12use log;
13use nalgebra::{Point3, Vector3};
14use rayon::prelude::*;
15use serde::{Deserialize, Serialize};
16
17use crate::auxiliary::atom::Atom;
18use crate::auxiliary::geometry::{self, Transform};
19use crate::auxiliary::molecule::Molecule;
20use crate::rotsym::{self, RotationalSymmetry};
21use crate::symmetry::symmetry_element::symmetry_operation::{
22    SpecialSymmetryTransformation, SymmetryOperation, sort_operations,
23};
24use crate::symmetry::symmetry_element::{
25    AntiunitaryKind, ROT, SIG, SO3, SymmetryElement, SymmetryElementKind, TRROT, TRSIG,
26};
27use crate::symmetry::symmetry_element_order::{ElementOrder, ORDER_1, ORDER_2};
28use crate::symmetry::symmetry_symbols::deduce_sigma_symbol;
29
30#[cfg(test)]
31mod symmetry_core_tests;
32
33#[cfg(test)]
34mod symmetry_group_detection_tests;
35
36#[derive(Debug)]
37pub struct PointGroupDetectionError(pub String);
38
39impl fmt::Display for PointGroupDetectionError {
40    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
41        write!(f, "Point-group detection error: {}.", self.0)
42    }
43}
44
45impl Error for PointGroupDetectionError {}
46
47/// Structure for storing and managing information required for symmetry analysis.
48#[derive(Clone, Builder, Debug, Serialize, Deserialize)]
49pub struct PreSymmetry {
50    /// The original molecule.
51    #[builder(setter(custom))]
52    pub original_molecule: Molecule,
53
54    /// The recentred molecule to be symmetry-analysed.
55    #[builder(setter(custom))]
56    pub recentred_molecule: Molecule,
57
58    /// Threshold for relative comparisons of moments of inertia.
59    #[builder(setter(custom))]
60    pub moi_threshold: f64,
61
62    /// The rotational symmetry of [`Self::recentred_molecule`] based on its moments of
63    /// inertia.
64    #[builder(setter(skip), default = "self.calc_rotational_symmetry()")]
65    pub rotational_symmetry: RotationalSymmetry,
66
67    /// The groups of symmetry-equivalent atoms in [`Self::recentred_molecule`].
68    #[builder(setter(skip), default = "self.calc_sea_groups()")]
69    pub sea_groups: Vec<Vec<Atom>>,
70
71    /// Threshold for relative distance comparisons.
72    #[builder(setter(skip), default = "self.get_dist_threshold()")]
73    pub dist_threshold: f64,
74}
75
76impl PreSymmetryBuilder {
77    /// Initialises the molecule to be symmetry-analysed.
78    ///
79    /// The original molecule and a recentred copy of the molecule will be stored. The recentred
80    /// copy will be used for subsequent symmetry analyses.
81    ///
82    /// # Arguments
83    ///
84    /// * molecule - The molecule to be symmetry-analysed.
85    ///
86    /// # Returns
87    /// A mutable borrow of `[Self]`.
88    pub fn molecule(&mut self, molecule: &Molecule) -> &mut Self {
89        self.recentred_molecule = Some(molecule.recentre());
90        self.original_molecule = Some(molecule.clone());
91        self
92    }
93
94    /// Initialises the threshold for moment-of-inertia comparisons.
95    ///
96    /// # Arguments
97    ///
98    /// * thresh - The threshold for moment-of-inertia comparisons.
99    ///
100    /// # Returns
101    /// A mutable borrow of `[Self]`.
102    pub fn moi_threshold(&mut self, thresh: f64) -> &mut Self {
103        if thresh >= f64::EPSILON {
104            self.moi_threshold = Some(thresh);
105        } else {
106            log::error!(
107                "Threshold value {} is invalid. Threshold must be at least the machine epsilon.",
108                thresh
109            );
110            self.moi_threshold = None;
111        }
112        self
113    }
114
115    /// Calculates the rotational symmetry of [`Self::recentred_molecule`].
116    fn calc_rotational_symmetry(&self) -> RotationalSymmetry {
117        let com = self
118            .recentred_molecule
119            .as_ref()
120            .expect("A molecule has not been set.")
121            .calc_com();
122        let inertia = self
123            .recentred_molecule
124            .as_ref()
125            .expect("A molecule has not been set.")
126            .calc_inertia_tensor(&com);
127        approx::assert_relative_eq!(
128            com,
129            Point3::origin(),
130            epsilon = self
131                .recentred_molecule
132                .as_ref()
133                .expect("A molecule has not been set.")
134                .threshold,
135            max_relative = self
136                .recentred_molecule
137                .as_ref()
138                .expect("A molecule has not been set.")
139                .threshold
140        );
141        rotsym::calc_rotational_symmetry(
142            &inertia,
143            self.moi_threshold.expect("MoI threshold has not been set."),
144        )
145    }
146
147    /// Calculates the symmetry-equivalent groups of the atoms in [`Self::recentred_molecule`].
148    fn calc_sea_groups(&self) -> Vec<Vec<Atom>> {
149        self.recentred_molecule
150            .as_ref()
151            .expect("A molecule has not been set.")
152            .calc_sea_groups()
153    }
154
155    /// Returns the distance threshold of [`Self::recentred_molecule`].
156    fn get_dist_threshold(&self) -> f64 {
157        self.recentred_molecule
158            .as_ref()
159            .expect("A molecule has not been set.")
160            .threshold
161    }
162}
163
164impl PreSymmetry {
165    /// Returns a builder to construct a new pre-symmetry struct.
166    ///
167    /// # Returns
168    ///
169    /// A builder to construct a new pre-symmetry struct.
170    #[must_use]
171    pub(crate) fn builder() -> PreSymmetryBuilder {
172        PreSymmetryBuilder::default()
173    }
174
175    /// Checks for the existence of the proper symmetry element $`C_n`$  or $`\theta C_n`$ along
176    /// `axis` in [`Self::recentred_molecule`].
177    ///
178    /// Non-time-reversed elements are always preferred.
179    ///
180    /// # Arguments
181    ///
182    /// * `order` - The geometrical order $`n`$ of the rotation axis. Only finite
183    ///   orders are supported.
184    /// * `axis` - The rotation axis.
185    /// * `tr` - A flag indicating if time reversal should also be considered in case the
186    ///   non-time-reversed symmetry element does not exist.
187    ///
188    /// # Returns
189    ///
190    /// An [`Option`] containing the proper kind if the $`C_n`$ or $`\theta C_n`$ element exists in
191    /// [`Self::recentred_molecule`]. If not, [`None`] is returned.
192    #[allow(clippy::trivially_copy_pass_by_ref)]
193    fn check_proper(
194        &self,
195        order: &ElementOrder,
196        axis: &Vector3<f64>,
197        tr: bool,
198    ) -> Option<SymmetryElementKind> {
199        assert_ne!(
200            *order,
201            ElementOrder::Inf,
202            "This method does not work for infinite-order elements."
203        );
204        let angle = 2.0 * std::f64::consts::PI / order.to_float();
205        let rotated_mol = self.recentred_molecule.rotate(angle, axis);
206        if rotated_mol == self.recentred_molecule {
207            Some(ROT)
208        } else if tr {
209            let tr_rotated_mol = rotated_mol.reverse_time();
210            if tr_rotated_mol == self.recentred_molecule {
211                Some(TRROT)
212            } else {
213                None
214            }
215        } else {
216            None
217        }
218    }
219
220    /// Checks for the existence of the improper symmetry element $`S_n`$, $`\dot{S}_n`$,
221    /// $`\theta S_n`$, or $`\theta \dot{S}_n`$ along `axis` in [`Self::recentred_molecule`].
222    ///
223    /// Non-time-reversed elements are always preferred.
224    ///
225    /// # Arguments
226    ///
227    /// * `order` - The geometrical order $`n`$ of the improper rotation axis. Only
228    ///   finite orders are supported.
229    /// * `axis` - The rotation axis.
230    /// * `kind` - The convention in which the improper element is defined. The time reversal
231    ///   property of this does not matter.
232    /// * `tr` - A flag indicating if time reversal should also be considered in case the
233    ///   non-time-reversed symmetry element does not exist.
234    ///
235    /// # Returns
236    ///
237    /// An [`Option`] containing the improper kind if the $`S_n`$, $`\theta S_n`$, $`\theta S_n`$,
238    /// or $`\theta \dot{S}_n`$ element exists in [`Self::recentred_molecule`]. If not, [`None`] is
239    /// returned.
240    #[allow(clippy::trivially_copy_pass_by_ref)]
241    fn check_improper(
242        &self,
243        order: &ElementOrder,
244        axis: &Vector3<f64>,
245        kind: &SymmetryElementKind,
246        tr: bool,
247    ) -> Option<SymmetryElementKind> {
248        assert_ne!(
249            *order,
250            ElementOrder::Inf,
251            "This method does not work for infinite-order elements."
252        );
253        let angle = 2.0 * std::f64::consts::PI / order.to_float();
254        let transformed_mol = self.recentred_molecule.improper_rotate(
255            angle,
256            axis,
257            &kind.to_tr(false).try_into().unwrap_or_else(|err| {
258                log::error!("Error detected: {err}.");
259                panic!("Error detected: {err}.")
260            }),
261        );
262        if transformed_mol == self.recentred_molecule {
263            Some(kind.to_tr(false))
264        } else if tr {
265            let tr_transformed_mol = transformed_mol.reverse_time();
266            if tr_transformed_mol == self.recentred_molecule {
267                Some(kind.to_tr(true))
268            } else {
269                None
270            }
271        } else {
272            None
273        }
274    }
275}
276
277/// Structure for storing and managing symmetry analysis results.
278#[derive(Builder, Clone, Debug, Serialize, Deserialize)]
279pub struct Symmetry {
280    /// The determined point group in Schönflies notation.
281    #[builder(setter(skip, strip_option), default = "None")]
282    pub group_name: Option<String>,
283
284    /// The symmetry elements found.
285    ///
286    /// Each entry in the hash map is for one kind of symmetry elements: the key gives the kind,
287    /// and the value is a hash map where each key gives the order and the corresponding value
288    /// gives the [`HashSet`] of the elements with that order.
289    ///
290    /// Note that for improper elements, the mirror-plane convention is preferred.
291    #[builder(setter(skip), default = "HashMap::new()")]
292    pub(crate) elements:
293        HashMap<SymmetryElementKind, HashMap<ElementOrder, IndexSet<SymmetryElement>>>,
294
295    /// The symmetry generators found.
296    ///
297    /// Each entry in the hash map is for one kind of symmetry generators: the key gives the kind,
298    /// and the value is a hash map where each key gives the order and the corresponding value
299    /// gives the [`HashSet`] of the generators with that order.
300    ///
301    /// Note that for improper generatrors, the mirror-plane convention is preferred.
302    #[builder(setter(skip), default = "HashMap::new()")]
303    pub(crate) generators:
304        HashMap<SymmetryElementKind, HashMap<ElementOrder, IndexSet<SymmetryElement>>>,
305}
306
307impl Symmetry {
308    /// Returns a builder to construct a new symmetry struct.
309    ///
310    /// # Returns
311    ///
312    /// A builder to construct a new symmetry struct.
313    #[must_use]
314    fn builder() -> SymmetryBuilder {
315        SymmetryBuilder::default()
316    }
317
318    /// Construct a new and empty symmetry struct.
319    #[must_use]
320    pub fn new() -> Self {
321        Symmetry::builder()
322            .build()
323            .expect("Unable to construct a `Symmetry` structure.")
324    }
325
326    /// Performs point-group detection analysis and populates the fields in this structure with the
327    /// results.
328    ///
329    /// # Arguments
330    ///
331    /// * `presym` - A pre-symmetry-analysis structure containing the molecule and its rotational
332    ///   symmetry required for point-group detection.
333    /// * `tr` - A boolean indicating if time reversal should also be considered. A time-reversed
334    ///   symmetry element will only be considered if its non-time-reversed version turns out to be
335    ///   not a symmetry element.
336    pub fn analyse(&mut self, presym: &PreSymmetry, tr: bool) -> Result<&mut Self, anyhow::Error> {
337        log::debug!("Rotational symmetry found: {}", presym.rotational_symmetry);
338
339        if tr {
340            log::debug!("Antiunitary symmetry generated by time reversal will be considered.");
341        };
342
343        // Add the identity, which must always exist.
344        let c1 = SymmetryElement::builder()
345            .threshold(presym.dist_threshold)
346            .proper_order(ORDER_1)
347            .proper_power(1)
348            .raw_axis(Vector3::new(0.0, 0.0, 1.0))
349            .kind(ROT)
350            .rotation_group(SO3)
351            .build()
352            .map_err(|err| format_err!(err))?;
353        self.add_proper(ORDER_1, c1.raw_axis(), false, presym.dist_threshold, false);
354
355        // Identify all symmetry elements and generators
356        match &presym.rotational_symmetry {
357            RotationalSymmetry::Spherical => self.analyse_spherical(presym, tr)?,
358            RotationalSymmetry::ProlateLinear => self.analyse_linear(presym, tr)?,
359            RotationalSymmetry::OblatePlanar
360            | RotationalSymmetry::OblateNonPlanar
361            | RotationalSymmetry::ProlateNonLinear => self.analyse_symmetric(presym, tr)?,
362            RotationalSymmetry::AsymmetricPlanar | RotationalSymmetry::AsymmetricNonPlanar => {
363                self.analyse_asymmetric(presym, tr)?
364            }
365        }
366
367        if tr {
368            if self.get_elements(&TRROT).is_none()
369                && self.get_elements(&TRSIG).is_none()
370                && self.get_generators(&TRROT).is_none()
371                && self.get_generators(&TRSIG).is_none()
372            {
373                log::debug!(
374                    "Antiunitary symmetry requested, but so far only non-time-reversed elements found."
375                );
376                // Time-reversal requested, but the above analysis gives only non-time-reversed
377                // elements, which means the system must also contain time reversal as a symmetry
378                // operation. This implies that the group is a grey group.
379                if presym.recentred_molecule == presym.recentred_molecule.reverse_time() {
380                    log::debug!("Time reversal is a symmetry element. This is a grey group.");
381                    // Add time-reversed copies of proper elements
382                    self.elements.insert(
383                        TRROT,
384                        self.get_elements(&ROT)
385                            .ok_or_else(|| format_err!("No proper elements found."))?
386                            .iter()
387                            .map(|(order, proper_elements)| {
388                                let tr_proper_elements = proper_elements
389                                    .iter()
390                                    .map(|proper_element| {
391                                        proper_element.to_tr(true)
392                                        // let mut tr_proper_element = proper_element.clone();
393                                        // tr_proper_element.kind = proper_element.kind.to_tr(true);
394                                        // tr_proper_element
395                                    })
396                                    .collect::<IndexSet<_>>();
397                                (*order, tr_proper_elements)
398                            })
399                            .collect::<HashMap<_, _>>(),
400                    );
401                    log::debug!("Time-reversed copies of all proper elements added.");
402
403                    // Add the time-reversal element as a generator
404                    self.add_proper(ORDER_1, c1.raw_axis(), true, presym.dist_threshold, true);
405
406                    // Add time-reversed copies of improper elements, if any
407                    if self.get_elements(&SIG).is_some() {
408                        self.elements.insert(
409                            TRSIG,
410                            self.get_elements(&SIG)
411                                .ok_or_else(|| format_err!("No improper elements found."))?
412                                .iter()
413                                .map(|(order, improper_elements)| {
414                                    let tr_improper_elements = improper_elements
415                                        .iter()
416                                        .map(|improper_element| improper_element.to_tr(true))
417                                        .collect::<IndexSet<_>>();
418                                    (*order, tr_improper_elements)
419                                })
420                                .collect::<HashMap<_, _>>(),
421                        );
422                        log::debug!("Time-reversed copies of all improper elements added.");
423                    }
424
425                    // Rename the group to include the antiunitary coset generated by time reversal
426                    let unitary_group = self
427                        .group_name
428                        .as_ref()
429                        .expect("No point groups found.")
430                        .clone();
431                    self.set_group_name(format!("{unitary_group} + θ·{unitary_group}"));
432                } else {
433                    log::debug!(
434                        "Time reversal is not a symmetry element. This is an ordinary group."
435                    );
436                }
437            } else {
438                log::debug!(
439                    "Antiunitary symmetry requested and some non-time-reversed elements found."
440                );
441                log::debug!(
442                    "Time reversal is not a symmetry element. This is a black-and-white group."
443                );
444            }
445        }
446        Ok(self)
447    }
448
449    /// Adds a proper symmetry element to this struct.
450    ///
451    /// # Arguments
452    ///
453    /// * `order` - The order of the proper symmetry element.
454    /// * `axis` - The axis of rotation of the proper symmetry element.
455    /// * `generator` - A boolean indicating if this element should be added as a generator.
456    /// * `threshold` - A threshold for element comparisons.
457    /// * `tr` - A boolean indicating if time reversal is to be considered.
458    ///
459    /// # Returns
460    ///
461    /// `true` if the specified element is not present and has just been added,
462    /// `false` otherwise.
463    fn add_proper(
464        &mut self,
465        order: ElementOrder,
466        axis: &Vector3<f64>,
467        generator: bool,
468        threshold: f64,
469        tr: bool,
470    ) -> bool {
471        let positive_axis = geometry::get_standard_positive_pole(axis, threshold).normalize();
472        let proper_kind = if tr { TRROT } else { ROT };
473        let element = SymmetryElement::builder()
474            .threshold(threshold)
475            .proper_order(order)
476            .proper_power(1)
477            .raw_axis(positive_axis)
478            .kind(proper_kind)
479            .rotation_group(SO3)
480            .generator(generator)
481            .build()
482            .expect("Unable to construct a proper element.");
483        let simplified_symbol = element.get_simplified_symbol();
484        let full_symbol = element.get_full_symbol();
485        let result = if generator {
486            if let Vacant(proper_generators) = self.generators.entry(proper_kind) {
487                proper_generators.insert(HashMap::from([(order, IndexSet::from([element]))]));
488                true
489            } else {
490                let proper_generators =
491                    self.generators.get_mut(&proper_kind).unwrap_or_else(|| {
492                        panic!(
493                            "{} generators not found.",
494                            if tr { "Time-reversed proper" } else { "Proper" }
495                        )
496                    });
497
498                if let Vacant(proper_generators_order) = proper_generators.entry(order) {
499                    proper_generators_order.insert(IndexSet::from([element]));
500                    true
501                } else {
502                    proper_generators
503                        .get_mut(&order)
504                        .unwrap_or_else(|| {
505                            panic!(
506                                "Proper generators {}C{order} not found.",
507                                if tr { "θ" } else { "" }
508                            )
509                        })
510                        .insert(element)
511                }
512            }
513        } else if let Vacant(proper_elements) = self.elements.entry(proper_kind) {
514            proper_elements.insert(HashMap::from([(order, IndexSet::from([element]))]));
515            true
516        } else {
517            let proper_elements = self.elements.get_mut(&proper_kind).unwrap_or_else(|| {
518                panic!(
519                    "{} elements not found.",
520                    if tr { "Time-reversed proper" } else { "Proper" }
521                )
522            });
523
524            if let Vacant(e) = proper_elements.entry(order) {
525                e.insert(IndexSet::from([element]));
526                true
527            } else {
528                proper_elements
529                    .get_mut(&order)
530                    .unwrap_or_else(|| {
531                        panic!(
532                            "Proper elements {}C{order} not found.",
533                            if tr { "θ" } else { "" }
534                        )
535                    })
536                    .insert(element)
537            }
538        };
539
540        let dest_str = if generator {
541            "generator".to_owned()
542        } else {
543            "element".to_owned()
544        };
545        if result {
546            log::debug!(
547                "Proper rotation {} ({}): {} axis along ({:+.3}, {:+.3}, {:+.3}) added.",
548                dest_str,
549                simplified_symbol,
550                full_symbol,
551                positive_axis[0] + 0.0,
552                positive_axis[1] + 0.0,
553                positive_axis[2] + 0.0,
554            );
555        }
556        result
557    }
558
559    /// Adds an improper symmetry element to this struct.
560    ///
561    /// The improper symmetry element can be defined in whichever convention
562    /// specified by `kind`, but it will always be added in the mirror-plane
563    /// convention.
564    ///
565    /// # Arguments
566    ///
567    /// * `order` - The order of the improper symmetry element in the convention
568    ///   specified by `kind`.
569    /// * `axis` - The axis of the improper symmetry element.
570    /// * `generator` - A flag indicating if this element should be added as a generator.
571    /// * `kind` - The convention in which the improper symmetry element is defined.
572    /// * `sigma` - An optional additional string indicating the type of mirror
573    ///   plane in the case the improper element is a mirror plane.
574    /// * `threshold` - A threshold for element comparisons.
575    /// * `tr` - A boolean indicating if time reversal is to be considered.
576    ///
577    /// # Returns
578    ///
579    /// `true` if the specified element is not present and has just been added,
580    /// `false` otherwise.
581    #[allow(clippy::too_many_lines, clippy::too_many_arguments)]
582    fn add_improper(
583        &mut self,
584        order: ElementOrder,
585        axis: &Vector3<f64>,
586        generator: bool,
587        kind: SymmetryElementKind,
588        sigma: Option<String>,
589        threshold: f64,
590        tr: bool,
591    ) -> bool {
592        let positive_axis = geometry::get_standard_positive_pole(axis, threshold).normalize();
593        let mirror_kind = if tr { TRSIG } else { SIG };
594        let element = if let Some(sigma_str) = sigma {
595            assert!(sigma_str == "d" || sigma_str == "v" || sigma_str == "h");
596            let mut sym_ele = SymmetryElement::builder()
597                .threshold(threshold)
598                .proper_order(order)
599                .proper_power(1)
600                .raw_axis(positive_axis)
601                .kind(kind.to_tr(tr))
602                .rotation_group(SO3)
603                .generator(generator)
604                .build()
605                .expect("Unable to construct an improper symmetry element.")
606                .convert_to_improper_kind(&mirror_kind, false);
607            if *sym_ele.raw_proper_order() == ElementOrder::Int(1) {
608                sym_ele.additional_subscript = sigma_str;
609            }
610            sym_ele
611        } else {
612            SymmetryElement::builder()
613                .threshold(threshold)
614                .proper_order(order)
615                .proper_power(1)
616                .raw_axis(positive_axis)
617                .kind(kind.to_tr(tr))
618                .rotation_group(SO3)
619                .generator(generator)
620                .build()
621                .expect("Unable to construct an improper symmetry element.")
622                .convert_to_improper_kind(&mirror_kind, false)
623        };
624        let order = *element.raw_proper_order();
625        let simplified_symbol = element.get_simplified_symbol();
626        let full_symbol = element.get_full_symbol();
627        let au = if tr {
628            Some(AntiunitaryKind::TimeReversal)
629        } else {
630            None
631        };
632        let is_o3_mirror_plane = element.is_o3_mirror_plane(au);
633        let is_o3_inversion_centre = element.is_o3_inversion_centre(au);
634        let improper_kind = if tr { TRSIG } else { SIG };
635        let result = if generator {
636            if let Vacant(improper_generators) = self.generators.entry(improper_kind) {
637                improper_generators.insert(HashMap::from([(order, IndexSet::from([element]))]));
638                true
639            } else {
640                let improper_generators =
641                    self.generators.get_mut(&improper_kind).unwrap_or_else(|| {
642                        panic!(
643                            "{} generators not found.",
644                            if tr {
645                                "Time-reversed improper"
646                            } else {
647                                "Improper"
648                            }
649                        )
650                    });
651
652                if let Vacant(e) = improper_generators.entry(order) {
653                    e.insert(IndexSet::from([element]));
654                    true
655                } else {
656                    improper_generators
657                        .get_mut(&order)
658                        .unwrap_or_else(|| {
659                            panic!(
660                                "Improper generators {}S{order} not found.",
661                                if tr { "θ" } else { "" }
662                            )
663                        })
664                        .insert(element)
665                }
666            }
667        } else if let Vacant(improper_elements) = self.elements.entry(improper_kind) {
668            improper_elements.insert(HashMap::from([(order, IndexSet::from([element]))]));
669            true
670        } else {
671            let improper_elements = self.elements.get_mut(&improper_kind).unwrap_or_else(|| {
672                panic!(
673                    "{} elements not found.",
674                    if tr {
675                        "Time-reversed improper"
676                    } else {
677                        "Improper"
678                    }
679                )
680            });
681
682            if let Vacant(e) = improper_elements.entry(order) {
683                e.insert(IndexSet::from([element]));
684                true
685            } else {
686                improper_elements
687                    .get_mut(&order)
688                    .unwrap_or_else(|| {
689                        panic!(
690                            "Improper elements {}S{order} not found.",
691                            if tr { "θ" } else { "" }
692                        )
693                    })
694                    .insert(element)
695            }
696        };
697        let dest_str = if generator {
698            "generator".to_owned()
699        } else {
700            "element".to_owned()
701        };
702        if result {
703            if is_o3_mirror_plane {
704                log::debug!(
705                    "Mirror plane {} ({}): {} axis along ({:+.3}, {:+.3}, {:+.3}) added.",
706                    dest_str,
707                    simplified_symbol,
708                    full_symbol,
709                    positive_axis[0] + 0.0,
710                    positive_axis[1] + 0.0,
711                    positive_axis[2] + 0.0,
712                );
713            } else if is_o3_inversion_centre {
714                log::debug!(
715                    "Inversion centre {} ({}): {} axis along ({:+.3}, {:+.3}, {:+.3}) added.",
716                    dest_str,
717                    simplified_symbol,
718                    full_symbol,
719                    positive_axis[0] + 0.0,
720                    positive_axis[1] + 0.0,
721                    positive_axis[2] + 0.0,
722                );
723            } else {
724                log::debug!(
725                    "Improper rotation {} ({}): {} axis along ({:+.3}, {:+.3}, {:+.3}) added.",
726                    dest_str,
727                    simplified_symbol,
728                    full_symbol,
729                    positive_axis[0] + 0.0,
730                    positive_axis[1] + 0.0,
731                    positive_axis[2] + 0.0,
732                );
733            }
734        }
735        result
736    }
737
738    /// Sets the name of the symmetry group.
739    ///
740    /// # Arguments
741    ///
742    /// * `name` - The name to be given to the symmetry group.
743    fn set_group_name(&mut self, name: String) {
744        self.group_name = Some(name);
745        log::debug!(
746            "Symmetry group determined: {}",
747            self.group_name.as_ref().expect("No symmetry groups found.")
748        );
749    }
750
751    /// Obtains elements of a particular kind.
752    ///
753    /// # Arguments
754    ///
755    /// * `kind` - An element kind to be obtained.
756    ///
757    /// # Returns
758    ///
759    /// An optional shared reference to the hash map of the required element kind.
760    #[must_use]
761    pub fn get_elements(
762        &self,
763        kind: &SymmetryElementKind,
764    ) -> Option<&HashMap<ElementOrder, IndexSet<SymmetryElement>>> {
765        self.elements.get(kind)
766    }
767
768    /// Obtains elements of a particular kind (mutable).
769    ///
770    /// # Arguments
771    ///
772    /// * `kind` - An element kind to be obtained.
773    ///
774    /// # Returns
775    ///
776    /// An optional exclusive reference to the hash map of the required element kind.
777    #[must_use]
778    pub fn get_elements_mut(
779        &mut self,
780        kind: &SymmetryElementKind,
781    ) -> Option<&mut HashMap<ElementOrder, IndexSet<SymmetryElement>>> {
782        self.elements.get_mut(kind)
783    }
784
785    /// Obtains generators of a particular kind.
786    ///
787    /// # Arguments
788    ///
789    /// * `kind` - A generator kind to be obtained.
790    ///
791    /// # Returns
792    ///
793    /// An optional shared reference to the hash map of the required generator kind.
794    #[must_use]
795    pub fn get_generators(
796        &self,
797        kind: &SymmetryElementKind,
798    ) -> Option<&HashMap<ElementOrder, IndexSet<SymmetryElement>>> {
799        self.generators.get(kind)
800    }
801
802    /// Obtains generators of a particular kind (mutable).
803    ///
804    /// # Arguments
805    ///
806    /// * `kind` - A generator kind to be obtained.
807    ///
808    /// # Returns
809    ///
810    /// An optional exclusive reference to the hash map of the required generator kind.
811    #[must_use]
812    pub fn get_generators_mut(
813        &mut self,
814        kind: &SymmetryElementKind,
815    ) -> Option<&mut HashMap<ElementOrder, IndexSet<SymmetryElement>>> {
816        self.generators.get_mut(kind)
817    }
818
819    /// Obtains mirror-plane elements by their type (`"h"`, `"v"`, `"d"`, or `""`), including both
820    /// time-reversed and non-time-reversed variants.
821    ///
822    /// # Arguments
823    ///
824    /// * `sigma` - The mirror-plane type which is one of `"h"`, `"v"`, `"d"`, or `""`.
825    ///
826    /// # Returns
827    ///
828    /// An option containing the set of the required mirror-plane element type, if exists. If not,
829    /// then `None` is returned.
830    #[must_use]
831    pub fn get_sigma_elements(&self, sigma: &str) -> Option<HashSet<&SymmetryElement>> {
832        self.get_improper(&ORDER_1)
833            .map(|sigma_elements| {
834                sigma_elements
835                    .iter()
836                    .filter_map(|ele| {
837                        if ele.additional_subscript == sigma {
838                            Some(*ele)
839                        } else {
840                            None
841                        }
842                    })
843                    .collect::<HashSet<_>>()
844            })
845            .filter(|sigma_elements| !sigma_elements.is_empty())
846    }
847
848    /// Obtains mirror-plane generators by their type (`"h"`, `"v"`, `"d"`, or `""`), including both
849    /// time-reversed and non-time-reversed variants.
850    ///
851    /// # Arguments
852    ///
853    /// * `sigma` - The mirror-plane type which is one of `"h"`, `"v"`, `"d"`, or `""`.
854    ///
855    /// # Returns
856    ///
857    /// A set of the required mirror-plane generator type, if exists.
858    #[must_use]
859    pub fn get_sigma_generators(&self, sigma: &str) -> Option<HashSet<&SymmetryElement>> {
860        let mut sigma_generators: HashSet<&SymmetryElement> = HashSet::new();
861        if let Some(improper_generators) = self.get_generators(&SIG)
862            && let Some(sigmas) = improper_generators.get(&ORDER_1)
863        {
864            sigma_generators.extend(
865                sigmas
866                    .iter()
867                    .filter(|ele| ele.additional_subscript == sigma),
868            );
869        }
870        if let Some(tr_improper_generators) = self.get_generators(&TRSIG)
871            && let Some(sigmas) = tr_improper_generators.get(&ORDER_1)
872        {
873            sigma_generators.extend(
874                sigmas
875                    .iter()
876                    .filter(|ele| ele.additional_subscript == sigma),
877            );
878        }
879        if sigma_generators.is_empty() {
880            None
881        } else {
882            Some(sigma_generators)
883        }
884    }
885
886    /// Obtains the highest proper rotation order.
887    ///
888    /// # Returns
889    ///
890    /// The highest proper rotation order.
891    #[must_use]
892    pub fn get_max_proper_order(&self) -> ElementOrder {
893        *self
894            .get_generators(&ROT)
895            .unwrap_or(&HashMap::new())
896            .keys()
897            .chain(self.get_elements(&ROT).unwrap_or(&HashMap::new()).keys())
898            .chain(
899                self.get_generators(&TRROT)
900                    .unwrap_or(&HashMap::new())
901                    .keys(),
902            )
903            .chain(self.get_elements(&TRROT).unwrap_or(&HashMap::new()).keys())
904            .max()
905            .expect("No highest proper rotation order could be obtained.")
906    }
907
908    /// Obtains all proper elements of a certain order (both time-reversed and non-time-reversed).
909    ///
910    /// # Arguments
911    ///
912    /// * `order` - The required order of elements.
913    ///
914    /// # Returns
915    ///
916    /// An optional hash set of proper elements of the required order. If no such elements exist,
917    /// `None` will be returned.
918    #[must_use]
919    pub fn get_proper(&self, order: &ElementOrder) -> Option<HashSet<&SymmetryElement>> {
920        let opt_proper_elements = self
921            .get_elements(&ROT)
922            .map(|proper_elements| proper_elements.get(order))
923            .unwrap_or_default();
924        let opt_tr_proper_elements = self
925            .get_elements(&TRROT)
926            .map(|tr_proper_elements| tr_proper_elements.get(order))
927            .unwrap_or_default();
928
929        match (opt_proper_elements, opt_tr_proper_elements) {
930            (None, None) => None,
931            (Some(proper_elements), None) => Some(proper_elements.iter().collect::<HashSet<_>>()),
932            (None, Some(tr_proper_elements)) => {
933                Some(tr_proper_elements.iter().collect::<HashSet<_>>())
934            }
935            (Some(proper_elements), Some(tr_proper_elements)) => Some(
936                proper_elements
937                    .iter()
938                    .chain(tr_proper_elements.iter())
939                    .collect::<HashSet<_>>(),
940            ),
941        }
942    }
943
944    /// Obtains all improper elements of a certain order (both time-reversed and non-time-reversed).
945    ///
946    /// # Arguments
947    ///
948    /// * `order` - The required order of elements.
949    ///
950    /// # Returns
951    ///
952    /// An optional hash set of improper elements of the required order. If no such elements exist,
953    /// `None` will be returned.
954    #[must_use]
955    pub fn get_improper(&self, order: &ElementOrder) -> Option<HashSet<&SymmetryElement>> {
956        let opt_improper_elements = self
957            .get_elements(&SIG)
958            .map(|improper_elements| improper_elements.get(order))
959            .unwrap_or_default();
960        let opt_tr_improper_elements = self
961            .get_elements(&TRSIG)
962            .map(|tr_improper_elements| tr_improper_elements.get(order))
963            .unwrap_or_default();
964
965        match (opt_improper_elements, opt_tr_improper_elements) {
966            (None, None) => None,
967            (Some(improper_elements), None) => {
968                Some(improper_elements.iter().collect::<HashSet<_>>())
969            }
970            (None, Some(tr_improper_elements)) => {
971                Some(tr_improper_elements.iter().collect::<HashSet<_>>())
972            }
973            (Some(improper_elements), Some(tr_improper_elements)) => Some(
974                improper_elements
975                    .iter()
976                    .chain(tr_improper_elements.iter())
977                    .collect::<HashSet<_>>(),
978            ),
979        }
980    }
981
982    /// Obtains a proper principal element, *i.e.* a time-reversed or non-time-reversed proper
983    /// element with the highest order.
984    ///
985    /// If there are several such elements, the element to be returned will be randomly chosen but
986    /// with any non-time-reversed ones prioritised.
987    ///
988    /// # Returns
989    ///
990    /// A proper principal element.
991    ///
992    /// # Panics
993    ///
994    /// Panics if no proper elements or generators can be found.
995    #[must_use]
996    pub fn get_proper_principal_element(&self) -> &SymmetryElement {
997        let max_ord = self.get_max_proper_order();
998        let principal_elements = self.get_proper(&max_ord).unwrap_or_else(|| {
999            let opt_proper_generators = self
1000                .get_generators(&ROT)
1001                .map(|proper_generators| proper_generators.get(&max_ord))
1002                .unwrap_or_default();
1003            let opt_tr_proper_generators = self
1004                .get_elements(&TRROT)
1005                .map(|tr_proper_generators| tr_proper_generators.get(&max_ord))
1006                .unwrap_or_default();
1007
1008            match (opt_proper_generators, opt_tr_proper_generators) {
1009                (None, None) => panic!("No proper elements found."),
1010                (Some(proper_generators), None) => proper_generators.iter().collect::<HashSet<_>>(),
1011                (None, Some(tr_proper_generators)) => {
1012                    tr_proper_generators.iter().collect::<HashSet<_>>()
1013                }
1014                (Some(proper_generators), Some(tr_proper_generators)) => proper_generators
1015                    .iter()
1016                    .chain(tr_proper_generators.iter())
1017                    .collect::<HashSet<_>>(),
1018            }
1019        });
1020        principal_elements
1021            .iter()
1022            .find(|ele| !ele.contains_time_reversal())
1023            .unwrap_or_else(|| {
1024                principal_elements
1025                    .iter()
1026                    .next()
1027                    .expect("No proper principal elements found.")
1028            })
1029    }
1030
1031    /// Determines if this group is an infinite group.
1032    ///
1033    /// # Returns
1034    ///
1035    /// A boolean indicating if this group is an infinite group.
1036    #[must_use]
1037    pub fn is_infinite(&self) -> bool {
1038        self.get_max_proper_order() == ElementOrder::Inf
1039            || *self
1040                .get_generators(&ROT)
1041                .unwrap_or(&HashMap::new())
1042                .keys()
1043                .chain(self.get_elements(&ROT).unwrap_or(&HashMap::new()).keys())
1044                .chain(
1045                    self.get_generators(&TRROT)
1046                        .unwrap_or(&HashMap::new())
1047                        .keys(),
1048                )
1049                .chain(self.get_elements(&TRROT).unwrap_or(&HashMap::new()).keys())
1050                .max()
1051                .unwrap_or(&ElementOrder::Int(0))
1052                == ElementOrder::Inf
1053    }
1054
1055    /// Returns the total number of symmetry elements (*NOT* symmetry operations). In
1056    /// infinite-order groups, this is the sum of the number of discrete symmetry elements and the
1057    /// number of discrete symmetry generators.
1058    pub fn n_elements(&self) -> usize {
1059        let n_elements = self
1060            .elements
1061            .values()
1062            .flat_map(|kind_elements| kind_elements.values())
1063            .flatten()
1064            .count();
1065        if self.is_infinite() {
1066            n_elements
1067                + self
1068                    .generators
1069                    .values()
1070                    .flat_map(|kind_elements| kind_elements.values())
1071                    .flatten()
1072                    .count()
1073        } else {
1074            n_elements
1075        }
1076    }
1077
1078    /// Generates all possible symmetry operations from the available symmetry elements.
1079    ///
1080    /// # Arguments
1081    ///
1082    /// * `infinite_order_to_finite` - A finite order to interpret infinite-order generators of
1083    ///   infinite groups.
1084    ///
1085    /// # Returns
1086    ///
1087    /// A vector of generated symmetry operations.
1088    ///
1089    /// # Panics
1090    ///
1091    /// Panics if the group is infinite but `infinite_order_to_finite` is `None`, or if the finite
1092    /// order specified in `infinite_order_to_finite` is incompatible with the infinite group.
1093    #[allow(clippy::too_many_lines)]
1094    pub fn generate_all_operations(
1095        &self,
1096        infinite_order_to_finite: Option<u32>,
1097    ) -> Vec<SymmetryOperation> {
1098        let handles_infinite_group = if self.is_infinite() {
1099            assert!(infinite_order_to_finite.is_some());
1100            infinite_order_to_finite
1101        } else {
1102            None
1103        };
1104
1105        if let Some(finite_order) = handles_infinite_group {
1106            let group_name = self.group_name.as_ref().expect("Group name not found.");
1107            if group_name.contains("O(3)") {
1108                if !matches!(finite_order, 2 | 4) {
1109                    log::error!(
1110                        "Finite order of `{finite_order}` is not yet supported for `{group_name}`."
1111                    );
1112                }
1113                assert!(
1114                    matches!(finite_order, 2 | 4),
1115                    "Finite order of `{finite_order}` is not yet supported for `{group_name}`."
1116                );
1117            }
1118        }
1119
1120        let id_element = self
1121            .get_elements(&ROT)
1122            .unwrap_or(&HashMap::new())
1123            .get(&ORDER_1)
1124            .expect("No identity elements found.")
1125            .iter()
1126            .next()
1127            .expect("No identity elements found.")
1128            .clone();
1129
1130        let id_operation = SymmetryOperation::builder()
1131            .generating_element(id_element)
1132            .power(1)
1133            .build()
1134            .expect("Unable to construct an identity operation.");
1135
1136        let empty_elements: HashMap<ElementOrder, IndexSet<SymmetryElement>> = HashMap::new();
1137
1138        // Finite proper operations
1139        let mut proper_orders = self
1140            .get_elements(&ROT)
1141            .unwrap_or(&empty_elements)
1142            .keys()
1143            .collect::<Vec<_>>();
1144        proper_orders.sort_by(|a, b| {
1145            a.partial_cmp(b)
1146                .unwrap_or_else(|| panic!("`{a}` and `{b}` cannot be compared."))
1147        });
1148        let proper_operations =
1149            proper_orders
1150                .iter()
1151                .fold(vec![id_operation], |mut acc, proper_order| {
1152                    self.get_elements(&ROT)
1153                        .unwrap_or(&empty_elements)
1154                        .get(proper_order)
1155                        .unwrap_or_else(|| panic!("Proper elements C{proper_order} not found."))
1156                        .iter()
1157                        .for_each(|proper_element| {
1158                            if let ElementOrder::Int(io) = proper_order {
1159                                acc.extend((1..*io).map(|power| {
1160                                    SymmetryOperation::builder()
1161                                        .generating_element(proper_element.clone())
1162                                        .power(power.try_into().unwrap_or_else(|_| {
1163                                            panic!("Unable to convert `{power}` to `i32`.")
1164                                        }))
1165                                        .build()
1166                                        .expect("Unable to construct a symmetry operation.")
1167                                }));
1168                            }
1169                        });
1170                    acc
1171                });
1172
1173        // Finite proper operations from generators
1174        let proper_operations_from_generators = if let Some(fin_ord) = handles_infinite_group {
1175            self.get_generators(&ROT)
1176                .unwrap_or(&empty_elements)
1177                .par_iter()
1178                .fold(std::vec::Vec::new, |mut acc, (order, proper_generators)| {
1179                    for proper_generator in proper_generators.iter() {
1180                        let finite_order = match order {
1181                            ElementOrder::Int(io) => *io,
1182                            ElementOrder::Inf => fin_ord,
1183                        };
1184                        let finite_proper_element = SymmetryElement::builder()
1185                            .threshold(proper_generator.threshold())
1186                            .proper_order(ElementOrder::Int(finite_order))
1187                            .proper_power(1)
1188                            .raw_axis(*proper_generator.raw_axis())
1189                            .kind(*proper_generator.kind())
1190                            .rotation_group(proper_generator.rotation_group().clone())
1191                            .additional_superscript(proper_generator.additional_superscript.clone())
1192                            .additional_subscript(proper_generator.additional_subscript.clone())
1193                            .build()
1194                            .expect("Unable to construct a symmetry element.");
1195                        acc.extend((1..finite_order).map(|power| {
1196                            SymmetryOperation::builder()
1197                                .generating_element(finite_proper_element.clone())
1198                                .power(power.try_into().unwrap_or_else(|_| {
1199                                    panic!("Unable to convert `{power}` to `i32`.")
1200                                }))
1201                                .build()
1202                                .expect("Unable to construct a symmetry operation.")
1203                        }));
1204                    }
1205                    acc
1206                })
1207                .reduce(std::vec::Vec::new, |mut acc, vec| {
1208                    acc.extend(vec);
1209                    acc
1210                })
1211        } else {
1212            vec![]
1213        };
1214
1215        // Finite time-reversed proper operations
1216        let mut tr_proper_orders = self
1217            .get_elements(&TRROT)
1218            .unwrap_or(&empty_elements)
1219            .keys()
1220            .collect::<Vec<_>>();
1221        tr_proper_orders.sort_by(|a, b| {
1222            a.partial_cmp(b)
1223                .unwrap_or_else(|| panic!("`{a}` and `{b}` cannot be compared."))
1224        });
1225        let tr_proper_operations =
1226            tr_proper_orders
1227                .iter()
1228                .fold(vec![], |mut acc, tr_proper_order| {
1229                    self.get_elements(&TRROT)
1230                        .unwrap_or(&empty_elements)
1231                        .get(tr_proper_order)
1232                        .unwrap_or_else(|| {
1233                            panic!("Proper elements θ·C{tr_proper_order} not found.")
1234                        })
1235                        .iter()
1236                        .for_each(|tr_proper_element| {
1237                            if let ElementOrder::Int(io) = tr_proper_order {
1238                                acc.extend((1..(2 * *io)).step_by(2).map(|power| {
1239                                    SymmetryOperation::builder()
1240                                        .generating_element(tr_proper_element.clone())
1241                                        .power(power.try_into().unwrap_or_else(|_| {
1242                                            panic!("Unable to convert `{power}` to `i32`.")
1243                                        }))
1244                                        .build()
1245                                        .expect("Unable to construct a symmetry operation.")
1246                                }));
1247                            }
1248                        });
1249                    acc
1250                });
1251
1252        // Finite time-reversed proper operations from generators
1253        let tr_proper_operations_from_generators = if let Some(fin_ord) = handles_infinite_group {
1254            self.get_generators(&TRROT)
1255                .unwrap_or(&empty_elements)
1256                .par_iter()
1257                .fold(
1258                    std::vec::Vec::new,
1259                    |mut acc, (order, tr_proper_generators)| {
1260                        for tr_proper_generator in tr_proper_generators.iter() {
1261                            let finite_order = match order {
1262                                ElementOrder::Int(io) => *io,
1263                                ElementOrder::Inf => fin_ord,
1264                            };
1265                            let finite_tr_proper_element = SymmetryElement::builder()
1266                                .threshold(tr_proper_generator.threshold())
1267                                .proper_order(ElementOrder::Int(finite_order))
1268                                .proper_power(1)
1269                                .raw_axis(*tr_proper_generator.raw_axis())
1270                                .kind(*tr_proper_generator.kind())
1271                                .rotation_group(tr_proper_generator.rotation_group().clone())
1272                                .additional_superscript(
1273                                    tr_proper_generator.additional_superscript.clone(),
1274                                )
1275                                .additional_subscript(
1276                                    tr_proper_generator.additional_subscript.clone(),
1277                                )
1278                                .build()
1279                                .expect("Unable to construct a symmetry element.");
1280                            acc.extend((1..finite_order).map(|power| {
1281                                SymmetryOperation::builder()
1282                                    .generating_element(finite_tr_proper_element.clone())
1283                                    .power(power.try_into().unwrap_or_else(|_| {
1284                                        panic!("Unable to convert `{power}` to `i32`.")
1285                                    }))
1286                                    .build()
1287                                    .expect("Unable to construct a symmetry operation.")
1288                            }));
1289                        }
1290                        acc
1291                    },
1292                )
1293                .reduce(std::vec::Vec::new, |mut acc, vec| {
1294                    acc.extend(vec);
1295                    acc
1296                })
1297        } else {
1298            vec![]
1299        };
1300
1301        // Finite improper operations
1302        let mut improper_orders = self
1303            .get_elements(&SIG)
1304            .unwrap_or(&empty_elements)
1305            .keys()
1306            .collect::<Vec<_>>();
1307        improper_orders.sort_by(|a, b| {
1308            a.partial_cmp(b)
1309                .unwrap_or_else(|| panic!("`{a}` and `{b}` cannot be compared."))
1310        });
1311        let improper_operations = improper_orders
1312            .iter()
1313            .fold(vec![], |mut acc, improper_order| {
1314                self.get_elements(&SIG)
1315                    .unwrap_or(&empty_elements)
1316                    .get(improper_order)
1317                    .unwrap_or_else(|| panic!("Improper elements S{improper_order} not found."))
1318                    .iter()
1319                    .for_each(|improper_element| {
1320                        if let ElementOrder::Int(io) = improper_order {
1321                            acc.extend((1..(2 * *io)).step_by(2).map(|power| {
1322                                SymmetryOperation::builder()
1323                                    .generating_element(improper_element.clone())
1324                                    .power(power.try_into().unwrap_or_else(|_| {
1325                                        panic!("Unable to convert `{power}` to `i32`.")
1326                                    }))
1327                                    .build()
1328                                    .expect("Unable to construct a symmetry operation.")
1329                            }));
1330                        }
1331                    });
1332                acc
1333            });
1334
1335        // Finite improper operations from generators
1336        let improper_operations_from_generators = if let Some(fin_ord) = handles_infinite_group {
1337            self.get_generators(&SIG)
1338                .unwrap_or(&empty_elements)
1339                .par_iter()
1340                .fold(
1341                    std::vec::Vec::new,
1342                    |mut acc, (order, improper_generators)| {
1343                        for improper_generator in improper_generators.iter() {
1344                            let finite_order = match order {
1345                                ElementOrder::Int(io) => *io,
1346                                ElementOrder::Inf => fin_ord,
1347                            };
1348                            let finite_improper_element = SymmetryElement::builder()
1349                                .threshold(improper_generator.threshold())
1350                                .proper_order(ElementOrder::Int(finite_order))
1351                                .proper_power(1)
1352                                .raw_axis(*improper_generator.raw_axis())
1353                                .kind(*improper_generator.kind())
1354                                .rotation_group(improper_generator.rotation_group().clone())
1355                                .additional_superscript(
1356                                    improper_generator.additional_superscript.clone(),
1357                                )
1358                                .additional_subscript(
1359                                    improper_generator.additional_subscript.clone(),
1360                                )
1361                                .build()
1362                                .expect("Unable to construct a symmetry element.");
1363                            acc.extend((1..(2 * finite_order)).step_by(2).map(|power| {
1364                                SymmetryOperation::builder()
1365                                    .generating_element(finite_improper_element.clone())
1366                                    .power(power.try_into().unwrap_or_else(|_| {
1367                                        panic!("Unable to convert `{power}` to `i32`.")
1368                                    }))
1369                                    .build()
1370                                    .expect("Unable to construct a symmetry operation.")
1371                            }));
1372                        }
1373                        acc
1374                    },
1375                )
1376                .reduce(std::vec::Vec::new, |mut acc, vec| {
1377                    acc.extend(vec);
1378                    acc
1379                })
1380        } else {
1381            vec![]
1382        };
1383
1384        // Finite time-reversed improper operations
1385        let mut tr_improper_orders = self
1386            .get_elements(&TRSIG)
1387            .unwrap_or(&empty_elements)
1388            .keys()
1389            .collect::<Vec<_>>();
1390        tr_improper_orders.sort_by(|a, b| {
1391            a.partial_cmp(b)
1392                .unwrap_or_else(|| panic!("`{a}` and `{b}` cannot be compared."))
1393        });
1394        let tr_improper_operations =
1395            tr_improper_orders
1396                .iter()
1397                .fold(vec![], |mut acc, tr_improper_order| {
1398                    self.get_elements(&TRSIG)
1399                        .unwrap_or(&empty_elements)
1400                        .get(tr_improper_order)
1401                        .unwrap_or_else(|| {
1402                            panic!("Improper elements θ·S{tr_improper_order} not found.")
1403                        })
1404                        .iter()
1405                        .for_each(|tr_improper_element| {
1406                            if let ElementOrder::Int(io) = tr_improper_order {
1407                                acc.extend((1..(2 * *io)).step_by(2).map(|power| {
1408                                    SymmetryOperation::builder()
1409                                        .generating_element(tr_improper_element.clone())
1410                                        .power(power.try_into().unwrap_or_else(|_| {
1411                                            panic!("Unable to convert `{power}` to `i32`.")
1412                                        }))
1413                                        .build()
1414                                        .expect("Unable to construct a symmetry operation.")
1415                                }));
1416                            }
1417                        });
1418                    acc
1419                });
1420
1421        // Finite time-reversed improper operations from generators
1422        let tr_improper_operations_from_generators = if let Some(fin_ord) = handles_infinite_group {
1423            self.get_generators(&TRSIG)
1424                .unwrap_or(&empty_elements)
1425                .par_iter()
1426                .fold(
1427                    std::vec::Vec::new,
1428                    |mut acc, (order, tr_improper_generators)| {
1429                        for tr_improper_generator in tr_improper_generators.iter() {
1430                            let finite_order = match order {
1431                                ElementOrder::Int(io) => *io,
1432                                ElementOrder::Inf => fin_ord,
1433                            };
1434                            let finite_tr_improper_element = SymmetryElement::builder()
1435                                .threshold(tr_improper_generator.threshold())
1436                                .proper_order(ElementOrder::Int(finite_order))
1437                                .proper_power(1)
1438                                .raw_axis(*tr_improper_generator.raw_axis())
1439                                .kind(*tr_improper_generator.kind())
1440                                .rotation_group(tr_improper_generator.rotation_group().clone())
1441                                .additional_superscript(
1442                                    tr_improper_generator.additional_superscript.clone(),
1443                                )
1444                                .additional_subscript(
1445                                    tr_improper_generator.additional_subscript.clone(),
1446                                )
1447                                .build()
1448                                .expect("Unable to construct a symmetry element.");
1449                            acc.extend((1..(2 * finite_order)).step_by(2).map(|power| {
1450                                SymmetryOperation::builder()
1451                                    .generating_element(finite_tr_improper_element.clone())
1452                                    .power(power.try_into().unwrap_or_else(|_| {
1453                                        panic!("Unable to convert `{power}` to `i32`.")
1454                                    }))
1455                                    .build()
1456                                    .expect("Unable to construct a symmetry operation.")
1457                            }));
1458                        }
1459                        acc
1460                    },
1461                )
1462                .reduce(std::vec::Vec::new, |mut acc, vec| {
1463                    acc.extend(vec);
1464                    acc
1465                })
1466        } else {
1467            vec![]
1468        };
1469
1470        let operations: IndexSet<_> = if handles_infinite_group.is_none() {
1471            proper_operations
1472                .into_iter()
1473                .chain(proper_operations_from_generators)
1474                .chain(improper_operations)
1475                .chain(improper_operations_from_generators)
1476                .chain(tr_proper_operations)
1477                .chain(tr_proper_operations_from_generators)
1478                .chain(tr_improper_operations)
1479                .chain(tr_improper_operations_from_generators)
1480                .collect()
1481        } else {
1482            // Fulfil group closure
1483            log::debug!("Fulfilling closure for a finite subgroup of an infinite group...");
1484            let mut existing_operations: IndexSet<_> = proper_operations
1485                .into_iter()
1486                .chain(proper_operations_from_generators)
1487                .chain(improper_operations)
1488                .chain(improper_operations_from_generators)
1489                .chain(tr_proper_operations)
1490                .chain(tr_proper_operations_from_generators)
1491                .chain(tr_improper_operations)
1492                .chain(tr_improper_operations_from_generators)
1493                .collect();
1494            let mut extra_operations = HashSet::<SymmetryOperation>::new();
1495            let mut npasses = 0;
1496            let mut nstable = 0;
1497
1498            let principal_element = self.get_proper_principal_element();
1499            while nstable < 2 || npasses == 0 {
1500                let n_extra_operations = extra_operations.len();
1501                existing_operations.extend(extra_operations);
1502
1503                npasses += 1;
1504                log::debug!(
1505                    "Generating all group elements: {} pass{}, {} element{} (of which {} {} new)",
1506                    npasses,
1507                    { if npasses > 1 { "es" } else { "" } },
1508                    existing_operations.len(),
1509                    {
1510                        if existing_operations.len() > 1 {
1511                            "s"
1512                        } else {
1513                            ""
1514                        }
1515                    },
1516                    n_extra_operations,
1517                    { if n_extra_operations == 1 { "is" } else { "are" } },
1518                );
1519
1520                extra_operations = existing_operations
1521                    .iter()
1522                    .combinations_with_replacement(2)
1523                    .par_bridge()
1524                    .filter_map(|op_pairs| {
1525                        let op_i_ref = op_pairs[0];
1526                        let op_j_ref = op_pairs[1];
1527                        let op_k = op_i_ref * op_j_ref;
1528                        if existing_operations.contains(&op_k) {
1529                            None
1530                        } else if op_k.is_proper() {
1531                            Some(op_k)
1532                        } else if op_k.is_spatial_reflection()
1533                            && op_k.generating_element.additional_subscript.is_empty()
1534                        {
1535                            if let Some(sigma_symbol) = deduce_sigma_symbol(
1536                                op_k.generating_element.raw_axis(),
1537                                principal_element,
1538                                op_k.generating_element.threshold(),
1539                                false,
1540                            ) {
1541                                let mut op_k_sym = op_k.convert_to_improper_kind(&SIG);
1542                                op_k_sym.generating_element.additional_subscript = sigma_symbol;
1543                                Some(op_k_sym)
1544                            } else {
1545                                Some(op_k.convert_to_improper_kind(&SIG))
1546                            }
1547                        } else {
1548                            Some(op_k.convert_to_improper_kind(&SIG))
1549                        }
1550                    })
1551                    .collect();
1552                if extra_operations.is_empty() {
1553                    nstable += 1;
1554                } else {
1555                    nstable = 0;
1556                }
1557            }
1558
1559            assert_eq!(extra_operations.len(), 0);
1560            log::debug!(
1561                "Group closure reached with {} elements.",
1562                existing_operations.len()
1563            );
1564            existing_operations
1565        };
1566
1567        let mut sorted_operations: Vec<SymmetryOperation> = operations.into_iter().collect();
1568        sort_operations(&mut sorted_operations);
1569        sorted_operations
1570    }
1571}
1572
1573impl Default for Symmetry {
1574    fn default() -> Self {
1575        Self::new()
1576    }
1577}
1578
1579/// Locates all proper rotation elements present in [`PreSymmetry::recentred_molecule`]
1580///
1581/// # Arguments
1582///
1583/// * `presym` - A pre-symmetry-analysis struct containing information about
1584///   the molecular system.
1585/// * `sym` - A symmetry struct to store the proper rotation elements found.
1586/// * `asymmetric` - If `true`, the search assumes that the group is one of the
1587///   Abelian point groups for which the highest possible rotation order is $`2`$
1588///   and there can be at most three $`C_2`$ axes.
1589/// * `tr` - A flag indicating if time reversal should also be considered.
1590#[allow(clippy::too_many_lines)]
1591fn _search_proper_rotations(
1592    presym: &PreSymmetry,
1593    sym: &mut Symmetry,
1594    asymmetric: bool,
1595    tr: bool,
1596) -> Result<(), anyhow::Error> {
1597    log::debug!("==============================");
1598    log::debug!("Proper rotation search begins.");
1599    log::debug!("==============================");
1600    let mut linear_sea_groups: Vec<&Vec<Atom>> = vec![];
1601    let mut count_c2: usize = 0;
1602    log::debug!("++++++++++++++++++++++++++");
1603    log::debug!("SEA group analysis begins.");
1604    log::debug!("++++++++++++++++++++++++++");
1605    for sea_group in &presym.sea_groups {
1606        if asymmetric && count_c2 == 3 {
1607            break;
1608        }
1609        let k_sea = sea_group.len();
1610        match k_sea {
1611            1 => {
1612                continue;
1613            }
1614            2 => {
1615                log::debug!("A linear SEA set detected: {:?}.", sea_group);
1616                linear_sea_groups.push(sea_group);
1617            }
1618            _ => {
1619                let sea_mol = Molecule::from_atoms(sea_group, presym.dist_threshold);
1620                let (sea_mois, sea_axes) = sea_mol.calc_moi();
1621                // Search for high-order rotation axes
1622                if approx::relative_eq!(
1623                    sea_mois[0] + sea_mois[1],
1624                    sea_mois[2],
1625                    epsilon = presym.moi_threshold,
1626                    max_relative = presym.moi_threshold,
1627                ) {
1628                    // Planar SEA
1629                    let k_fac_range: Vec<_> = if approx::relative_eq!(
1630                        sea_mois[0],
1631                        sea_mois[1],
1632                        epsilon = presym.moi_threshold,
1633                        max_relative = presym.moi_threshold,
1634                    ) {
1635                        // Regular k-sided polygon
1636                        log::debug!(
1637                            "A regular {}-sided polygon SEA set detected: {:?}.",
1638                            k_sea,
1639                            sea_group
1640                        );
1641                        let mut divisors = divisors::get_divisors(k_sea);
1642                        divisors.push(k_sea);
1643                        divisors
1644                    } else {
1645                        // Irregular k-sided polygon
1646                        log::debug!(
1647                            "An irregular {}-sided polygon SEA set detected: {:?}.",
1648                            k_sea,
1649                            sea_group
1650                        );
1651                        divisors::get_divisors(k_sea)
1652                    };
1653                    for k_fac in &k_fac_range {
1654                        if let Some(proper_kind) =
1655                            presym.check_proper(
1656                                &ElementOrder::Int((*k_fac).try_into().unwrap_or_else(|_| {
1657                                    panic!("Unable to convert {k_fac} to `u32`.")
1658                                })),
1659                                &sea_axes[2],
1660                                tr,
1661                            )
1662                        {
1663                            match *k_fac {
1664                                2 => {
1665                                    count_c2 += usize::from(sym.add_proper(
1666                                        ElementOrder::Int((*k_fac).try_into().unwrap_or_else(
1667                                            |_| panic!("Unable to convert {k_fac} to `u32`."),
1668                                        )),
1669                                        &sea_axes[2],
1670                                        false,
1671                                        presym.dist_threshold,
1672                                        proper_kind.contains_time_reversal(),
1673                                    ));
1674                                }
1675                                _ => {
1676                                    sym.add_proper(
1677                                        ElementOrder::Int((*k_fac).try_into().unwrap_or_else(
1678                                            |_| panic!("Unable to convert {k_fac} to `u32`."),
1679                                        )),
1680                                        &sea_axes[2],
1681                                        false,
1682                                        presym.dist_threshold,
1683                                        proper_kind.contains_time_reversal(),
1684                                    );
1685                                }
1686                            }
1687                        }
1688                    }
1689                } else {
1690                    // Polyhedral SEA
1691                    if approx::relative_eq!(
1692                        sea_mois[1],
1693                        sea_mois[2],
1694                        epsilon = presym.moi_threshold,
1695                        max_relative = presym.moi_threshold,
1696                    ) {
1697                        // The number of atoms in this SEA group must be even.
1698                        ensure!(
1699                            k_sea % 2 == 0,
1700                            "Unexpected odd number of atoms in this SEA group."
1701                        );
1702                        if approx::relative_eq!(
1703                            sea_mois[0],
1704                            sea_mois[1],
1705                            epsilon = presym.moi_threshold,
1706                            max_relative = presym.moi_threshold,
1707                        ) {
1708                            // Spherical top SEA
1709                            log::debug!("A spherical top SEA set detected: {:?}", sea_group);
1710                            let sea_presym = PreSymmetry::builder()
1711                                .moi_threshold(presym.moi_threshold)
1712                                .molecule(&sea_mol)
1713                                .build()
1714                                .expect("Unable to construct a `PreSymmetry` structure.");
1715                            let mut sea_sym = Symmetry::builder()
1716                                .build()
1717                                .expect("Unable to construct a default `Symmetry` structure.");
1718                            log::debug!("-----------------------------------------------");
1719                            log::debug!("Symmetry analysis for spherical top SEA begins.");
1720                            log::debug!("-----------------------------------------------");
1721                            sea_sym.analyse(&sea_presym, tr)?;
1722                            log::debug!("---------------------------------------------");
1723                            log::debug!("Symmetry analysis for spherical top SEA ends.");
1724                            log::debug!("---------------------------------------------");
1725                            for (order, proper_elements) in sea_sym
1726                                .get_elements(&ROT)
1727                                .unwrap_or(&HashMap::new())
1728                                .iter()
1729                                .chain(
1730                                    sea_sym
1731                                        .get_elements(&TRROT)
1732                                        .unwrap_or(&HashMap::new())
1733                                        .iter(),
1734                                )
1735                            {
1736                                for proper_element in proper_elements {
1737                                    if let Some(proper_kind) =
1738                                        presym.check_proper(order, proper_element.raw_axis(), tr)
1739                                    {
1740                                        sym.add_proper(
1741                                            *order,
1742                                            proper_element.raw_axis(),
1743                                            false,
1744                                            presym.dist_threshold,
1745                                            proper_kind.contains_time_reversal(),
1746                                        );
1747                                    }
1748                                }
1749                            }
1750                        } else {
1751                            // Prolate symmetric top
1752                            log::debug!("A prolate symmetric top SEA set detected.");
1753                            for k_fac in divisors::get_divisors(k_sea / 2)
1754                                .iter()
1755                                .chain([k_sea / 2].iter())
1756                            {
1757                                let k_fac_order =
1758                                    ElementOrder::Int((*k_fac).try_into().unwrap_or_else(|_| {
1759                                        panic!("Unable to convert {k_fac} to u32.")
1760                                    }));
1761                                if let Some(proper_kind) =
1762                                    presym.check_proper(&k_fac_order, &sea_axes[0], tr)
1763                                {
1764                                    if *k_fac == 2 {
1765                                        count_c2 += usize::from(sym.add_proper(
1766                                            k_fac_order,
1767                                            &sea_axes[0],
1768                                            false,
1769                                            presym.dist_threshold,
1770                                            proper_kind.contains_time_reversal(),
1771                                        ));
1772                                    } else {
1773                                        sym.add_proper(
1774                                            k_fac_order,
1775                                            &sea_axes[0],
1776                                            false,
1777                                            presym.dist_threshold,
1778                                            proper_kind.contains_time_reversal(),
1779                                        );
1780                                    }
1781                                }
1782                            }
1783                        }
1784                    } else if approx::relative_eq!(
1785                        sea_mois[0],
1786                        sea_mois[1],
1787                        epsilon = presym.moi_threshold,
1788                        max_relative = presym.moi_threshold,
1789                    ) {
1790                        // Oblate symmetry top
1791                        log::debug!("An oblate symmetric top SEA set detected.");
1792                        ensure!(
1793                            k_sea % 2 == 0,
1794                            "Unexpected odd number of atoms in this SEA group."
1795                        );
1796                        for k_fac in divisors::get_divisors(k_sea / 2)
1797                            .iter()
1798                            .chain([k_sea / 2].iter())
1799                        {
1800                            let k_fac_order =
1801                                ElementOrder::Int((*k_fac).try_into().map_err(|_| {
1802                                    format_err!("Unable to convert `{k_fac}` to `u32`.")
1803                                })?);
1804                            if let Some(proper_kind) =
1805                                presym.check_proper(&k_fac_order, &sea_axes[2], tr)
1806                            {
1807                                if *k_fac == 2 {
1808                                    count_c2 += usize::from(sym.add_proper(
1809                                        k_fac_order,
1810                                        &sea_axes[2],
1811                                        false,
1812                                        presym.dist_threshold,
1813                                        proper_kind.contains_time_reversal(),
1814                                    ));
1815                                } else {
1816                                    sym.add_proper(
1817                                        k_fac_order,
1818                                        &sea_axes[2],
1819                                        false,
1820                                        presym.dist_threshold,
1821                                        proper_kind.contains_time_reversal(),
1822                                    );
1823                                }
1824                            }
1825                        }
1826                    } else {
1827                        // Asymmetric top
1828                        log::debug!("An asymmetric top SEA set detected.");
1829                        for sea_axis in &sea_axes {
1830                            if let Some(proper_kind) = presym.check_proper(&ORDER_2, sea_axis, tr) {
1831                                count_c2 += usize::from(sym.add_proper(
1832                                    ORDER_2,
1833                                    sea_axis,
1834                                    false,
1835                                    presym.dist_threshold,
1836                                    proper_kind.contains_time_reversal(),
1837                                ));
1838                            }
1839                        }
1840                    }
1841                }
1842            }
1843        } // end match k_sea
1844
1845        // Search for any remaining C2 axes
1846        log::debug!("Searching for any remaining C2 axes in this SEA group...");
1847        for atom2s in sea_group.iter().combinations(2) {
1848            if asymmetric && count_c2 == 3 {
1849                log::debug!(
1850                    "Three C2 axes have been found for an asymmetric top. No more C2 axes will be found. The C2 search has completed."
1851                );
1852                break;
1853            }
1854            let atom_i_pos = atom2s[0].coordinates;
1855            let atom_j_pos = atom2s[1].coordinates;
1856
1857            // Case B: C2 might cross through any two atoms
1858            if let Some(proper_kind) = presym.check_proper(&ORDER_2, &atom_i_pos.coords, tr) {
1859                count_c2 += usize::from(sym.add_proper(
1860                    ORDER_2,
1861                    &atom_i_pos.coords,
1862                    false,
1863                    presym.dist_threshold,
1864                    proper_kind.contains_time_reversal(),
1865                ));
1866            }
1867
1868            // Case A: C2 might cross through the midpoint of two atoms
1869            // Care needs to be taken if the midpoint is at the centre of mass of the molecule,
1870            // because then `midvec` would be a zero vector and `c2_check` would be `None`,
1871            // regardless of whether there is actually a C2 axis passing through this midpoint or
1872            // not.
1873            let midvec = 0.5 * (atom_i_pos.coords + atom_j_pos.coords);
1874            let c2_check = presym.check_proper(&ORDER_2, &midvec, tr);
1875            if midvec.norm() > presym.dist_threshold
1876                && let Some(c2) = c2_check
1877            {
1878                count_c2 += usize::from(sym.add_proper(
1879                    ORDER_2,
1880                    &midvec,
1881                    false,
1882                    presym.dist_threshold,
1883                    c2.contains_time_reversal(),
1884                ));
1885            } else if let Some(electric_atoms) = &presym.recentred_molecule.electric_atoms {
1886                let com = presym.recentred_molecule.calc_com();
1887                let e_vector = electric_atoms[0].coordinates - com;
1888                if let Some(proper_kind) = presym.check_proper(&ORDER_2, &e_vector, tr) {
1889                    count_c2 += usize::from(sym.add_proper(
1890                        ORDER_2,
1891                        &e_vector,
1892                        false,
1893                        presym.dist_threshold,
1894                        proper_kind.contains_time_reversal(),
1895                    ));
1896                }
1897            } else if midvec.norm() <= presym.dist_threshold {
1898                for principal_axis in &presym.recentred_molecule.calc_moi().1 {
1899                    if let Some(proper_kind) = presym.check_proper(&ORDER_2, principal_axis, tr) {
1900                        count_c2 += usize::from(sym.add_proper(
1901                            ORDER_2,
1902                            principal_axis,
1903                            false,
1904                            presym.dist_threshold,
1905                            proper_kind.contains_time_reversal(),
1906                        ));
1907                    }
1908                }
1909            }
1910        }
1911        log::debug!("Searching for any remaining C2 axes in this SEA group... Done.");
1912    } // end for sea_group in presym.sea_groups.iter()
1913    log::debug!("++++++++++++++++++++++++");
1914    log::debug!("SEA group analysis ends.");
1915    log::debug!("++++++++++++++++++++++++");
1916
1917    // Search for any remaining C2 axes.
1918    if asymmetric && count_c2 == 3 {
1919    } else {
1920        // Case C: Molecules with two or more sets of non-parallel linear diatomic SEA groups
1921        if linear_sea_groups.len() >= 2 {
1922            let normal_option = linear_sea_groups.iter().combinations(2).find_map(|pair| {
1923                let vec_0 = pair[0][1].coordinates - pair[0][0].coordinates;
1924                let vec_1 = pair[1][1].coordinates - pair[1][0].coordinates;
1925                let trial_normal = vec_0.cross(&vec_1);
1926                if trial_normal.norm() > presym.dist_threshold {
1927                    Some(trial_normal)
1928                } else {
1929                    None
1930                }
1931            });
1932            if let Some(normal) = normal_option
1933                && let Some(proper_kind) = presym.check_proper(&ORDER_2, &normal, tr)
1934            {
1935                sym.add_proper(
1936                    ORDER_2,
1937                    &normal,
1938                    false,
1939                    presym.dist_threshold,
1940                    proper_kind.contains_time_reversal(),
1941                );
1942            }
1943        }
1944    }
1945    log::debug!("============================");
1946    log::debug!("Proper rotation search ends.");
1947    log::debug!("============================");
1948
1949    Ok(())
1950}
1951
1952mod symmetry_core_asymmetric;
1953mod symmetry_core_linear;
1954mod symmetry_core_spherical;
1955mod symmetry_core_symmetric;