qsym2/symmetry/symmetry_core/
symmetry_core_spherical.rs

1//! Molecular symmetry element detection for spherical tops.
2
3use std::collections::HashSet;
4
5use anyhow::{self, ensure, format_err};
6use itertools::{self, Itertools};
7use log;
8use nalgebra::Vector3;
9
10use crate::auxiliary::geometry;
11use crate::rotsym::RotationalSymmetry;
12use crate::symmetry::symmetry_element::SIG;
13use crate::symmetry::symmetry_element_order::{ElementOrder, ORDER_1, ORDER_2};
14
15use super::{PreSymmetry, Symmetry};
16
17impl Symmetry {
18    /// Locates and adds all possible and distinct $`C_2`$ axes present in the
19    /// molecule in `presym`, provided that `presym` is a spherical top.
20    ///
21    /// # Arguments
22    ///
23    /// * `presym` - A pre-symmetry-analysis structure containing information about the molecular
24    ///   system.
25    /// * `tr` - A flag indicating if time reversal should also be considered. A time-reversed
26    ///   symmetry element will only be considered if its non-time-reversed version turns out to be
27    ///   not a symmetry element.
28    ///
29    /// # Returns
30    ///
31    /// The number of distinct $`C_2`$ axes located.
32    fn search_c2_spherical(
33        &mut self,
34        presym: &PreSymmetry,
35        tr: bool,
36    ) -> Result<u32, anyhow::Error> {
37        ensure!(
38            matches!(presym.rotational_symmetry, RotationalSymmetry::Spherical),
39            "Unexpected rotational symmetry -- expected: {}, actual: {}",
40            RotationalSymmetry::Spherical,
41            presym.rotational_symmetry
42        );
43
44        let start_guard: u32 = 30;
45        let stable_c2_ratio: f64 = 0.5;
46        let c2_termination_counts = HashSet::from([3, 9, 15]);
47
48        let mut count_c2: u32 = 0;
49        let mut count_c2_stable: u32 = 0;
50        let mut n_pairs: u32 = 0;
51
52        let sea_groups = &presym.sea_groups;
53        for sea_group in sea_groups.iter() {
54            if sea_group.len() < 2 {
55                continue;
56            }
57            for atom2s in sea_group.iter().combinations(2) {
58                n_pairs += 1;
59                let atom_i_pos = atom2s[0].coordinates;
60                let atom_j_pos = atom2s[1].coordinates;
61
62                // Case B: C2 might cross through any two atoms
63                if let Some(proper_kind) = presym.check_proper(&ORDER_2, &atom_i_pos.coords, tr)
64                    && self.add_proper(
65                        ORDER_2,
66                        &atom_i_pos.coords,
67                        false,
68                        presym.recentred_molecule.threshold,
69                        proper_kind.contains_time_reversal(),
70                    )
71                {
72                    count_c2 += 1;
73                    count_c2_stable = 0;
74                }
75
76                // Case A: C2 might cross through the midpoint of two atoms
77                let midvec = 0.5 * (atom_i_pos.coords + atom_j_pos.coords);
78                if let Some(proper_kind) = presym.check_proper(&ORDER_2, &midvec, tr)
79                    && midvec.norm() > presym.recentred_molecule.threshold
80                    && self.add_proper(
81                        ORDER_2,
82                        &midvec,
83                        false,
84                        presym.recentred_molecule.threshold,
85                        proper_kind.contains_time_reversal(),
86                    )
87                {
88                    count_c2 += 1;
89                    count_c2_stable = 0;
90                }
91
92                // Check if count_c2 has reached stability.
93                if (f64::from(count_c2_stable)) / (f64::from(n_pairs)) > stable_c2_ratio
94                    && n_pairs > start_guard
95                    && c2_termination_counts.contains(&count_c2)
96                {
97                    break;
98                }
99                count_c2_stable += 1;
100            }
101
102            if c2_termination_counts.contains(&count_c2) {
103                break;
104            }
105        }
106        Ok(count_c2)
107    }
108
109    /// Performs point-group detection analysis for a spherical top.
110    ///
111    /// # Arguments
112    ///
113    /// * `presym` - A pre-symmetry-analysis structure containing information about the molecular
114    ///   system.
115    /// * `tr` - A flag indicating if time reversal should also be considered. A time-reversed
116    ///   symmetry element will only be considered if its non-time-reversed version turns out to be
117    ///   not a symmetry element.
118    #[allow(clippy::too_many_lines)]
119    pub(super) fn analyse_spherical(
120        &mut self,
121        presym: &PreSymmetry,
122        tr: bool,
123    ) -> Result<(), anyhow::Error> {
124        ensure!(
125            matches!(presym.rotational_symmetry, RotationalSymmetry::Spherical),
126            "Unexpected rotational symmetry -- expected: {}, actual: {}",
127            RotationalSymmetry::Spherical,
128            presym.rotational_symmetry
129        );
130        if presym.recentred_molecule.atoms.len() == 1 {
131            self.set_group_name("O(3)".to_owned());
132            self.add_proper(
133                ElementOrder::Inf,
134                &Vector3::z(),
135                true,
136                presym.recentred_molecule.threshold,
137                false,
138            );
139            self.add_proper(
140                ElementOrder::Inf,
141                &Vector3::y(),
142                true,
143                presym.recentred_molecule.threshold,
144                false,
145            );
146            self.add_proper(
147                ElementOrder::Inf,
148                &Vector3::x(),
149                true,
150                presym.recentred_molecule.threshold,
151                false,
152            );
153            self.add_improper(
154                ElementOrder::Int(2),
155                &Vector3::z(),
156                true,
157                SIG,
158                None,
159                presym.recentred_molecule.threshold,
160                false,
161            );
162            return Ok(());
163        }
164
165        // Locating all possible and distinct C2 axes
166        let count_c2 = self.search_c2_spherical(presym, tr)?;
167        log::debug!("Located {} C2 axes.", count_c2);
168        ensure!(
169            HashSet::from([3, 9, 15]).contains(&count_c2),
170            "Unexpected number of C2 axes -- expected: 3 or 9 or 15, actual: {count_c2}"
171        );
172
173        // Locating improper elements
174        match count_c2 {
175            3 => {
176                // Tetrahedral, so either T, Td, or Th
177                log::debug!("Tetrahedral family.");
178                if let Some(improper_kind) =
179                    presym.check_improper(&ORDER_2, &Vector3::z(), &SIG, tr)
180                {
181                    // Inversion centre
182                    log::debug!("Located an inversion centre.");
183                    self.set_group_name("Th".to_owned());
184                    ensure!(
185                        self.add_improper(
186                            ORDER_2,
187                            &Vector3::z(),
188                            false,
189                            SIG,
190                            None,
191                            presym.recentred_molecule.threshold,
192                            improper_kind.contains_time_reversal()
193                        ),
194                        "Expected improper element not added."
195                    );
196                    ensure!(
197                        self.add_improper(
198                            ORDER_2,
199                            &Vector3::z(),
200                            true,
201                            SIG,
202                            None,
203                            presym.recentred_molecule.threshold,
204                            improper_kind.contains_time_reversal()
205                        ),
206                        "Expected improper generator not added."
207                    );
208                } else {
209                    let mut c2s = self
210                        .get_proper(&ORDER_2)
211                        .ok_or_else(|| format_err!("No C2 elements found."))?
212                        .into_iter()
213                        .take(2)
214                        .cloned()
215                        .collect_vec()
216                        .into_iter();
217                    let normal = c2s
218                        .next()
219                        .ok_or_else(|| format_err!("No C2 elements found."))?
220                        .raw_axis()
221                        + c2s
222                            .next()
223                            .ok_or_else(|| {
224                                format_err!("Two C2 elements expected, but only one found.")
225                            })?
226                            .raw_axis();
227                    if presym.check_improper(&ORDER_1, &normal, &SIG, tr).is_some() {
228                        // σd
229                        log::debug!("Located σd.");
230                        self.set_group_name("Td".to_owned());
231                        let sigmad_normals = {
232                            let mut axes = vec![];
233                            for c2s in self
234                                .get_proper(&ORDER_2)
235                                .ok_or_else(|| format_err!("No C2 elements found."))?
236                                .iter()
237                                .combinations(2)
238                            {
239                                let axis_p = c2s[0].raw_axis() + c2s[1].raw_axis();
240                                let p_improper_check =
241                                    presym.check_improper(&ORDER_1, &axis_p, &SIG, tr);
242                                ensure!(
243                                    p_improper_check.is_some(),
244                                    "Expected mirror plane perpendicular to {axis_p} not found."
245                                );
246                                axes.push((
247                                    axis_p,
248                                    p_improper_check
249                                        .ok_or_else(|| format_err!("Expected mirror plane perpendicular to {axis_p} not found."))?
250                                        .contains_time_reversal(),
251                                ));
252
253                                let axis_m = c2s[0].raw_axis() - c2s[1].raw_axis();
254                                let m_improper_check =
255                                    presym.check_improper(&ORDER_1, &axis_m, &SIG, tr);
256                                ensure!(
257                                    m_improper_check.is_some(),
258                                    "Expected mirror plane perpendicular to {axis_m} not found."
259                                );
260                                axes.push((
261                                    axis_m,
262                                    m_improper_check
263                                        .ok_or_else(|| format_err!("Expected mirror plane perpendicular to {axis_m} not found."))?
264                                        .contains_time_reversal(),
265                                ));
266                            }
267                            axes
268                        };
269                        let sigmad_generator_normal = sigmad_normals[0];
270                        for (axis, axis_tr) in sigmad_normals {
271                            ensure!(
272                                self.add_improper(
273                                    ORDER_1,
274                                    &axis,
275                                    false,
276                                    SIG,
277                                    Some("d".to_owned()),
278                                    presym.recentred_molecule.threshold,
279                                    axis_tr
280                                ),
281                                "Expected improper element not added."
282                            );
283                        }
284                        ensure!(
285                            self.add_improper(
286                                ORDER_1,
287                                &sigmad_generator_normal.0,
288                                true,
289                                SIG,
290                                Some("d".to_owned()),
291                                presym.recentred_molecule.threshold,
292                                sigmad_generator_normal.1
293                            ),
294                            "Expected improper generator not added."
295                        );
296                    } else {
297                        // No σd => chiral
298                        self.set_group_name("T".to_owned());
299                    }
300                }
301            } // end count_c2 = 3
302            9 => {
303                // 6 C2 and 3 C4^2; Octahedral, so either O or Oh
304                log::debug!("Octahedral family.");
305                if let Some(improper_kind) =
306                    presym.check_improper(&ORDER_2, &Vector3::z(), &SIG, tr)
307                {
308                    // Inversion centre
309                    log::debug!("Located an inversion centre.");
310                    self.set_group_name("Oh".to_owned());
311                    ensure!(
312                        self.add_improper(
313                            ORDER_2,
314                            &Vector3::z(),
315                            false,
316                            SIG,
317                            None,
318                            presym.recentred_molecule.threshold,
319                            improper_kind.contains_time_reversal()
320                        ),
321                        "Expected improper element not added."
322                    );
323                    ensure!(
324                        self.add_improper(
325                            ORDER_2,
326                            &Vector3::z(),
327                            true,
328                            SIG,
329                            None,
330                            presym.recentred_molecule.threshold,
331                            improper_kind.contains_time_reversal()
332                        ),
333                        "Expected improper generator not added."
334                    );
335                } else {
336                    // No inversion centre => chiral
337                    self.set_group_name("O".to_owned());
338                }
339            } // end count_c2 = 9
340            15 => {
341                // Icosahedral, so either I or Ih
342                log::debug!("Icosahedral family.");
343                if let Some(improper_kind) =
344                    presym.check_improper(&ORDER_2, &Vector3::z(), &SIG, tr)
345                {
346                    // Inversion centre
347                    log::debug!("Located an inversion centre.");
348                    self.set_group_name("Ih".to_owned());
349                    ensure!(
350                        self.add_improper(
351                            ORDER_2,
352                            &Vector3::z(),
353                            false,
354                            SIG,
355                            None,
356                            presym.recentred_molecule.threshold,
357                            improper_kind.contains_time_reversal()
358                        ),
359                        "Expected improper element not added."
360                    );
361                    ensure!(
362                        self.add_improper(
363                            ORDER_2,
364                            &Vector3::z(),
365                            true,
366                            SIG,
367                            None,
368                            presym.recentred_molecule.threshold,
369                            improper_kind.contains_time_reversal()
370                        ),
371                        "Expected improper generator not added."
372                    );
373                } else {
374                    // No inversion centre => chiral
375                    self.set_group_name("I".to_owned());
376                }
377            } // end count_c2 = 15
378            _ => return Err(format_err!("Invalid number of C2 axes.")),
379        } // end match count_c2
380
381        // Locating all possible and distinct C3 axes
382        let mut count_c3 = 0;
383        let mut found_consistent_c3 = false;
384        let sea_groups = &presym.sea_groups;
385        let order_3 = ElementOrder::Int(3);
386        for sea_group in sea_groups.iter() {
387            if sea_group.len() < 3 {
388                continue;
389            }
390            if found_consistent_c3 {
391                break;
392            };
393            for atom3s in sea_group.iter().combinations(3) {
394                let atom_i = atom3s[0];
395                let atom_j = atom3s[1];
396                let atom_k = atom3s[2];
397                if !geometry::check_regular_polygon(&[atom_i, atom_j, atom_k]) {
398                    continue;
399                }
400                let vec_ij = atom_j.coordinates - atom_i.coordinates;
401                let vec_ik = atom_k.coordinates - atom_i.coordinates;
402                let vec_normal = vec_ij.cross(&vec_ik);
403                ensure!(
404                    vec_normal.norm() > presym.recentred_molecule.threshold,
405                    "Unexpected zero-norm vector."
406                );
407                if let Some(proper_kind) = presym.check_proper(&order_3, &vec_normal, tr) {
408                    count_c3 += i32::from(self.add_proper(
409                        order_3,
410                        &vec_normal,
411                        false,
412                        presym.recentred_molecule.threshold,
413                        proper_kind.contains_time_reversal(),
414                    ));
415                }
416                if count_c2 == 3 && count_c3 == 4 {
417                    // Tetrahedral, 4 C3 axes
418                    found_consistent_c3 = true;
419                    break;
420                }
421                if count_c2 == 9 && count_c3 == 4 {
422                    // Octahedral, 4 C3 axes
423                    found_consistent_c3 = true;
424                    break;
425                }
426                if count_c2 == 15 && count_c3 == 10 {
427                    // Icosahedral, 10 C3 axes
428                    found_consistent_c3 = true;
429                    break;
430                }
431            }
432        }
433        ensure!(
434            found_consistent_c3,
435            "Unexpected number of C3 axes: {count_c3}."
436        );
437
438        if count_c3 == 4 {
439            // Tetrahedral or octahedral, C3 axes are also generators.
440            let c3s = self
441                .get_proper(&order_3)
442                .ok_or_else(|| format_err!(" No C3 elements found."))?
443                .into_iter()
444                .cloned()
445                .collect_vec();
446            for c3 in &c3s {
447                self.add_proper(
448                    order_3,
449                    c3.raw_axis(),
450                    true,
451                    presym.recentred_molecule.threshold,
452                    c3.contains_time_reversal(),
453                );
454            }
455        }
456
457        // Locating all possible and distinct C4 axes for O and Oh point groups
458        if count_c2 == 9 {
459            let mut count_c4 = 0;
460            let mut found_consistent_c4 = false;
461            let sea_groups = &presym.sea_groups;
462            let order_4 = ElementOrder::Int(4);
463            for sea_group in sea_groups.iter() {
464                if sea_group.len() < 4 {
465                    continue;
466                }
467                if found_consistent_c4 {
468                    break;
469                };
470                for atom4s in sea_group.iter().combinations(4) {
471                    let atom_i = atom4s[0];
472                    let atom_j = atom4s[1];
473                    let atom_k = atom4s[2];
474                    let atom_l = atom4s[3];
475                    if !geometry::check_regular_polygon(&[atom_i, atom_j, atom_k, atom_l]) {
476                        continue;
477                    }
478                    let vec_ij = atom_j.coordinates - atom_i.coordinates;
479                    let vec_ik = atom_k.coordinates - atom_i.coordinates;
480                    let vec_normal = vec_ij.cross(&vec_ik);
481                    ensure!(
482                        vec_normal.norm() > presym.recentred_molecule.threshold,
483                        "Unexpected zero-norm vector."
484                    );
485                    if let Some(proper_kind) = presym.check_proper(&order_4, &vec_normal, tr) {
486                        count_c4 += i32::from(self.add_proper(
487                            order_4,
488                            &vec_normal,
489                            false,
490                            presym.recentred_molecule.threshold,
491                            proper_kind.contains_time_reversal(),
492                        ));
493                    }
494                    if count_c4 == 3 {
495                        found_consistent_c4 = true;
496                        break;
497                    }
498                }
499            }
500            ensure!(
501                found_consistent_c4,
502                "Unexpected number of C4 axes: {count_c4}."
503            );
504
505            // Add a C4 as a generator
506            let c4 = *self
507                .get_proper(&order_4)
508                .ok_or_else(|| format_err!(" No C4 elements found."))?
509                .iter()
510                .next()
511                .ok_or_else(|| format_err!("Expected C4 not found."))?;
512            let c4_axis = *c4.raw_axis();
513            self.add_proper(
514                order_4,
515                &c4_axis,
516                true,
517                presym.recentred_molecule.threshold,
518                c4.contains_time_reversal(),
519            );
520        } // end locating C4 axes for O and Oh
521
522        // Locating all possible and distinct C5 axes for I and Ih point groups
523        if count_c2 == 15 {
524            let mut count_c5 = 0;
525            let mut found_consistent_c5 = false;
526            let sea_groups = &presym.sea_groups;
527            let order_5 = ElementOrder::Int(5);
528            for sea_group in sea_groups.iter() {
529                if sea_group.len() < 5 {
530                    continue;
531                }
532                if found_consistent_c5 {
533                    break;
534                };
535                for atom5s in sea_group.iter().combinations(5) {
536                    let atom_i = atom5s[0];
537                    let atom_j = atom5s[1];
538                    let atom_k = atom5s[2];
539                    let atom_l = atom5s[3];
540                    let atom_m = atom5s[4];
541                    if !geometry::check_regular_polygon(&[atom_i, atom_j, atom_k, atom_l, atom_m]) {
542                        continue;
543                    }
544                    let vec_ij = atom_j.coordinates - atom_i.coordinates;
545                    let vec_ik = atom_k.coordinates - atom_i.coordinates;
546                    let vec_normal = vec_ij.cross(&vec_ik);
547                    ensure!(
548                        vec_normal.norm() > presym.recentred_molecule.threshold,
549                        "Unexpected zero-norm vector."
550                    );
551                    if let Some(proper_kind) = presym.check_proper(&order_5, &vec_normal, tr) {
552                        count_c5 += i32::from(self.add_proper(
553                            order_5,
554                            &vec_normal,
555                            false,
556                            presym.recentred_molecule.threshold,
557                            proper_kind.contains_time_reversal(),
558                        ));
559                        self.add_proper(
560                            order_5,
561                            &vec_normal,
562                            true,
563                            presym.recentred_molecule.threshold,
564                            proper_kind.contains_time_reversal(),
565                        );
566                    }
567                    if count_c5 == 6 {
568                        found_consistent_c5 = true;
569                        break;
570                    }
571                }
572            }
573            ensure!(
574                found_consistent_c5,
575                "Unexpected number of C5 axes: {count_c5}."
576            );
577        } // end locating C5 axes for I and Ih
578
579        // Locating any other improper rotation axes for the non-chinal groups
580        if *self
581            .group_name
582            .as_ref()
583            .ok_or_else(|| format_err!("No point groups found."))?
584            == "Td"
585        {
586            // Locating S4
587            let order_4 = ElementOrder::Int(4);
588            let improper_s4_axes: Vec<(Vector3<f64>, bool)> = {
589                self.get_proper(&ORDER_2)
590                    .ok_or_else(|| format_err!("Expected C2 elements not found."))?
591                    .iter()
592                    .filter_map(|c2_ele| {
593                        presym
594                            .check_improper(&order_4, c2_ele.raw_axis(), &SIG, tr)
595                            .map(|improper_kind| {
596                                (*c2_ele.raw_axis(), improper_kind.contains_time_reversal())
597                            })
598                    })
599                    .collect()
600            };
601            let mut count_s4 = 0;
602            for (s4_axis, s4_axis_tr) in improper_s4_axes {
603                count_s4 += i32::from(self.add_improper(
604                    order_4,
605                    &s4_axis,
606                    false,
607                    SIG,
608                    None,
609                    presym.recentred_molecule.threshold,
610                    s4_axis_tr,
611                ));
612            }
613            ensure!(count_s4 == 3, "Unexpected number of S4 axes: {count_s4}.");
614        }
615        // end locating improper axes for Td
616        else if *self
617            .group_name
618            .as_ref()
619            .ok_or_else(|| format_err!("No point groups found."))?
620            == "Th"
621        {
622            // Locating σh
623            let sigmah_normals: Vec<(Vector3<f64>, bool)> = {
624                self.get_proper(&ORDER_2)
625                    .ok_or_else(|| format_err!("Expected C2 elements not found."))?
626                    .iter()
627                    .filter_map(|c2_ele| {
628                        presym
629                            .check_improper(&ORDER_1, c2_ele.raw_axis(), &SIG, tr)
630                            .map(|improper_kind| {
631                                (*c2_ele.raw_axis(), improper_kind.contains_time_reversal())
632                            })
633                    })
634                    .collect()
635            };
636            let mut count_sigmah = 0;
637            for (sigmah_normal, sigmah_normal_tr) in sigmah_normals {
638                count_sigmah += i32::from(self.add_improper(
639                    ORDER_1,
640                    &sigmah_normal,
641                    false,
642                    SIG,
643                    Some("h".to_owned()),
644                    presym.recentred_molecule.threshold,
645                    sigmah_normal_tr,
646                ));
647            }
648            ensure!(
649                count_sigmah == 3,
650                "Unexpected number of σh mirror planes: {count_sigmah}."
651            );
652
653            // Locating S6
654            let order_6 = ElementOrder::Int(6);
655            let s6_axes: Vec<(Vector3<f64>, bool)> = {
656                self.get_proper(&order_3)
657                    .ok_or_else(|| format_err!("Expected C3 elements not found."))?
658                    .iter()
659                    .filter_map(|c3_ele| {
660                        presym
661                            .check_improper(&order_6, c3_ele.raw_axis(), &SIG, tr)
662                            .map(|improper_kind| {
663                                (*c3_ele.raw_axis(), improper_kind.contains_time_reversal())
664                            })
665                    })
666                    .collect()
667            };
668            let mut count_s6 = 0;
669            for (s6_axis, s6_axis_tr) in s6_axes {
670                count_s6 += i32::from(self.add_improper(
671                    order_6,
672                    &s6_axis,
673                    false,
674                    SIG,
675                    None,
676                    presym.recentred_molecule.threshold,
677                    s6_axis_tr,
678                ));
679            }
680            ensure!(count_s6 == 4, "Unexpected number of S6 axes: {count_s6}.");
681        }
682        // end locating improper axes for Th
683        else if *self
684            .group_name
685            .as_ref()
686            .ok_or_else(|| format_err!("No point groups found."))?
687            == "Oh"
688        {
689            // Locating S4
690            let order_4 = ElementOrder::Int(4);
691            let s4_axes: Vec<(Vector3<f64>, bool)> = {
692                self.get_proper(&ORDER_2)
693                    .ok_or_else(|| format_err!("Expected C2 elements not found."))?
694                    .iter()
695                    .filter_map(|c2_ele| {
696                        presym
697                            .check_improper(&order_4, c2_ele.raw_axis(), &SIG, tr)
698                            .map(|improper_kind| {
699                                (*c2_ele.raw_axis(), improper_kind.contains_time_reversal())
700                            })
701                    })
702                    .collect()
703            };
704            let mut count_s4 = 0;
705            for (s4_axis, s4_axis_tr) in &s4_axes {
706                count_s4 += i32::from(self.add_improper(
707                    order_4,
708                    s4_axis,
709                    false,
710                    SIG,
711                    None,
712                    presym.recentred_molecule.threshold,
713                    *s4_axis_tr,
714                ));
715            }
716            ensure!(count_s4 == 3, "Unexpected number of S4 axes: {count_s4}.");
717
718            let sigmah_axes: Vec<(Vector3<f64>, bool)> = {
719                s4_axes
720                    .iter()
721                    .filter_map(|(sigmah_axis, _)| {
722                        presym.check_improper(&ORDER_1, sigmah_axis, &SIG, tr).map(
723                            |improper_kind| (*sigmah_axis, improper_kind.contains_time_reversal()),
724                        )
725                    })
726                    .collect()
727            };
728            let mut count_sigmah = 0;
729            for (sigmah_axis, sigmah_axis_tr) in sigmah_axes {
730                count_sigmah += i32::from(self.add_improper(
731                    ORDER_1,
732                    &sigmah_axis,
733                    false,
734                    SIG,
735                    Some("h".to_owned()),
736                    presym.recentred_molecule.threshold,
737                    sigmah_axis_tr,
738                ));
739            }
740            ensure!(
741                count_sigmah == 3,
742                "Unexpected number of σh mirror planes: {count_sigmah}."
743            );
744
745            // Locating σd
746            let sigmad_normals: Vec<(Vector3<f64>, bool)> = {
747                self.get_proper(&ORDER_2)
748                    .ok_or_else(|| format_err!("Expected C2 elements not found."))?
749                    .iter()
750                    .filter_map(|c2_ele| {
751                        if presym
752                            .check_improper(&order_4, c2_ele.raw_axis(), &SIG, tr)
753                            .is_none()
754                        {
755                            presym
756                                .check_improper(&ORDER_1, c2_ele.raw_axis(), &SIG, tr)
757                                .map(|improper_kind| {
758                                    (*c2_ele.raw_axis(), improper_kind.contains_time_reversal())
759                                })
760                        } else {
761                            None
762                        }
763                    })
764                    .collect()
765            };
766            let mut count_sigmad = 0;
767            for (sigmad_normal, sigmad_normal_tr) in sigmad_normals {
768                count_sigmad += i32::from(self.add_improper(
769                    ORDER_1,
770                    &sigmad_normal,
771                    false,
772                    SIG,
773                    Some("d".to_owned()),
774                    presym.recentred_molecule.threshold,
775                    sigmad_normal_tr,
776                ));
777            }
778            ensure!(
779                count_sigmad == 6,
780                "Unexpected number of σd mirror planes: {count_sigmad}."
781            );
782
783            // Locating S6
784            let order_6 = ElementOrder::Int(6);
785            let s6_axes: Vec<(Vector3<f64>, bool)> = {
786                self.get_proper(&order_3)
787                    .ok_or_else(|| format_err!("Expected C3 elements not found."))?
788                    .iter()
789                    .filter_map(|c3_ele| {
790                        presym
791                            .check_improper(&order_6, c3_ele.raw_axis(), &SIG, tr)
792                            .map(|improper_kind| {
793                                (*c3_ele.raw_axis(), improper_kind.contains_time_reversal())
794                            })
795                    })
796                    .collect()
797            };
798            let mut count_s6 = 0;
799            for (s6_axis, s6_axis_tr) in s6_axes {
800                count_s6 += i32::from(self.add_improper(
801                    order_6,
802                    &s6_axis,
803                    false,
804                    SIG,
805                    None,
806                    presym.recentred_molecule.threshold,
807                    s6_axis_tr,
808                ));
809            }
810            ensure!(count_s6 == 4, "Unexpected number of S6 axes: {count_s6}.");
811        }
812        // end locating improper axes for Oh
813        else if *self
814            .group_name
815            .as_ref()
816            .ok_or_else(|| format_err!("No point groups found."))?
817            == "Ih"
818        {
819            // Locating S10
820            let order_5 = ElementOrder::Int(5);
821            let order_10 = ElementOrder::Int(10);
822            let s10_axes: Vec<(Vector3<f64>, bool)> = {
823                self.get_proper(&order_5)
824                    .ok_or_else(|| format_err!("Expected C5 elements not found."))?
825                    .iter()
826                    .filter_map(|c5_ele| {
827                        presym
828                            .check_improper(&order_10, c5_ele.raw_axis(), &SIG, tr)
829                            .map(|improper_kind| {
830                                (*c5_ele.raw_axis(), improper_kind.contains_time_reversal())
831                            })
832                    })
833                    .collect()
834            };
835            let mut count_s10 = 0;
836            for (s10_axis, s10_axis_tr) in s10_axes {
837                count_s10 += i32::from(self.add_improper(
838                    order_10,
839                    &s10_axis,
840                    false,
841                    SIG,
842                    None,
843                    presym.recentred_molecule.threshold,
844                    s10_axis_tr,
845                ));
846            }
847            ensure!(
848                count_s10 == 6,
849                "Unexpected number of S10 axes: {count_s10}."
850            );
851
852            // Locating S6
853            let order_6 = ElementOrder::Int(6);
854            let s6_axes: Vec<(Vector3<f64>, bool)> = {
855                self.get_proper(&order_3)
856                    .ok_or_else(|| format_err!("Expected C3 elements not found."))?
857                    .iter()
858                    .filter_map(|c3_ele| {
859                        presym
860                            .check_improper(&order_6, c3_ele.raw_axis(), &SIG, tr)
861                            .map(|improper_kind| {
862                                (*c3_ele.raw_axis(), improper_kind.contains_time_reversal())
863                            })
864                    })
865                    .collect()
866            };
867            let mut count_s6 = 0;
868            for (s6_axis, s6_axis_tr) in s6_axes {
869                count_s6 += i32::from(self.add_improper(
870                    order_6,
871                    &s6_axis,
872                    false,
873                    SIG,
874                    None,
875                    presym.recentred_molecule.threshold,
876                    s6_axis_tr,
877                ));
878            }
879            ensure!(count_s6 == 10, "Unexpected number of S6 axes: {count_s6}.");
880
881            // Locating σ
882            let sigma_normals: Vec<(Vector3<f64>, bool)> = {
883                self.get_proper(&ORDER_2)
884                    .ok_or_else(|| format_err!("Expected C2 elements not found."))?
885                    .iter()
886                    .filter_map(|c2_ele| {
887                        presym
888                            .check_improper(&ORDER_1, c2_ele.raw_axis(), &SIG, tr)
889                            .map(|improper_kind| {
890                                (*c2_ele.raw_axis(), improper_kind.contains_time_reversal())
891                            })
892                    })
893                    .collect()
894            };
895            let mut count_sigma = 0;
896            for (sigma_normal, sigma_normal_tr) in sigma_normals {
897                count_sigma += i32::from(self.add_improper(
898                    ORDER_1,
899                    &sigma_normal,
900                    false,
901                    SIG,
902                    None,
903                    presym.recentred_molecule.threshold,
904                    sigma_normal_tr,
905                ));
906            }
907            ensure!(
908                count_sigma == 15,
909                "Unexpected number of σ mirror planes: {count_sigma}."
910            );
911        } // end locating improper axes for Ih
912
913        Ok(())
914    }
915}