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}