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