qsym2/symmetry/symmetry_element/
symmetry_operation.rs

1//! Symmetry operations.
2
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::ops::Mul;
6
7use anyhow::{self, format_err};
8use approx;
9use derive_builder::Builder;
10use fraction;
11use nalgebra::{Point3, Vector3};
12use ndarray::{Array2, Axis, ShapeBuilder};
13use num::Complex;
14use num_traits::{Inv, Pow, Zero};
15use ordered_float::OrderedFloat;
16use serde::{Deserialize, Serialize};
17
18use crate::angmom::spinor_rotation_3d::dmat_angleaxis_gen_single;
19use crate::auxiliary::geometry::{
20    self, IMINV, PositiveHemisphere, Transform, improper_rotation_matrix, proper_rotation_matrix,
21};
22use crate::auxiliary::misc::{self, HashableFloat};
23use crate::group::FiniteOrder;
24use crate::permutation::{IntoPermutation, PermutableCollection, Permutation};
25use crate::symmetry::symmetry_element::{
26    AntiunitaryKind, INV, ROT, SIG, SO3, SU2_0, SU2_1, SymmetryElement, SymmetryElementKind, TRINV,
27    TRROT, TRSIG,
28};
29use crate::symmetry::symmetry_element_order::ElementOrder;
30
31type F = fraction::GenericFraction<u32>;
32type Quaternion = (f64, Vector3<f64>);
33
34#[cfg(test)]
35#[path = "symmetry_operation_tests.rs"]
36mod symmetry_operation_tests;
37
38// =================
39// Trait definitions
40// =================
41
42/// Trait for special symmetry transformations.
43pub trait SpecialSymmetryTransformation {
44    // =================
45    // Group-theoretical
46    // =================
47
48    /// Checks if the proper rotation part of the symmetry operation is in $`\mathsf{SU}(2)`$.
49    ///
50    /// # Returns
51    ///
52    /// A boolean indicating if this symmetry operation contains an $`\mathsf{SU}(2)`$ proper
53    /// rotation.
54    fn is_su2(&self) -> bool;
55
56    /// Checks if the proper rotation part of the symmetry operation is in $`\mathsf{SU}(2)`$ and
57    /// connected to the identity via a homotopy path of class 1.
58    ///
59    /// # Returns
60    ///
61    /// A boolean indicating if this symmetry operation contains an $`\mathsf{SU}(2)`$ proper
62    /// rotation connected to the identity via a homotopy path of class 1.
63    fn is_su2_class_1(&self) -> bool;
64
65    // ============
66    // Spatial part
67    // ============
68
69    /// Checks if the spatial part of the symmetry operation is proper.
70    ///
71    /// # Returns
72    ///
73    /// A boolean indicating if the spatial part of the symmetry operation is proper.
74    fn is_proper(&self) -> bool;
75
76    /// Checks if the spatial part of the symmetry operation is the spatial identity.
77    ///
78    /// # Returns
79    ///
80    /// A boolean indicating if the spatial part of the symmetry operation is the spatial identity.
81    fn is_spatial_identity(&self) -> bool;
82
83    /// Checks if the spatial part of the symmetry operation is a spatial binary rotation.
84    ///
85    /// # Returns
86    ///
87    /// A boolean indicating if the spatial part of the symmetry operation is a spatial binary
88    /// rotation.
89    fn is_spatial_binary_rotation(&self) -> bool;
90
91    /// Checks if the spatial part of the symmetry operation is the spatial inversion.
92    ///
93    /// # Returns
94    ///
95    /// A boolean indicating if the spatial part of the symmetry operation is the spatial inversion.
96    fn is_spatial_inversion(&self) -> bool;
97
98    /// Checks if the spatial part of the symmetry operation is a spatial reflection.
99    ///
100    /// # Returns
101    ///
102    /// A boolean indicating if the spatial part of the symmetry operation is a spatial reflection.
103    fn is_spatial_reflection(&self) -> bool;
104
105    // ==================
106    // Time-reversal part
107    // ==================
108
109    /// Checks if the symmetry operation contains time reversal.
110    ///
111    /// # Returns
112    ///
113    /// A boolean indicating if the symmetry oppperation contains time reversal.
114    fn contains_time_reversal(&self) -> bool;
115
116    // ==========================
117    // Overall - provided methods
118    // ==========================
119
120    /// Checks if the symmetry operation is the identity in $`\mathsf{O}(3)`$, `E`, or
121    /// in $`\mathsf{SU}(2)`$, `E(Σ)`.
122    ///
123    /// # Returns
124    ///
125    /// A boolean indicating if this symmetry operation is the identity.
126    fn is_identity(&self) -> bool {
127        self.is_spatial_identity() && !self.contains_time_reversal() && !self.is_su2_class_1()
128    }
129
130    /// Checks if the symmetry operation is a pure time-reversal in $`\mathsf{O}(3)`$, `θ`, or
131    /// in $`\mathsf{SU}(2)`$, `θ(Σ)`.
132    ///
133    /// # Returns
134    ///
135    /// A boolean indicating if this symmetry operation is a pure time-reversal.
136    fn is_time_reversal(&self) -> bool {
137        self.is_spatial_identity() && self.contains_time_reversal() && !self.is_su2_class_1()
138    }
139
140    /// Checks if the symmetry operation is an inversion in $`\mathsf{O}(3)`$, `i`, but not in
141    /// $`\mathsf{SU}(2)`$, `i(Σ)`.
142    ///
143    /// # Returns
144    ///
145    /// A boolean indicating if this symmetry operation is an inversion in $`\mathsf{O}(3)`$.
146    fn is_inversion(&self) -> bool {
147        self.is_spatial_inversion() && !self.contains_time_reversal() && !self.is_su2()
148    }
149
150    /// Checks if the symmetry operation is a reflection in $`\mathsf{O}(3)`$, `σ`, but not in
151    /// $`\mathsf{SU}(2)`$, `σ(Σ)`.
152    ///
153    /// # Returns
154    ///
155    /// A boolean indicating if this symmetry operation is a reflection in $`\mathsf{O}(3)`$.
156    fn is_reflection(&self) -> bool {
157        self.is_spatial_reflection() && !self.contains_time_reversal() && !self.is_su2()
158    }
159}
160
161// ======================================
162// Struct definitions and implementations
163// ======================================
164
165/// Structure for managing symmetry operations generated from symmetry elements.
166///
167/// A symmetry element serves as a generator for symmetry operations. Thus, a symmetry element
168/// together with a signed integer indicating the number of times the symmetry element is applied
169/// (positively or negatively) specifies a symmetry operation.
170#[derive(Builder, Clone, Serialize, Deserialize)]
171pub struct SymmetryOperation {
172    /// The generating symmetry element for this symmetry operation.
173    pub generating_element: SymmetryElement,
174
175    /// The integral power indicating the number of times
176    /// [`Self::generating_element`] is applied to form the symmetry operation.
177    pub power: i32,
178
179    /// The total proper rotation angle associated with this operation (after taking into account
180    /// the power of the operation).
181    ///
182    /// This is simply the proper rotation angle of [`Self::generating_element`] multiplied by
183    /// [`Self::power`] and then folded onto the open interval $`(-\pi, \pi]`$.
184    ///
185    /// This angle lies in the open interval $`(-\pi, \pi]`$. For improper operations, this angle
186    /// depends on the convention used to describe the [`Self::generating_element`].
187    #[builder(setter(skip), default = "self.calc_total_proper_angle()")]
188    total_proper_angle: f64,
189
190    /// The fraction $`pk/n \in (-1/2, 1/2]`$ of the proper rotation, represented exactly for
191    /// hashing and comparison purposes.
192    ///
193    /// This is not defined for operations with infinite-order generating elements.
194    #[builder(setter(skip), default = "self.calc_total_proper_fraction()")]
195    pub(crate) total_proper_fraction: Option<F>,
196
197    /// The positive hemisphere used for distinguishing positive and negative rotation poles. If
198    /// `None`, the standard positive hemisphere as defined in S.L. Altmann, Rotations,
199    /// Quaternions, and Double Groups (Dover Publications, Inc., New York, 2005) is used.
200    #[builder(default = "None")]
201    pub positive_hemisphere: Option<PositiveHemisphere>,
202}
203
204impl SymmetryOperationBuilder {
205    fn calc_total_proper_angle(&self) -> f64 {
206        let (total_proper_angle, _) = geometry::normalise_rotation_angle(
207            self.generating_element
208                .as_ref()
209                .expect("Generating element has not been set.")
210                .proper_angle
211                .expect("Proper angle has not been set.")
212                * (f64::from(self.power.expect("Power has not been set."))),
213            self.generating_element
214                .as_ref()
215                .expect("Generating element has not been set.")
216                .threshold,
217        );
218        total_proper_angle
219    }
220
221    fn calc_total_proper_fraction(&self) -> Option<F> {
222        match self
223            .generating_element
224            .as_ref()
225            .expect("Generating element has not been set.")
226            .proper_fraction
227        {
228            Some(frac) => {
229                let pow = self.power.expect("Power has not been set.");
230                let (total_proper_fraction, _) =
231                    geometry::normalise_rotation_fraction(frac * F::from(pow));
232                Some(total_proper_fraction)
233            }
234            None => None,
235        }
236    }
237}
238
239impl SymmetryOperation {
240    /// Returns a builder to construct a new symmetry operation.
241    ///
242    /// # Returns
243    ///
244    /// A builder to construct a new symmetry operation.
245    #[must_use]
246    pub(crate) fn builder() -> SymmetryOperationBuilder {
247        SymmetryOperationBuilder::default()
248    }
249
250    /// Constructs a finite-order-element-generated symmetry operation from a quaternion.
251    ///
252    /// The rotation angle encoded in the quaternion is taken to be non-negative and assigned as
253    /// the proper rotation angle associated with the element generating the operation.
254    ///
255    /// If an improper operation is required, its generator will be constructed in the
256    /// inversion-centre convention.
257    ///
258    /// # Arguments
259    ///
260    /// * `qtn` - A quaternion encoding the proper rotation associated with the
261    ///   generating element of the operation to be constructed.
262    /// * `proper` - A boolean indicating if the operation is proper or improper.
263    /// * `thresh` - Threshold for comparisons.
264    /// * `tr` - A boolean indicating if the resulting symmetry operation should be accompanied by
265    ///   a time-reversal operation.
266    /// * `su2` - A boolean indicating if the resulting symmetry operation is to contain a proper
267    ///   rotation in $`\mathsf{SU}(2)`$. The homotopy class of the operation will be deduced from
268    ///   the specified quaternion.
269    /// * `poshem` - An option containing any custom positive hemisphere used to distinguish
270    ///   positive and negative rotation poles.
271    ///
272    /// # Returns
273    ///
274    /// The constructed symmetry operation.
275    ///
276    /// # Panics
277    ///
278    /// Panics when the scalar part of the provided quaternion lies outside $`[0, 1]`$ by more than
279    /// the specified threshold `thresh`, or when the rotation angle associated with the quaternion
280    /// cannot be gracefully converted into an integer tuple of order and power.
281    #[must_use]
282    pub fn from_quaternion(
283        qtn: Quaternion,
284        proper: bool,
285        thresh: f64,
286        max_trial_power: u32,
287        tr: bool,
288        su2: bool,
289        poshem: Option<PositiveHemisphere>,
290    ) -> Self {
291        let (scalar_part, vector_part) = qtn;
292        let kind = if proper {
293            if tr { TRROT } else { ROT }
294        } else if tr {
295            TRINV
296        } else {
297            INV
298        };
299        let element = if su2 {
300            // SU(2)
301            assert!(
302                -1.0 - thresh <= scalar_part && scalar_part <= 1.0 + thresh,
303                "The scalar part of the quaternion must be in the interval [-1, +1]."
304            );
305            let (axis, order, power, su2_grp) =
306                if approx::abs_diff_eq!(scalar_part, 1.0, epsilon = thresh,) {
307                    // Zero-degree rotation, i.e. identity or inversion, class 0
308                    (Vector3::z(), 1u32, 1u32, SU2_0)
309                } else if approx::abs_diff_eq!(scalar_part, -1.0, epsilon = thresh,) {
310                    // 360-degree rotation, i.e. identity or inversion, class 1
311                    (Vector3::z(), 1u32, 1u32, SU2_1)
312                } else if approx::abs_diff_eq!(scalar_part, 0.0, epsilon = thresh,) {
313                    // 180-degree rotation, i.e. binary rotation or reflection. Whether the resultant
314                    // operation is in class 0 or class 1 depends on whether the vector part is in the
315                    // positive hemisphere or negative hemisphere.
316                    let positive_axis = poshem
317                        .as_ref()
318                        .cloned()
319                        .unwrap_or_default()
320                        .get_positive_pole(&vector_part, thresh);
321                    (
322                        positive_axis,
323                        2u32,
324                        1u32,
325                        if poshem
326                            .as_ref()
327                            .cloned()
328                            .unwrap_or_default()
329                            .check_positive_pole(&vector_part, thresh)
330                        {
331                            SU2_0
332                        } else {
333                            SU2_1
334                        },
335                    )
336                } else {
337                    // scalar_part != 0, 1, or -1
338                    let (standardised_scalar_part, standardised_vector_part, su2_grp) =
339                        if scalar_part > 0.0 {
340                            (scalar_part, vector_part, SU2_0)
341                        } else {
342                            (-scalar_part, -vector_part, SU2_1)
343                        };
344                    let half_proper_angle = standardised_scalar_part.acos();
345                    let proper_angle = 2.0 * half_proper_angle;
346                    let axis = standardised_vector_part / half_proper_angle.sin();
347                    let proper_fraction =
348                        geometry::get_proper_fraction(proper_angle, thresh, max_trial_power)
349                            .unwrap_or_else(|| {
350                                panic!(
351                                    "No proper fraction could be found for angle `{proper_angle}`."
352                                )
353                            });
354                    (
355                        axis,
356                        *proper_fraction.denom().unwrap_or_else(|| {
357                            panic!("Unable to extract the denominator of `{proper_fraction}`.")
358                        }),
359                        *proper_fraction.numer().unwrap_or_else(|| {
360                            panic!("Unable to extract the numerator of `{proper_fraction}`.")
361                        }),
362                        su2_grp,
363                    )
364                };
365            SymmetryElement::builder()
366                .threshold(thresh)
367                .proper_order(ElementOrder::Int(order))
368                .proper_power(
369                    power
370                        .try_into()
371                        .expect("Unable to convert the proper power to `i32`."),
372                )
373                .raw_axis(axis)
374                .kind(kind)
375                .rotation_group(su2_grp)
376                .build()
377                .unwrap_or_else(|_|
378                    panic!("Unable to construct a symmetry element of kind `{kind}` with the proper part in SU(2).")
379                )
380        } else {
381            // SO(3)
382            assert!(
383                -thresh <= scalar_part && scalar_part <= 1.0 + thresh,
384                "The scalar part of the quaternion must be in the interval [0, +1] when only SO(3) rotations are considered."
385            );
386            let (axis, order, power) = if approx::abs_diff_eq!(scalar_part, 1.0, epsilon = thresh,)
387            {
388                // Zero-degree rotation, i.e. identity or inversion
389                (Vector3::z(), 1u32, 1i32)
390            } else {
391                let half_proper_angle = scalar_part.acos(); // acos returns values in [0, π]
392                let proper_angle = 2.0 * half_proper_angle;
393                let axis = vector_part / half_proper_angle.sin();
394                let proper_fraction =
395                    geometry::get_proper_fraction(proper_angle, thresh, max_trial_power)
396                        .unwrap_or_else(|| {
397                            panic!("No proper fraction could be found for angle `{proper_angle}`.")
398                        });
399                let proper_power = if proper_fraction.is_sign_positive() {
400                    i32::try_from(*proper_fraction.numer().unwrap_or_else(|| {
401                        panic!("Unable to extract the numerator of `{proper_fraction}`.")
402                    }))
403                    .expect("Unable to convert the numerator of the proper fraction to `i32`.")
404                } else {
405                    -i32::try_from(*proper_fraction.numer().unwrap_or_else(|| {
406                        panic!("Unable to extract the numerator of `{proper_fraction}`.")
407                    }))
408                    .expect("Unable to convert the numerator of the proper fraction to `i32`.")
409                };
410                (
411                    axis,
412                    *proper_fraction.denom().unwrap_or_else(|| {
413                        panic!("Unable to extract the denominator of `{proper_fraction}`.")
414                    }),
415                    proper_power,
416                )
417            };
418
419            SymmetryElement::builder()
420                .threshold(thresh)
421                .proper_order(ElementOrder::Int(order))
422                .proper_power(power)
423                .raw_axis(axis)
424                .kind(kind)
425                .rotation_group(SO3)
426                .build()
427                .expect(
428                    "Unable to construct a symmetry element without an associated spin rotation.",
429                )
430        };
431
432        SymmetryOperation::builder()
433            .generating_element(element)
434            .power(1)
435            .positive_hemisphere(poshem)
436            .build()
437            .unwrap_or_else(|_|
438                panic!(
439                    "Unable to construct a symmetry operation of kind `{kind}` with {} rotation from the quaternion `{qtn:?}`.",
440                    if su2 { "SU(2)" } else { "SO(3)" }
441                )
442            )
443    }
444
445    /// Finds the quaternion associated with this operation.
446    ///
447    /// The rotation angle encoded in the quaternion is taken to be non-negative and assigned as
448    /// the proper rotation angle associated with the element generating the operation.
449    ///
450    /// If this is an operation generated from an improper element, the inversion-centre convention
451    /// will be used to determine the angle of proper rotation.
452    ///
453    /// Both $`\mathsf{SO}(3)`$ and $`\mathsf{SU}(2)`$ proper rotations are supported. For
454    /// $`\mathsf{SO}(3)`$ proper rotations, only quaternions in the standardised form are
455    /// returned.
456    ///
457    /// See S.L. Altmann, Rotations, Quaternions, and Double Groups (Dover Publications, Inc., New
458    /// York, 2005) (Chapter 9) for further information.
459    ///
460    /// # Returns
461    ///
462    /// The quaternion associated with this operation.
463    ///
464    /// # Panics
465    ///
466    /// Panics if the calculated scalar part of the quaternion lies outside the closed interval
467    /// $`[0, 1]`$ by more than the threshold value stored in the generating element in `self`.
468    #[must_use]
469    pub fn calc_quaternion(&self) -> Quaternion {
470        let c_self = if self.is_proper() {
471            self.clone()
472        } else {
473            // Time-reversal does not matter here.
474            self.convert_to_improper_kind(&INV)
475        };
476        debug_assert_eq!(
477            self.is_su2_class_1(),
478            c_self.is_su2_class_1(),
479            "`{self}` and `{c_self}` are in different homotopy classes."
480        );
481
482        // We only need the absolute value of the angle. Its sign information is
483        // encoded in the pole. `abs_angle` thus lies in [0, π], and so
484        //  cos(abs_angle/2) >= 0 and sin(abs_angle/2) >= 0.
485        // The scalar part is guaranteed to be in [0, 1].
486        // For binary rotations, the scalar part is zero, but the definition of pole ensures that
487        // the vector part still lies in the positive hemisphere.
488        let abs_angle = c_self.total_proper_angle.abs();
489        let scalar_part = (0.5 * abs_angle).cos();
490        let vector_part = (0.5 * abs_angle).sin() * c_self.calc_pole().coords;
491        debug_assert!(
492            -self.generating_element.threshold <= scalar_part
493                && scalar_part <= 1.0 + self.generating_element.threshold
494        );
495        debug_assert!(if approx::abs_diff_eq!(
496            scalar_part,
497            0.0,
498            epsilon = c_self.generating_element.threshold
499        ) {
500            c_self
501                .positive_hemisphere
502                .as_ref()
503                .cloned()
504                .unwrap_or_default()
505                .check_positive_pole(&vector_part, c_self.generating_element.threshold)
506        } else {
507            true
508        },);
509
510        if self.is_su2_class_1() {
511            (-scalar_part, -vector_part)
512        } else {
513            (scalar_part, vector_part)
514        }
515    }
516
517    /// Finds the pole associated with this operation with respect to the positive hemisphere
518    /// defined in [`Self::positive_hemisphere`].
519    ///
520    /// This is the point on the unit sphere that is left invariant by the operation.
521    ///
522    /// For improper operations, the inversion-centre convention is used to define
523    /// the pole. This allows a proper rotation and its improper partner to have the
524    /// same pole, thus facilitating the consistent specification of poles for the
525    /// identity / inversion and binary rotations / reflections.
526    ///
527    /// Note that binary rotations / reflections have unique poles on the positive
528    /// hemisphere (*i.e.*, $`C_2(\hat{\mathbf{n}}) = C_2^{-1}(\hat{\mathbf{n}})`$
529    /// and $`\sigma(\hat{\mathbf{n}}) = \sigma^{-1}(\hat{\mathbf{n}})`$).
530    ///
531    /// See S.L. Altmann, Rotations, Quaternions, and Double Groups (Dover
532    /// Publications, Inc., New York, 2005) (Chapter 9) for further information.
533    ///
534    /// # Returns
535    ///
536    /// The pole associated with this operation.
537    ///
538    /// # Panics
539    ///
540    /// Panics when no total proper fractions could be found for this operation.
541    #[must_use]
542    pub fn calc_pole(&self) -> Point3<f64> {
543        let op = if self.is_proper() {
544            self.clone()
545        } else {
546            // Time-reversal does not matter here.
547            self.convert_to_improper_kind(&INV)
548        };
549        match *op.generating_element.raw_proper_order() {
550            ElementOrder::Int(_) => {
551                let frac_1_2 = F::new(1u32, 2u32);
552                let total_proper_fraction = op
553                    .total_proper_fraction
554                    .expect("No total proper fractions found.");
555                if total_proper_fraction == frac_1_2 {
556                    // Binary rotations or reflections
557                    Point3::from(
558                        self.positive_hemisphere
559                            .as_ref()
560                            .cloned()
561                            .unwrap_or_default()
562                            .get_positive_pole(
563                                &op.generating_element.raw_axis,
564                                op.generating_element.threshold,
565                            ),
566                    )
567                } else if total_proper_fraction > F::zero() {
568                    // Positive rotation angles
569                    Point3::from(op.generating_element.raw_axis)
570                } else if total_proper_fraction < F::zero() {
571                    // Negative rotation angles
572                    Point3::from(-op.generating_element.raw_axis)
573                } else {
574                    // Identity or inversion
575                    assert!(total_proper_fraction.is_zero());
576                    Point3::from(Vector3::z())
577                }
578            }
579            ElementOrder::Inf => {
580                if approx::abs_diff_eq!(
581                    op.total_proper_angle,
582                    std::f64::consts::PI,
583                    epsilon = op.generating_element.threshold
584                ) {
585                    // Binary rotations or reflections
586                    Point3::from(
587                        self.positive_hemisphere
588                            .as_ref()
589                            .cloned()
590                            .unwrap_or_default()
591                            .get_positive_pole(
592                                &op.generating_element.raw_axis,
593                                op.generating_element.threshold,
594                            ),
595                    )
596                } else if approx::abs_diff_ne!(
597                    op.total_proper_angle,
598                    0.0,
599                    epsilon = op.generating_element.threshold
600                ) {
601                    Point3::from(op.total_proper_angle.signum() * op.generating_element.raw_axis)
602                } else {
603                    approx::assert_abs_diff_eq!(
604                        op.total_proper_angle,
605                        0.0,
606                        epsilon = op.generating_element.threshold
607                    );
608                    Point3::from(Vector3::z())
609                }
610            }
611        }
612    }
613
614    /// Finds the pole associated with the proper rotation of this operation.
615    ///
616    /// This is the point on the unit sphere that is left invariant by the proper rotation part of
617    /// the operation.
618    ///
619    /// For improper operations, no conversions will be performed, unlike in [`Self::calc_pole`].
620    ///
621    /// Note that binary rotations have unique poles on the positive hemisphere (*i.e.*,
622    /// $`C_2(\hat{\mathbf{n}}) = C_2^{-1}(\hat{\mathbf{n}})`$ and
623    /// $`\sigma(\hat{\mathbf{n}}) = \sigma^{-1}(\hat{\mathbf{n}})`$).
624    ///
625    /// See S.L. Altmann, Rotations, Quaternions, and Double Groups (Dover
626    /// Publications, Inc., New York, 2005) (Chapter 9) for further information.
627    ///
628    /// # Returns
629    ///
630    /// The pole associated with the proper rotation of this operation.
631    ///
632    /// # Panics
633    ///
634    /// Panics when no total proper fractions could be found for this operation.
635    #[must_use]
636    pub fn calc_proper_rotation_pole(&self) -> Point3<f64> {
637        match *self.generating_element.raw_proper_order() {
638            ElementOrder::Int(_) => {
639                let frac_1_2 = F::new(1u32, 2u32);
640                let total_proper_fraction = self
641                    .total_proper_fraction
642                    .expect("No total proper fractions found.");
643                if total_proper_fraction == frac_1_2 {
644                    // Binary rotations or reflections
645                    Point3::from(
646                        self.positive_hemisphere
647                            .as_ref()
648                            .cloned()
649                            .unwrap_or_default()
650                            .get_positive_pole(
651                                &self.generating_element.raw_axis,
652                                self.generating_element.threshold,
653                            ),
654                    )
655                } else if total_proper_fraction > F::zero() {
656                    // Positive rotation angles
657                    Point3::from(self.generating_element.raw_axis)
658                } else if total_proper_fraction < F::zero() {
659                    // Negative rotation angles
660                    Point3::from(-self.generating_element.raw_axis)
661                } else {
662                    // Identity or inversion
663                    assert!(total_proper_fraction.is_zero());
664                    Point3::from(Vector3::z())
665                }
666            }
667            ElementOrder::Inf => {
668                if approx::abs_diff_eq!(
669                    self.total_proper_angle,
670                    std::f64::consts::PI,
671                    epsilon = self.generating_element.threshold
672                ) {
673                    // Binary rotations or reflections
674                    Point3::from(
675                        self.positive_hemisphere
676                            .as_ref()
677                            .cloned()
678                            .unwrap_or_default()
679                            .get_positive_pole(
680                                &self.generating_element.raw_axis,
681                                self.generating_element.threshold,
682                            ),
683                    )
684                } else if approx::abs_diff_ne!(
685                    self.total_proper_angle,
686                    0.0,
687                    epsilon = self.generating_element.threshold
688                ) {
689                    Point3::from(
690                        self.total_proper_angle.signum() * self.generating_element.raw_axis,
691                    )
692                } else {
693                    approx::assert_abs_diff_eq!(
694                        self.total_proper_angle,
695                        0.0,
696                        epsilon = self.generating_element.threshold
697                    );
698                    Point3::from(Vector3::z())
699                }
700            }
701        }
702    }
703
704    /// Finds the pole angle associated with this operation.
705    ///
706    /// This is the non-negative angle that, together with the pole, uniquely determines the proper
707    /// part of this operation. This angle lies in the interval $`[0, \pi]`$.
708    ///
709    /// For improper operations, the inversion-centre convention is used to define the pole angle.
710    /// This allows a proper rotation and its improper partner to have the same pole angle, thus
711    /// facilitating the consistent specification of poles for the identity / inversion and binary
712    /// rotations / reflections.
713    ///
714    /// # Returns
715    ///
716    /// The pole angle associated with this operation.
717    ///
718    /// # Panics
719    ///
720    /// Panics when no total proper fractions could be found for this operation.
721    #[must_use]
722    pub fn calc_pole_angle(&self) -> f64 {
723        let c_self = if self.is_proper() {
724            self.clone()
725        } else {
726            // Time-reversal does not matter here.
727            self.convert_to_improper_kind(&INV)
728        };
729
730        c_self.total_proper_angle.abs()
731    }
732
733    /// Returns a copy of the current symmetry operation with the generating element
734    /// converted to the requested improper kind (power-preserving), provided that
735    /// it is an improper element.
736    ///
737    /// # Arguments
738    ///
739    /// * `improper_kind` - The improper kind to which `self` is to be converted. There is no need
740    ///   to make sure the time reversal specification in `improper_kind` matches that of the
741    ///   generating element of `self` as the conversion will take care of this.
742    ///
743    /// # Panics
744    ///
745    /// Panics if the converted symmetry operation cannot be constructed.
746    #[must_use]
747    pub fn convert_to_improper_kind(&self, improper_kind: &SymmetryElementKind) -> Self {
748        let c_element = self
749            .generating_element
750            .convert_to_improper_kind(improper_kind, true);
751        debug_assert_eq!(
752            self.generating_element.is_su2_class_1(),
753            c_element.is_su2_class_1()
754        );
755        Self::builder()
756            .generating_element(c_element)
757            .power(self.power)
758            .positive_hemisphere(self.positive_hemisphere.clone())
759            .build()
760            .expect("Unable to construct a symmetry operation.")
761    }
762
763    /// Converts the current symmetry operation $`O`$ to an equivalent symmetry element $`E`$ such
764    /// that $`O = E^1`$.
765    ///
766    /// The proper rotation axis of $`E`$ is the proper rotation pole (*not* the overall pole) of
767    /// $`O`$, and the proper rotation angle of $`E`$ is the total proper rotation angle of $`O`$,
768    /// either as an (order, power) integer tuple or an angle floating-point number.
769    ///
770    /// If $`O`$ is improper, then the improper generating element for $`E`$ is the same as that in
771    /// the generating element of $`O`$.
772    ///
773    /// # Returns
774    ///
775    /// The equivalent symmetry element $`E`$.
776    pub fn to_symmetry_element(&self) -> SymmetryElement {
777        let kind = if self.is_proper() {
778            let tr = self.contains_time_reversal();
779            if tr { TRROT } else { ROT }
780        } else {
781            self.generating_element.kind
782        };
783        let additional_superscript = if self.is_proper() {
784            String::new()
785        } else {
786            self.generating_element.additional_superscript.clone()
787        };
788        let additional_subscript = if self.is_proper() {
789            String::new()
790        } else {
791            self.generating_element.additional_subscript.clone()
792        };
793        let rotation_group = if self.is_su2_class_1() {
794            SU2_1
795        } else if self.is_su2() {
796            SU2_0
797        } else {
798            SO3
799        };
800        let axis = if self.is_spatial_reflection() {
801            self.positive_hemisphere
802                .as_ref()
803                .cloned()
804                .unwrap_or_default()
805                .get_positive_pole(
806                    self.generating_element.raw_axis(),
807                    self.generating_element.threshold,
808                )
809        } else {
810            self.calc_proper_rotation_pole().coords
811        };
812
813        if let Some(total_proper_fraction) = self.total_proper_fraction {
814            let proper_order = *total_proper_fraction
815                .denom()
816                .expect("Unable to extract the denominator of the total proper fraction.");
817            let numer = *total_proper_fraction
818                .numer()
819                .expect("Unable to extract the numerator of the total proper fraction.");
820            let proper_power =
821                i32::try_from(numer).expect("Unable to convert the numerator to `i32`.");
822            SymmetryElement::builder()
823                .threshold(self.generating_element.threshold())
824                .proper_order(ElementOrder::Int(proper_order))
825                .proper_power(proper_power)
826                .raw_axis(axis)
827                .kind(kind)
828                .rotation_group(rotation_group)
829                .additional_superscript(additional_superscript)
830                .additional_subscript(additional_subscript)
831                .build()
832                .unwrap()
833        } else {
834            let proper_angle = self.total_proper_angle;
835            SymmetryElement::builder()
836                .threshold(self.generating_element.threshold())
837                .proper_order(ElementOrder::Inf)
838                .proper_angle(proper_angle)
839                .raw_axis(axis)
840                .kind(kind)
841                .rotation_group(rotation_group)
842                .additional_superscript(additional_superscript)
843                .additional_subscript(additional_subscript)
844                .build()
845                .unwrap()
846        }
847    }
848
849    /// Generates the abbreviated symbol for this symmetry operation.
850    #[must_use]
851    pub fn get_abbreviated_symbol(&self) -> String {
852        self.to_symmetry_element()
853            .get_simplified_symbol_signed_power()
854    }
855
856    /// Returns the representation matrix for the spatial part of this symmetry operation.
857    ///
858    /// This representation matrix is in the basis of coordinate *functions* $`(y, z, x)`$.
859    #[must_use]
860    pub fn get_3d_spatial_matrix(&self) -> Array2<f64> {
861        if self.is_proper() {
862            if self.is_identity() || self.is_time_reversal() {
863                Array2::<f64>::eye(3)
864            } else {
865                let angle = self.calc_pole_angle();
866                let axis = self.calc_pole().coords;
867                let mat = proper_rotation_matrix(angle, &axis, 1);
868
869                // nalgebra matrix iter is column-major.
870                Array2::<f64>::from_shape_vec(
871                    (3, 3).f(),
872                    mat.iter().copied().collect::<Vec<_>>(),
873                )
874                .unwrap_or_else(
875                    |_| panic!(
876                        "Unable to construct a three-dimensional rotation matrix for angle {angle} and axis {axis}."
877                    )
878                )
879                .select(Axis(0), &[1, 2, 0])
880                .select(Axis(1), &[1, 2, 0])
881            }
882        } else if self.is_spatial_inversion() {
883            -Array2::<f64>::eye(3)
884        } else {
885            // Pole and pole angle are obtained in the inversion-centre convention.
886            let angle = self.calc_pole_angle();
887            let axis = self.calc_pole().coords;
888            let mat = improper_rotation_matrix(angle, &axis, 1, &IMINV);
889
890            // nalgebra matrix iter is column-major.
891            Array2::<f64>::from_shape_vec(
892                (3, 3).f(),
893                mat.iter().copied().collect::<Vec<_>>(),
894            )
895            .unwrap_or_else(
896                |_| panic!(
897                    "Unable to construct a three-dimensional improper rotation matrix for angle {angle} and axis {axis}."
898                )
899            )
900            .select(Axis(0), &[1, 2, 0])
901            .select(Axis(1), &[1, 2, 0])
902        }
903    }
904
905    /// Returns the representation matrix for this symmetry operation in a $`j`$-adapted basis.
906    ///
907    /// This representation matrix is in the basis of $`\ket{j, m_j}`$. For improper rotations in
908    /// half-odd-integer $`j`$ bases, the Pauli gauge is used where the inversion operation is
909    /// represented by a trivial identity matrix.
910    ///
911    /// # Arguments
912    ///
913    /// * `two_j` - The value of $`2j`$ for the basis.
914    /// * `increasingm` - A boolean indicating if the $`\ket{j, m_j}`$ kets are arranged in
915    ///   increasing $`m_j`$ values.
916    ///
917    /// # Errors
918    ///
919    /// This function returns an error if `two_j` indicates a half-odd-integer spinor basis but the
920    /// symmetry operation is one in $`\mathsf{SO}(3)`$.
921    pub fn get_wigner_matrix(
922        &self,
923        two_j: u32,
924        increasingm: bool,
925    ) -> Result<Array2<Complex<f64>>, anyhow::Error> {
926        let spinor_basis = two_j.rem_euclid(2) == 1;
927        if spinor_basis && !self.is_su2() {
928            Err(format_err!(
929                "Unable to construct a Wigner matrix for an SO(3) rotation in a spinor basis."
930            ))
931        } else {
932            let odd_sh_basis = (!spinor_basis) && two_j.rem_euclid(4) == 2;
933            let dmat_rotation = {
934                let angle = self.calc_pole_angle();
935                let axis = self.calc_pole().coords;
936                if (spinor_basis && self.is_su2_class_1()) || (odd_sh_basis && !self.is_proper()) {
937                    -dmat_angleaxis_gen_single(two_j, angle, axis, increasingm)
938                } else {
939                    dmat_angleaxis_gen_single(two_j, angle, axis, increasingm)
940                }
941            };
942            if self.contains_time_reversal() {
943                // The complex conjugation is required by corepresentation theory.
944                Ok(dmat_angleaxis_gen_single(
945                    two_j,
946                    std::f64::consts::PI,
947                    Vector3::y(),
948                    increasingm,
949                )
950                .dot(&dmat_rotation.map(|x| x.conj())))
951            } else {
952                Ok(dmat_rotation)
953            }
954        }
955    }
956
957    /// Convert the proper rotation of the current operation to one in hopotopy class 0 of
958    /// $`\mathsf{SU}(2)`$.
959    ///
960    /// # Returns
961    ///
962    /// A symmetry element in $`\mathsf{SU}(2)`$.
963    pub fn to_su2_class_0(&self) -> Self {
964        let q_identity = Self::from_quaternion(
965            (-1.0, -Vector3::z()),
966            true,
967            self.generating_element.threshold(),
968            1,
969            false,
970            true,
971            None,
972        );
973        if self.is_su2() {
974            if self.is_su2_class_1() {
975                self * q_identity
976            } else {
977                self.clone()
978            }
979        } else {
980            let mut op = self.clone();
981            op.generating_element.rotation_group = SU2_0;
982            if op.is_su2_class_1() {
983                let mut q_op = op * q_identity;
984                if !q_op.is_proper() {
985                    q_op = q_op.convert_to_improper_kind(&SIG);
986                }
987                q_op
988            } else {
989                op
990            }
991        }
992    }
993
994    /// Sets the positive hemisphere governing this symmetry operation.
995    ///
996    /// # Arguments
997    ///
998    /// * `poshem` - An `Option` containing a custom positive hemisphere, if any.
999    pub fn set_positive_hemisphere(&mut self, poshem: Option<&PositiveHemisphere>) {
1000        self.positive_hemisphere = poshem.cloned();
1001    }
1002}
1003
1004// =====================
1005// Trait implementations
1006// =====================
1007
1008impl FiniteOrder for SymmetryOperation {
1009    type Int = u32;
1010
1011    /// Calculates the order of this symmetry operation.
1012    fn order(&self) -> Self::Int {
1013        let denom = *self
1014            .total_proper_fraction
1015            .expect("No total proper fractions found.")
1016            .denom()
1017            .expect("Unable to extract the denominator.");
1018        let spatial_order =
1019            if (self.is_proper() && !self.contains_time_reversal()) || denom.rem_euclid(2) == 0 {
1020                denom
1021            } else {
1022                2 * denom
1023            };
1024        if self.is_su2() {
1025            2 * spatial_order
1026        } else {
1027            spatial_order
1028        }
1029    }
1030}
1031
1032impl SpecialSymmetryTransformation for SymmetryOperation {
1033    // ============
1034    // Spatial part
1035    // ============
1036
1037    /// Checks if the spatial part of the symmetry operation is proper.
1038    ///
1039    /// # Returns
1040    ///
1041    /// A boolean indicating if the spatial part of the symmetry operation is proper.
1042    fn is_proper(&self) -> bool {
1043        let au = self.generating_element.contains_antiunitary();
1044        self.generating_element.is_o3_proper(au) || self.power.rem_euclid(2) == 0
1045    }
1046
1047    /// Checks if the spatial part of the symmetry operation is the spatial identity.
1048    ///
1049    /// # Returns
1050    ///
1051    /// A boolean indicating if the spatial part of the symmetry operation is the spatial identity.
1052    fn is_spatial_identity(&self) -> bool {
1053        self.is_proper()
1054            && match *self.generating_element.raw_proper_order() {
1055                ElementOrder::Int(_) => self
1056                    .total_proper_fraction
1057                    .expect("Total proper fraction not found for a finite-order operation.")
1058                    .is_zero(),
1059                ElementOrder::Inf => approx::abs_diff_eq!(
1060                    self.total_proper_angle,
1061                    0.0,
1062                    epsilon = self.generating_element.threshold
1063                ),
1064            }
1065    }
1066
1067    /// Checks if the spatial part of the symmetry operation is a spatial binary rotation.
1068    ///
1069    /// # Returns
1070    ///
1071    /// A boolean indicating if the spatial part of the symmetry operation is a spatial binary
1072    /// rotation.
1073    fn is_spatial_binary_rotation(&self) -> bool {
1074        self.is_proper()
1075            && match *self.generating_element.raw_proper_order() {
1076                ElementOrder::Int(_) => {
1077                    self.total_proper_fraction
1078                        .expect("Total proper fraction not found for a finite-order operation.")
1079                        == F::new(1u32, 2u32)
1080                }
1081                ElementOrder::Inf => {
1082                    approx::abs_diff_eq!(
1083                        self.total_proper_angle,
1084                        std::f64::consts::PI,
1085                        epsilon = self.generating_element.threshold
1086                    )
1087                }
1088            }
1089    }
1090
1091    /// Checks if the spatial part of the symmetry operation is the spatial inversion.
1092    ///
1093    /// # Returns
1094    ///
1095    /// A boolean indicating if the spatial part of the symmetry operation is the spatial inversion.
1096    fn is_spatial_inversion(&self) -> bool {
1097        !self.is_proper()
1098            && match self.generating_element.kind {
1099                SymmetryElementKind::ImproperMirrorPlane(_) => {
1100                    if let ElementOrder::Int(_) = *self.generating_element.raw_proper_order() {
1101                        self.total_proper_fraction
1102                            .expect("Total proper fraction not found for a finite-order operation.")
1103                            == F::new(1u32, 2u32)
1104                    } else {
1105                        approx::abs_diff_eq!(
1106                            self.total_proper_angle,
1107                            std::f64::consts::PI,
1108                            epsilon = self.generating_element.threshold
1109                        )
1110                    }
1111                }
1112                SymmetryElementKind::ImproperInversionCentre(_) => {
1113                    if let ElementOrder::Int(_) = *self.generating_element.raw_proper_order() {
1114                        self.total_proper_fraction
1115                            .expect("Total proper fraction not found for a finite-order operation.")
1116                            .is_zero()
1117                    } else {
1118                        approx::abs_diff_eq!(
1119                            self.total_proper_angle,
1120                            0.0,
1121                            epsilon = self.generating_element.threshold
1122                        )
1123                    }
1124                }
1125                _ => false,
1126            }
1127    }
1128
1129    /// Checks if the spatial part of the symmetry operation is a spatial reflection.
1130    ///
1131    /// # Returns
1132    ///
1133    /// A boolean indicating if the spatial part of the symmetry operation is a spatial reflection.
1134    fn is_spatial_reflection(&self) -> bool {
1135        !self.is_proper()
1136            && match self.generating_element.kind {
1137                SymmetryElementKind::ImproperMirrorPlane(_) => {
1138                    if let ElementOrder::Int(_) = *self.generating_element.raw_proper_order() {
1139                        self.total_proper_fraction
1140                            .expect("Total proper fraction not found for a finite-order operation.")
1141                            .is_zero()
1142                    } else {
1143                        approx::abs_diff_eq!(
1144                            self.total_proper_angle,
1145                            0.0,
1146                            epsilon = self.generating_element.threshold
1147                        )
1148                    }
1149                }
1150                SymmetryElementKind::ImproperInversionCentre(_) => {
1151                    if let ElementOrder::Int(_) = self.generating_element.raw_proper_order() {
1152                        self.total_proper_fraction
1153                            .expect("Total proper fraction not found for a finite-order operation.")
1154                            == F::new(1u32, 2u32)
1155                    } else {
1156                        approx::abs_diff_eq!(
1157                            self.total_proper_angle,
1158                            std::f64::consts::PI,
1159                            epsilon = self.generating_element.threshold
1160                        )
1161                    }
1162                }
1163                _ => false,
1164            }
1165    }
1166
1167    // ==================
1168    // Time-reversal part
1169    // ==================
1170
1171    /// Checks if the symmetry operation is antiunitary or not.
1172    ///
1173    /// # Returns
1174    ///
1175    /// A boolean indicating if the symmetry oppperation is antiunitary.
1176    fn contains_time_reversal(&self) -> bool {
1177        self.generating_element.contains_time_reversal() && self.power.rem_euclid(2) == 1
1178    }
1179
1180    // ==================
1181    // Spin rotation part
1182    // ==================
1183
1184    /// Checks if the proper rotation part of the symmetry operation is in $`\mathsf{SU}(2)`$.
1185    ///
1186    /// # Returns
1187    ///
1188    /// A boolean indicating if this symmetry operation contains an $`\mathsf{SU}(2)`$ proper
1189    /// rotation.
1190    fn is_su2(&self) -> bool {
1191        self.generating_element.rotation_group.is_su2()
1192    }
1193
1194    /// Checks if the proper rotation part of the symmetry operation (in the inversion convention)
1195    /// is in $`\mathsf{SU}(2)`$ and connected to the identity via a homotopy path of class 1.
1196    ///
1197    /// # Returns
1198    ///
1199    /// A boolean indicating if this symmetry operation contains an $`\mathsf{SU}(2)`$ proper
1200    /// rotation connected to the identity via a homotopy path of class 1.
1201    fn is_su2_class_1(&self) -> bool {
1202        if self.is_su2() {
1203            // The following is wrong, because `self.is_proper()` takes into account the power applied
1204            // to the spatial part, but not yet to the spin rotation part. Then, for example,
1205            // [QΣ·S3(+0.816, -0.408, +0.408)]^2 would become Σ'·[C3(+0.816, -0.408, +0.408)]^2 where
1206            // Σ' is the associated spin rotation of [C3(+0.816, -0.408, +0.408)]^2, which is not the
1207            // same as Σ^2.
1208            // let c_self = if self.is_proper() {
1209            //     self.clone()
1210            // } else {
1211            //     self.convert_to_improper_kind(&INV)
1212            // };
1213            //
1214            // The following is correct.
1215            let c_self = match self.generating_element.kind {
1216                SymmetryElementKind::Proper(_)
1217                | SymmetryElementKind::ImproperInversionCentre(_) => self.clone(),
1218                SymmetryElementKind::ImproperMirrorPlane(au) => {
1219                    self.convert_to_improper_kind(&SymmetryElementKind::ImproperInversionCentre(au))
1220                }
1221            };
1222            let generating_element_au = c_self.generating_element.contains_antiunitary();
1223            let spatial_proper_identity = c_self
1224                .generating_element
1225                .is_o3_identity(generating_element_au)
1226                || c_self
1227                    .generating_element
1228                    .is_o3_inversion_centre(generating_element_au);
1229
1230            let inverse_from_time_reversal =
1231                if self.is_su2() && generating_element_au == Some(AntiunitaryKind::TimeReversal) {
1232                    self.power.rem_euclid(4) == 2 || self.power.rem_euclid(4) == 3
1233                } else {
1234                    false
1235                };
1236
1237            let inverse_from_rotation_group = if spatial_proper_identity {
1238                // The proper part of the generating element is the identity. In this case, no
1239                // matter the value of proper power, the result is always the identity.
1240                false
1241            } else {
1242                let thresh = c_self.generating_element.threshold;
1243                let odd_jumps_from_angle = c_self
1244                    .generating_element
1245                    .proper_fraction
1246                    .map(|frac| {
1247                        let pow = c_self.power;
1248                        let total_unormalised_proper_fraction = frac * F::from(pow);
1249                        let (_, x) = geometry::normalise_rotation_fraction(
1250                            total_unormalised_proper_fraction,
1251                        );
1252                        x.rem_euclid(2) == 1
1253                    })
1254                    .unwrap_or_else(|| {
1255                        let total_unormalised_proper_angle = c_self
1256                            .generating_element
1257                            .proper_angle
1258                            .expect("Proper angle of generating element not found.")
1259                            * f64::from(c_self.power);
1260                        let (_, x) = geometry::normalise_rotation_angle(
1261                            total_unormalised_proper_angle,
1262                            thresh,
1263                        );
1264                        x.rem_euclid(2) == 1
1265                    });
1266                let single_jump_from_c2 = (c_self.is_spatial_binary_rotation()
1267                    || c_self.is_spatial_reflection())
1268                    && !self
1269                        .positive_hemisphere
1270                        .as_ref()
1271                        .cloned()
1272                        .unwrap_or_default()
1273                        .check_positive_pole(c_self.generating_element.raw_axis(), thresh);
1274                odd_jumps_from_angle != single_jump_from_c2
1275            };
1276
1277            let intrinsic_inverse = c_self.generating_element.rotation_group().is_su2_class_1()
1278                && c_self.power.rem_euclid(2) == 1;
1279
1280            let inverse_count = [
1281                inverse_from_time_reversal,
1282                inverse_from_rotation_group,
1283                intrinsic_inverse,
1284            ]
1285            .into_iter()
1286            .filter(|&inverse| inverse)
1287            .count();
1288
1289            inverse_count.rem_euclid(2) == 1
1290        } else {
1291            false
1292        }
1293    }
1294}
1295
1296impl fmt::Debug for SymmetryOperation {
1297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1298        if self.power == 1 {
1299            write!(f, "{:?}", self.generating_element)
1300        } else if self.power >= 0 {
1301            write!(f, "[{:?}]^{}", self.generating_element, self.power)
1302        } else {
1303            write!(f, "[{:?}]^({})", self.generating_element, self.power)
1304        }
1305    }
1306}
1307
1308impl fmt::Display for SymmetryOperation {
1309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1310        if self.power == 1 {
1311            write!(f, "{}", self.generating_element)
1312        } else if self.power >= 0 {
1313            write!(f, "[{}]^{}", self.generating_element, self.power)
1314        } else {
1315            write!(f, "[{}]^({})", self.generating_element, self.power)
1316        }
1317    }
1318}
1319
1320impl PartialEq for SymmetryOperation {
1321    fn eq(&self, other: &Self) -> bool {
1322        if (*self.generating_element.raw_proper_order() == ElementOrder::Inf)
1323            != (*other.generating_element.raw_proper_order() == ElementOrder::Inf)
1324        {
1325            // We disable comparisons between operations with infinite-order and
1326            // finite-order generating elements, because they cannot be made to
1327            // have the same hashes without losing the fidelity of exact-fraction
1328            // representations for operations with finite-order generating elements.
1329            return false;
1330        }
1331
1332        // =================
1333        // Group-theoretical
1334        // =================
1335
1336        if self.is_su2() != other.is_su2() {
1337            return false;
1338        }
1339
1340        if self.is_su2_class_1() != other.is_su2_class_1() {
1341            return false;
1342        }
1343
1344        // ==========================
1345        // Special general operations
1346        // ==========================
1347
1348        if self.is_proper() != other.is_proper() {
1349            return false;
1350        }
1351
1352        if self.contains_time_reversal() != other.contains_time_reversal() {
1353            return false;
1354        }
1355
1356        // ===========================
1357        // Special specific operations
1358        // ===========================
1359
1360        // At this stage, `self` and `other` must have the same spatial parity, unitarity, and
1361        // SO3/SU2 properties.
1362        if self.is_spatial_identity() && other.is_spatial_identity() {
1363            // assert_eq!(misc::calculate_hash(self), misc::calculate_hash(other));
1364            // return true;
1365            return misc::calculate_hash(self) == misc::calculate_hash(other);
1366        }
1367
1368        // ======
1369        // Others
1370        // ======
1371
1372        let thresh =
1373            (self.generating_element.threshold * other.generating_element.threshold).sqrt();
1374
1375        let result = if (self.is_spatial_binary_rotation() && other.is_spatial_binary_rotation())
1376            || (self.is_spatial_reflection() && other.is_spatial_reflection())
1377        {
1378            approx::relative_eq!(
1379                self.calc_pole(),
1380                other.calc_pole(),
1381                epsilon = thresh,
1382                max_relative = thresh
1383            )
1384        } else {
1385            let c_self = if self.is_proper() {
1386                self.clone()
1387            } else {
1388                // Time-reversal does not matter here.
1389                self.convert_to_improper_kind(&INV)
1390            };
1391            let c_other = if other.is_proper() {
1392                other.clone()
1393            } else {
1394                // Time-reversal does not matter here.
1395                other.convert_to_improper_kind(&INV)
1396            };
1397
1398            let angle_comparison = if let (Some(s_frac), Some(o_frac)) =
1399                (c_self.total_proper_fraction, c_other.total_proper_fraction)
1400            {
1401                s_frac.abs() == o_frac.abs()
1402            } else {
1403                approx::relative_eq!(
1404                    c_self.total_proper_angle.abs(),
1405                    c_other.total_proper_angle.abs(),
1406                    epsilon = thresh,
1407                    max_relative = thresh
1408                )
1409            };
1410
1411            angle_comparison
1412                && approx::relative_eq!(
1413                    self.calc_pole(),
1414                    other.calc_pole(),
1415                    epsilon = thresh,
1416                    max_relative = thresh
1417                )
1418        };
1419
1420        // if result {
1421        //     assert_eq!(
1422        //         misc::calculate_hash(self),
1423        //         misc::calculate_hash(other),
1424        //         "`{self}` and `{other}` have unequal hashes.",
1425        //     );
1426        // }
1427        result && (misc::calculate_hash(self) == misc::calculate_hash(other))
1428    }
1429}
1430
1431impl Eq for SymmetryOperation {}
1432
1433impl Hash for SymmetryOperation {
1434    fn hash<H: Hasher>(&self, state: &mut H) {
1435        let c_self = match self.generating_element.kind {
1436            SymmetryElementKind::Proper(_) | SymmetryElementKind::ImproperInversionCentre(_) => {
1437                self.clone()
1438            }
1439            SymmetryElementKind::ImproperMirrorPlane(_) => self.convert_to_improper_kind(&INV),
1440        };
1441        // ==========================
1442        // Special general operations
1443        // ==========================
1444        c_self.is_proper().hash(state);
1445        c_self.contains_time_reversal().hash(state);
1446        c_self.is_su2().hash(state);
1447        c_self.is_su2_class_1().hash(state);
1448
1449        // ===========================
1450        // Special specific operations
1451        // ===========================
1452        if c_self.is_spatial_identity() {
1453            true.hash(state);
1454        } else {
1455            let pole = c_self.calc_pole();
1456            pole[0]
1457                .round_factor(c_self.generating_element.threshold)
1458                .integer_decode()
1459                .hash(state);
1460            pole[1]
1461                .round_factor(c_self.generating_element.threshold)
1462                .integer_decode()
1463                .hash(state);
1464            pole[2]
1465                .round_factor(c_self.generating_element.threshold)
1466                .integer_decode()
1467                .hash(state);
1468
1469            if !c_self.is_spatial_binary_rotation() && !c_self.is_spatial_reflection() {
1470                if let Some(frac) = c_self.total_proper_fraction {
1471                    // self.total_proper_fraction lies in (-1/2, 0) ∪ (0, 1/2).
1472                    // 0 and 1/2 are excluded because this is not an identity,
1473                    // inversion, binary rotation, or reflection.
1474                    frac.abs().hash(state);
1475                } else {
1476                    // self.total_proper_angle lies in (-π, 0) ∪ (0, π).
1477                    // 0 and π are excluded because this is not an identity,
1478                    // inversion, binary rotation, or reflection.
1479                    let abs_ang = c_self.total_proper_angle.abs();
1480                    abs_ang
1481                        .round_factor(c_self.generating_element.threshold)
1482                        .integer_decode()
1483                        .hash(state);
1484                };
1485            }
1486        };
1487    }
1488}
1489
1490impl Mul<&'_ SymmetryOperation> for &SymmetryOperation {
1491    type Output = SymmetryOperation;
1492
1493    fn mul(self, rhs: &SymmetryOperation) -> Self::Output {
1494        assert_eq!(
1495            self.is_su2(),
1496            rhs.is_su2(),
1497            "`self` and `rhs` must both have or not have associated spin rotations."
1498        );
1499        assert_eq!(self.positive_hemisphere, rhs.positive_hemisphere);
1500        let su2 = self.is_su2();
1501        let (q1_s, q1_v) = self.calc_quaternion();
1502        let (q2_s, q2_v) = rhs.calc_quaternion();
1503
1504        let q3_s = q1_s * q2_s - q1_v.dot(&q2_v);
1505        let q3_v = q1_s * q2_v + q2_s * q1_v + q1_v.cross(&q2_v);
1506
1507        // Is the resulting operation proper?
1508        let proper = self.is_proper() == rhs.is_proper();
1509
1510        // Does the resulting operation contain a time reversal?
1511        let tr = self.contains_time_reversal() != rhs.contains_time_reversal();
1512
1513        // Does the resulting operation pick up a quaternion sign change due to θ^2?
1514        let tr2 = self.contains_time_reversal() && rhs.contains_time_reversal();
1515
1516        let thresh = (self.generating_element.threshold * rhs.generating_element.threshold).sqrt();
1517        let max_trial_power = u32::MAX;
1518
1519        let q3 = if su2 {
1520            if tr2 { (-q3_s, -q3_v) } else { (q3_s, q3_v) }
1521        } else if q3_s >= 0.0 {
1522            (q3_s, q3_v)
1523        } else {
1524            (-q3_s, -q3_v)
1525        };
1526
1527        SymmetryOperation::from_quaternion(
1528            q3,
1529            proper,
1530            thresh,
1531            max_trial_power,
1532            tr,
1533            su2,
1534            self.positive_hemisphere.clone(),
1535        )
1536    }
1537}
1538
1539impl Mul<&'_ SymmetryOperation> for SymmetryOperation {
1540    type Output = SymmetryOperation;
1541
1542    fn mul(self, rhs: &SymmetryOperation) -> Self::Output {
1543        &self * rhs
1544    }
1545}
1546
1547impl Mul<SymmetryOperation> for SymmetryOperation {
1548    type Output = SymmetryOperation;
1549
1550    fn mul(self, rhs: SymmetryOperation) -> Self::Output {
1551        &self * &rhs
1552    }
1553}
1554
1555impl Mul<SymmetryOperation> for &SymmetryOperation {
1556    type Output = SymmetryOperation;
1557
1558    fn mul(self, rhs: SymmetryOperation) -> Self::Output {
1559        self * &rhs
1560    }
1561}
1562
1563impl Pow<i32> for &SymmetryOperation {
1564    type Output = SymmetryOperation;
1565
1566    fn pow(self, rhs: i32) -> Self::Output {
1567        SymmetryOperation::builder()
1568            .generating_element(self.generating_element.clone())
1569            .power(self.power * rhs)
1570            .positive_hemisphere(self.positive_hemisphere.clone())
1571            .build()
1572            .expect("Unable to construct a symmetry operation.")
1573    }
1574}
1575
1576impl Pow<i32> for SymmetryOperation {
1577    type Output = SymmetryOperation;
1578
1579    fn pow(self, rhs: i32) -> Self::Output {
1580        (&self).pow(rhs)
1581    }
1582}
1583
1584impl Inv for &SymmetryOperation {
1585    type Output = SymmetryOperation;
1586
1587    fn inv(self) -> Self::Output {
1588        SymmetryOperation::builder()
1589            .generating_element(self.generating_element.clone())
1590            .power(-self.power)
1591            .positive_hemisphere(self.positive_hemisphere.clone())
1592            .build()
1593            .expect("Unable to construct an inverse symmetry operation.")
1594    }
1595}
1596
1597impl Inv for SymmetryOperation {
1598    type Output = SymmetryOperation;
1599
1600    fn inv(self) -> Self::Output {
1601        (&self).inv()
1602    }
1603}
1604
1605impl<M> IntoPermutation<M> for SymmetryOperation
1606where
1607    M: Transform + PermutableCollection<Rank = usize>,
1608{
1609    fn act_permute(&self, rhs: &M) -> Option<Permutation<usize>> {
1610        let angle = self.calc_pole_angle();
1611        let axis = self.calc_pole().coords;
1612        let mut t_mol = if self.is_proper() {
1613            rhs.rotate(angle, &axis)
1614        } else {
1615            rhs.improper_rotate(angle, &axis, &IMINV)
1616        };
1617        if self.contains_time_reversal() {
1618            t_mol.reverse_time_mut();
1619        }
1620        rhs.get_perm_of(&t_mol)
1621    }
1622}
1623
1624// =================
1625// Utility functions
1626// =================
1627/// Sorts symmetry operations in-place based on:
1628///
1629/// * whether they are unitary or antiunitary
1630/// * whether they are proper or improper
1631/// * whether they are the identity or inversion
1632/// * whether they are a spatial binary rotation or spatial reflection
1633/// * their orders
1634/// * their powers
1635/// * their closeness to Cartesian axes
1636/// * the axes of closest inclination
1637/// * whether they are of homotopy class 1 in $`\mathsf{SU}'(2)`$.
1638///
1639/// # Arguments
1640///
1641/// * `operations` - A mutable reference to a vector of symmetry operations.
1642pub(crate) fn sort_operations(operations: &mut [SymmetryOperation]) {
1643    operations.sort_by_key(|op| {
1644        let (axis_closeness, closest_axis) = op.generating_element.closeness_to_cartesian_axes();
1645        let c_op = if op.is_proper()
1646            || op.generating_element.kind == SIG
1647            || op.generating_element.kind == TRSIG
1648        {
1649            op.clone()
1650        } else if op.contains_time_reversal() {
1651            op.convert_to_improper_kind(&TRSIG)
1652        } else {
1653            op.convert_to_improper_kind(&SIG)
1654        };
1655
1656        let total_proper_fraction = c_op
1657            .total_proper_fraction
1658            .expect("No total proper fractions found.");
1659        let denom = i64::from(
1660            *total_proper_fraction
1661                .denom()
1662                .expect("The denominator of the total proper fraction cannot be extracted."),
1663        );
1664        let numer = i64::from(
1665            *total_proper_fraction
1666                .numer()
1667                .expect("The numerator of the total proper fraction cannot be extracted."),
1668        );
1669
1670        let negative_rotation = !c_op
1671            .positive_hemisphere
1672            .as_ref()
1673            .cloned()
1674            .unwrap_or_default()
1675            .check_positive_pole(
1676                &c_op.calc_proper_rotation_pole().coords,
1677                c_op.generating_element.threshold(),
1678            );
1679
1680        (
1681            c_op.contains_time_reversal(),
1682            !c_op.is_proper(),
1683            !(c_op.is_spatial_identity() || c_op.is_spatial_inversion()),
1684            c_op.is_spatial_binary_rotation() || c_op.is_spatial_reflection(),
1685            -denom,
1686            negative_rotation,
1687            if negative_rotation { -numer } else { numer },
1688            OrderedFloat(axis_closeness),
1689            closest_axis,
1690            c_op.is_su2_class_1(),
1691        )
1692    });
1693}