qsym2/symmetry/symmetry_transformation/
mod.rs

1//! Transformations under symmetry operations.
2
3use std::error::Error;
4use std::fmt;
5
6use anyhow::format_err;
7use itertools::Itertools;
8use nalgebra::Vector3;
9use ndarray::{Array, Array2, Axis, RemoveAxis};
10use num_complex::Complex;
11#[cfg(feature = "python")]
12use pyo3::prelude::*;
13use serde::{Deserialize, Serialize};
14
15use crate::angmom::sh_conversion::{sh_cart2r, sh_r2cart};
16use crate::angmom::sh_rotation_3d::rlmat;
17use crate::angmom::spinor_rotation_3d::dmat_angleaxis;
18use crate::basis::ao::{
19    BasisAngularOrder, CartOrder, PureOrder, ShellOrder, SpinorBalanceSymmetry, SpinorOrder,
20    SpinorParticleType,
21};
22use crate::permutation::{PermutableCollection, Permutation};
23use crate::symmetry::symmetry_element::symmetry_operation::{
24    SpecialSymmetryTransformation, SymmetryOperation,
25};
26
27#[cfg(test)]
28#[path = "symmetry_transformation_tests.rs"]
29mod symmetry_transformation_tests;
30
31// ================
32// Enum definitions
33// ================
34
35/// Enumerated type for managing the kind of symmetry transformation on an object.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[cfg_attr(feature = "python", pyclass)]
38pub enum SymmetryTransformationKind {
39    /// Spatial-only transformation.
40    Spatial,
41
42    /// Spatial-only transformation but with spin-including time reversal.
43    SpatialWithSpinTimeReversal,
44
45    /// Spin-only transformation.
46    Spin,
47
48    /// Spin-spatial coupled transformation.
49    SpinSpatial,
50}
51
52impl Default for SymmetryTransformationKind {
53    fn default() -> Self {
54        SymmetryTransformationKind::Spatial
55    }
56}
57
58impl fmt::Display for SymmetryTransformationKind {
59    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60        match self {
61            Self::Spatial => write!(f, "Spatial-only transformation"),
62            Self::SpatialWithSpinTimeReversal => write!(
63                f,
64                "Spatial-only transformation but with spin-including time reversal"
65            ),
66            Self::Spin => write!(f, "Spin-only transformation"),
67            Self::SpinSpatial => write!(f, "Spin-spatial coupled transformation"),
68        }
69    }
70}
71
72// =================
73// Trait definitions
74// =================
75
76#[derive(Debug, Clone)]
77pub struct TransformationError(pub String);
78
79impl fmt::Display for TransformationError {
80    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
81        write!(f, "Transformation error: {}", self.0)
82    }
83}
84
85impl Error for TransformationError {}
86
87/// Trait for spatial unitary transformation. A spatial unitary transformation also permutes
88/// off-origin sites.
89pub trait SpatialUnitaryTransformable: Clone {
90    // ----------------
91    // Required methods
92    // ----------------
93    /// Performs a spatial transformation in-place.
94    ///
95    /// # Arguments
96    ///
97    /// * `rmat` - The three-dimensional representation matrix of the transformation in the basis
98    /// of coordinate *functions* $`(y, z, x)`$.
99    /// * `perm` - An optional permutation describing how any off-origin sites are permuted amongst
100    /// each other under the transformation.
101    fn transform_spatial_mut(
102        &mut self,
103        rmat: &Array2<f64>,
104        perm: Option<&Permutation<usize>>,
105    ) -> Result<&mut Self, TransformationError>;
106
107    // ----------------
108    // Provided methods
109    // ----------------
110    /// Performs a spatial transformation and returns the transformed result.
111    ///
112    /// # Arguments
113    ///
114    /// * `rmat` - The three-dimensional representation matrix of the transformation in the basis
115    /// of coordinate *functions* $`(y, z, x)`$.
116    ///
117    /// # Returns
118    ///
119    /// The transformed result.
120    fn transform_spatial(
121        &self,
122        rmat: &Array2<f64>,
123        perm: Option<&Permutation<usize>>,
124    ) -> Result<Self, TransformationError> {
125        let mut tself = self.clone();
126        tself.transform_spatial_mut(rmat, perm)?;
127        Ok(tself)
128    }
129}
130
131/// Trait for spin unitary transformations. A spin unitary transformation has no spatial effects.
132pub trait SpinUnitaryTransformable: Clone {
133    // ----------------
134    // Required methods
135    // ----------------
136    /// Performs a spin transformation in-place.
137    ///
138    /// # Arguments
139    ///
140    /// * `dmat` - The two-dimensional representation matrix of the transformation in the basis of
141    /// the $`\{ \alpha, \beta \}`$ spinors (*i.e.* decreasing $`m`$ order).
142    fn transform_spin_mut(
143        &mut self,
144        dmat: &Array2<Complex<f64>>,
145    ) -> Result<&mut Self, TransformationError>;
146
147    // ----------------
148    // Provided methods
149    // ----------------
150    /// Performs a spin transformation and returns the transformed result.
151    ///
152    /// # Arguments
153    ///
154    /// * `dmat` - The two-dimensional representation matrix of the transformation in the basis of
155    /// the $`\{ \alpha, \beta \}`$ spinors (*i.e.* decreasing $`m`$ order).
156    ///
157    /// # Returns
158    ///
159    /// The transformed result.
160    fn transform_spin(&self, dmat: &Array2<Complex<f64>>) -> Result<Self, TransformationError> {
161        let mut tself = self.clone();
162        tself.transform_spin_mut(dmat)?;
163        Ok(tself)
164    }
165}
166
167/// Trait for complex-conjugation transformations.
168pub trait ComplexConjugationTransformable: Clone {
169    // ----------------
170    // Required methods
171    // ----------------
172    /// Performs a complex conjugation in-place.
173    fn transform_cc_mut(&mut self) -> Result<&mut Self, TransformationError>;
174
175    // ----------------
176    // Provided methods
177    // ----------------
178    /// Performs a complex conjugation and returns the complex-conjugated result.
179    ///
180    /// # Returns
181    ///
182    /// The complex-conjugated result.
183    fn transform_cc(&self) -> Result<Self, TransformationError> {
184        let mut tself = self.clone();
185        tself.transform_cc_mut()?;
186        Ok(tself)
187    }
188}
189
190/// Trait for time-reversal transformations.
191///
192/// This trait has a blanket implementation for any implementor of the [`SpinUnitaryTransformable`]
193/// trait and the [`ComplexConjugationTransformable`] trait together with the
194/// [`DefaultTimeReversalTransformable`] marker trait.
195pub trait TimeReversalTransformable: ComplexConjugationTransformable {
196    // ----------------
197    // Required methods
198    // ----------------
199    /// Performs a time-reversal transformation in-place.
200    fn transform_timerev_mut(&mut self) -> Result<&mut Self, TransformationError>;
201
202    // ----------------
203    // Provided methods
204    // ----------------
205    /// Performs a time-reversal transformation and returns the time-reversed result.
206    ///
207    /// # Returns
208    ///
209    /// The time-reversed result.
210    fn transform_timerev(&self) -> Result<Self, TransformationError> {
211        let mut tself = self.clone();
212        tself.transform_timerev_mut()?;
213        Ok(tself)
214    }
215}
216
217/// Trait for transformations using [`SymmetryOperation`].
218pub trait SymmetryTransformable:
219    SpatialUnitaryTransformable + SpinUnitaryTransformable + TimeReversalTransformable
220{
221    // ----------------
222    // Required methods
223    // ----------------
224    /// Determines the permutation of sites (*e.g.* atoms in molecules) due to the action of a
225    /// symmetry operation.
226    ///
227    /// # Arguments
228    ///
229    /// * `symop` - A symmetry operation.
230    ///
231    /// # Returns
232    ///
233    /// The resultant site permutation under the action of `symop`, or an error if no such
234    /// permutation can be found.
235    fn sym_permute_sites_spatial(
236        &self,
237        symop: &SymmetryOperation,
238    ) -> Result<Permutation<usize>, TransformationError>;
239
240    // ----------------
241    // Provided methods
242    // ----------------
243    /// Performs a spatial transformation according to a specified symmetry operation in-place.
244    ///
245    /// Note that both $`\mathsf{SO}(3)`$ and $`\mathsf{SU}(2)`$ rotations effect the same spatial
246    /// transformation. Also note that, if the transformation contains time reversal, it will be
247    /// accompanied by a complex conjugation.
248    ///
249    /// # Arguments
250    ///
251    /// * `symop` - A symmetry operation.
252    fn sym_transform_spatial_mut(
253        &mut self,
254        symop: &SymmetryOperation,
255    ) -> Result<&mut Self, TransformationError> {
256        let rmat = symop.get_3d_spatial_matrix();
257        let perm = self.sym_permute_sites_spatial(symop)?;
258        self.transform_spatial_mut(&rmat, Some(&perm))
259            .map_err(|err| TransformationError(err.to_string()))?;
260        if symop.contains_time_reversal() {
261            self.transform_cc_mut()
262        } else {
263            Ok(self)
264        }
265    }
266
267    /// Performs a spatial transformation according to a specified symmetry operation and returns
268    /// the transformed result.
269    ///
270    /// Note that both $`\mathsf{SO}(3)`$ and $`\mathsf{SU}(2)`$ rotations effect the same spatial
271    /// transformation. Also note that, if the transformation contains time reversal, it will be
272    /// accompanied by a complex conjugation.
273    ///
274    /// # Arguments
275    ///
276    /// * `symop` - A symmetry operation.
277    ///
278    /// # Returns
279    ///
280    /// The transformed result.
281    fn sym_transform_spatial(
282        &self,
283        symop: &SymmetryOperation,
284    ) -> Result<Self, TransformationError> {
285        let mut tself = self.clone();
286        tself.sym_transform_spatial_mut(symop)?;
287        Ok(tself)
288    }
289
290    /// Performs a spatial transformation according to a specified symmetry operation in-place, but
291    /// with spin-including time reversal.
292    ///
293    /// Note that both $`\mathsf{SO}(3)`$ and $`\mathsf{SU}(2)`$ rotations effect the same spatial
294    /// transformation. Also note that, if the transformation contains time reversal, it will be
295    /// accompanied by a rotation by $`\pi`$ about the space-fixed $`y`$-axis followed by a complex
296    /// conjugation.
297    ///
298    /// # Arguments
299    ///
300    /// * `symop` - A symmetry operation.
301    fn sym_transform_spatial_with_spintimerev_mut(
302        &mut self,
303        symop: &SymmetryOperation,
304    ) -> Result<&mut Self, TransformationError> {
305        let rmat = symop.get_3d_spatial_matrix();
306        let perm = self.sym_permute_sites_spatial(symop)?;
307        self.transform_spatial_mut(&rmat, Some(&perm))
308            .map_err(|err| TransformationError(err.to_string()))?;
309        if symop.contains_time_reversal() {
310            self.transform_timerev_mut()?;
311        }
312        Ok(self)
313    }
314
315    /// Performs a spatial transformation according to a specified symmetry operation but with
316    /// spin-including time reversal and returns the transformed result.
317    ///
318    /// Note that both $`\mathsf{SO}(3)`$ and $`\mathsf{SU}(2)`$ rotations effect the same spatial
319    /// transformation. Also note that, if the transformation contains time reversal, it will be
320    /// accompanied by a rotation by $`\pi`$ about the space-fixed $`y`$-axis followed by a complex
321    /// conjugation.
322    ///
323    /// # Arguments
324    ///
325    /// * `symop` - A symmetry operation.
326    ///
327    /// # Returns
328    ///
329    /// The transformed result.
330    fn sym_transform_spatial_with_spintimerev(
331        &self,
332        symop: &SymmetryOperation,
333    ) -> Result<Self, TransformationError> {
334        let mut tself = self.clone();
335        tself.sym_transform_spatial_with_spintimerev_mut(symop)?;
336        Ok(tself)
337    }
338
339    /// Performs a spin transformation according to a specified symmetry operation in-place.
340    ///
341    /// Note that only $`\mathsf{SU}(2)`$ rotations can effect spin transformations. Also note
342    /// that, if the transformation contains a time reversal, the corresponding explicit time
343    /// reveral action will also be carried out.
344    ///
345    /// # Arguments
346    ///
347    /// * `symop` - A symmetry operation.
348    fn sym_transform_spin_mut(
349        &mut self,
350        symop: &SymmetryOperation,
351    ) -> Result<&mut Self, TransformationError> {
352        if symop.is_su2() {
353            let angle = symop.calc_pole_angle();
354            let axis = symop.calc_pole().coords;
355            let dmat = if symop.is_su2_class_1() {
356                -dmat_angleaxis(angle, axis, false)
357            } else {
358                dmat_angleaxis(angle, axis, false)
359            };
360            self.transform_spin_mut(&dmat)?;
361        }
362        if symop.contains_time_reversal() {
363            self.transform_timerev_mut()?;
364        }
365        Ok(self)
366    }
367
368    /// Performs a spin transformation according to a specified symmetry operation and returns the
369    /// transformed result.
370    ///
371    /// Note that only $`\mathsf{SU}(2)`$ rotations can effect spin transformations. Also note
372    /// that, if the transformation is antiunitary, it will be accompanied by a time reversal.
373    ///
374    /// # Arguments
375    ///
376    /// * `symop` - A symmetry operation.
377    ///
378    /// # Returns
379    ///
380    /// The transformed result.
381    fn sym_transform_spin(&self, symop: &SymmetryOperation) -> Result<Self, TransformationError> {
382        let mut tself = self.clone();
383        tself.sym_transform_spin_mut(symop)?;
384        Ok(tself)
385    }
386
387    /// Performs a coupled spin-spatial transformation according to a specified symmetry operation
388    /// in-place.
389    ///
390    /// Note that only $`\mathsf{SU}(2)`$ rotations can effect spin transformations.
391    ///
392    /// # Arguments
393    ///
394    /// * `symop` - A symmetry operation.
395    fn sym_transform_spin_spatial_mut(
396        &mut self,
397        symop: &SymmetryOperation,
398    ) -> Result<&mut Self, TransformationError> {
399        // We cannot do the following, because each of the two methods carries out its own
400        // antiunitary action, so we'd be double-acting the antiunitary action.
401        // self.sym_transform_spatial_mut(symop)?
402        //     .sym_transform_spin_mut(symop)
403
404        // Spatial
405        let rmat = symop.get_3d_spatial_matrix();
406        let perm = self.sym_permute_sites_spatial(symop)?;
407        self.transform_spatial_mut(&rmat, Some(&perm))
408            .map_err(|err| TransformationError(err.to_string()))?;
409
410        // Spin -- only SU(2) rotations can effect spin transformations.
411        if symop.is_su2() {
412            let angle = symop.calc_pole_angle();
413            let axis = symop.calc_pole().coords;
414            let dmat = if symop.is_su2_class_1() {
415                -dmat_angleaxis(angle, axis, false)
416            } else {
417                dmat_angleaxis(angle, axis, false)
418            };
419            self.transform_spin_mut(&dmat)?;
420        }
421
422        // Time reversal, if any.
423        if symop.contains_time_reversal() {
424            self.transform_timerev_mut()?;
425        }
426        Ok(self)
427    }
428
429    /// Performs a coupled spin-spatial transformation according to a specified symmetry operation
430    /// and returns the transformed result.
431    ///
432    /// Note that only $`\mathsf{SU}(2)`$ rotations can effect spin transformations.
433    ///
434    /// # Arguments
435    ///
436    /// * `symop` - A symmetry operation.
437    ///
438    /// # Returns
439    ///
440    /// The transformed result.
441    fn sym_transform_spin_spatial(
442        &self,
443        symop: &SymmetryOperation,
444    ) -> Result<Self, TransformationError> {
445        let mut tself = self.clone();
446        tself.sym_transform_spin_spatial_mut(symop)?;
447        Ok(tself)
448    }
449}
450
451// ----------------------
452// Blanket implementation
453// ----------------------
454
455/// Marker trait indicating that the implementing type should get the blanket implementation for
456/// [`TimeReversalTransformable`].
457pub trait DefaultTimeReversalTransformable {}
458
459impl<T> TimeReversalTransformable for T
460where
461    T: DefaultTimeReversalTransformable
462        + SpinUnitaryTransformable
463        + ComplexConjugationTransformable,
464{
465    /// Performs a time-reversal transformation in-place.
466    ///
467    /// The default implementation of the time-reversal transformation for any type that implements
468    /// [`SpinUnitaryTransformable`] and [`ComplexConjugationTransformable`] is a spin rotation by
469    /// $`\pi`$ about the space-fixed $`y`$-axis followed by a complex conjugation.
470    fn transform_timerev_mut(&mut self) -> Result<&mut Self, TransformationError> {
471        let dmat_y = dmat_angleaxis(std::f64::consts::PI, Vector3::y(), false);
472        self.transform_spin_mut(&dmat_y)?.transform_cc_mut()
473    }
474}
475
476// =========
477// Functions
478// =========
479
480/// Permutes the generalised rows of an array along one or more dimensions.
481///
482/// Each generalised row corresponds to a basis function, and consecutive generalised rows
483/// corresponding to basis functions localised on a single atom are grouped together and then
484/// permuted according to the permutation of the atoms.
485///
486/// # Arguments
487///
488/// * `arr` - A coefficient array of any dimensions.
489/// * `atom_perm` - A permutation for the atoms.
490/// * `axes` - The dimensions along which the generalised rows are to be permuted. The number of
491/// generalised rows along each of these dimensions *must* be equal to the number of functions in
492/// the basis.
493/// * `bao` - A structure specifying the angular order of the underlying basis.
494///
495/// # Returns
496///
497/// The permuted array.
498///
499/// # Panics
500///
501/// Panics if the number of generalised rows along any of the dimensions in `axes` does not match
502/// the number of functions in the basis, or if the permutation rank does not match the number of
503/// atoms in the basis.
504pub(crate) fn permute_array_by_atoms<T, D>(
505    arr: &Array<T, D>,
506    atom_perm: &Permutation<usize>,
507    axes: &[Axis],
508    bao: &BasisAngularOrder,
509) -> Array<T, D>
510where
511    D: RemoveAxis,
512    T: Clone,
513{
514    assert_eq!(
515        atom_perm.rank(),
516        bao.n_atoms(),
517        "The rank of permutation does not match the number of atoms in the basis."
518    );
519    let atom_boundary_indices = bao.atom_boundary_indices();
520    let permuted_shell_indices: Vec<usize> = atom_perm
521        .image()
522        .iter()
523        .flat_map(|&i| {
524            let (shell_min, shell_max) = atom_boundary_indices[i];
525            shell_min..shell_max
526        })
527        .collect();
528
529    let mut r = arr.clone();
530    for axis in axes {
531        assert_eq!(
532            arr.shape()[axis.0],
533            bao.n_funcs(),
534            "The number of generalised rows along {axis:?} in the given array does not match the number of basis functions, {}.",
535            bao.n_funcs()
536        );
537        r = r.select(*axis, &permuted_shell_indices);
538    }
539    r
540}
541
542/// Assembles spherical-harmonic rotation matrices for all shells.
543///
544/// # Arguments
545///
546/// * `baos` - Structure specifying the angular order of the underlying basis, one for each
547/// explicit component per coefficient matrix.
548/// * `rmat` - The three-dimensional representation matrix of the transformation in the basis
549/// of coordinate *functions* $`(y, z, x)`$.
550/// * `perm` - An optional permutation describing how any off-origin sites are permuted amongst
551/// each other under the transformation.
552///
553/// # Returns
554///
555/// A vector of vectors of spherical-harmonic rotation matrices. Each inner vector is for each
556/// shells in `bao`, and each outer vector is for each explicit component per coefficient matrix.
557/// Non-standard orderings of functions in shells are taken into account.
558pub fn assemble_sh_rotation_3d_matrices(
559    baos: &[&BasisAngularOrder],
560    rmat: &Array2<f64>,
561    perm: Option<&Permutation<usize>>,
562) -> Result<Vec<Vec<Array2<f64>>>, anyhow::Error> {
563    let rmatss_res = baos.iter().map(|&bao| {
564        let pbao = if let Some(p) = perm {
565            bao.permute(p)?
566        } else {
567            bao.clone()
568        };
569        let mut rls = vec![Array2::<f64>::eye(1), rmat.clone()];
570        let lmax = pbao
571            .basis_shells()
572            .map(|shl| shl.l)
573            .max()
574            .expect("The maximum angular momentum cannot be found.");
575        for l in 2..=lmax {
576            let rl = rlmat(
577                l,
578                rmat,
579                rls.last()
580                    .expect("The representation matrix for the last angular momentum cannot be found."),
581            );
582            rls.push(rl);
583        }
584
585        // All matrices in `rls` are in increasing-m order by default. See the function `rlmat` for
586        // the origin of this order. Hence, conversion matrices must also honour this.
587        let cart2rss_lex: Vec<Vec<Array2<f64>>> = (0..=lmax)
588            .map(|lcart| sh_cart2r(lcart, &CartOrder::lex(lcart), true, PureOrder::increasingm))
589            .collect();
590        let r2cartss_lex: Vec<Vec<Array2<f64>>> = (0..=lmax)
591            .map(|lcart| sh_r2cart(lcart, &CartOrder::lex(lcart), true, PureOrder::increasingm))
592            .collect();
593
594        let rmats_res = pbao.basis_shells()
595            .map(|shl| {
596                let l = usize::try_from(shl.l).unwrap_or_else(|_| {
597                    panic!(
598                        "Unable to convert the angular momentum order `{}` to `usize`.",
599                        shl.l
600                    );
601                });
602                let po_il = PureOrder::increasingm(shl.l);
603                match &shl.shell_order {
604                    ShellOrder::Pure(pureorder) => {
605                        // Spherical functions.
606                        let rl = rls[l].clone();
607                        if *pureorder != po_il {
608                            // `rl` is in increasing-m order by default. See the function `rlmat` for
609                            // the origin of this order.
610                            let perm = pureorder
611                                .get_perm_of(&po_il)
612                                .expect("Unable to obtain the permutation that maps `pureorder` to the increasing order.");
613                            Ok(rl.select(Axis(0), &perm.image()).select(Axis(1), &perm.image()))
614                        } else {
615                            Ok(rl)
616                        }
617                    }
618                    ShellOrder::Cart(cart_order) => {
619                        // Cartesian functions. Convert them to real solid harmonics first, then
620                        // applying the transformation, then convert back.
621                        // The actual Cartesian order will be taken into account.
622
623                        // Perform the conversion using lexicographic order first. This allows for the
624                        // conversion matrices to be computed only once in the lexicographic order.
625                        let cart2rs = &cart2rss_lex[l];
626                        let r2carts = &r2cartss_lex[l];
627                        let rl = cart2rs.iter().zip(r2carts.iter()).enumerate().fold(
628                            Array2::zeros((cart_order.ncomps(), cart_order.ncomps())),
629                            |acc, (i, (xmat, wmat))| {
630                                let lpure = l - 2 * i;
631                                acc + wmat.dot(&rls[lpure]).dot(xmat)
632                            },
633                        );
634                        let lex_cart_order = CartOrder::lex(shl.l);
635
636                        // Now deal with the actual Cartesian order by permutations.
637                        if *cart_order != lex_cart_order {
638                            // `rl` is in lexicographic order (because of `wmat` and `xmat`) by default.
639                            // Consider a transformation R and its representation matrix D in a
640                            // lexicographically-ordered Cartesian basis b collected in a row vector.
641                            // Then,
642                            //      R b = b D.
643                            // If we now permute the basis functions in b by a permutation π, then the
644                            // representation matrix for R changes:
645                            //      R πb = πb D(π).
646                            // To relate D(π) to D, we first note the representation matrix for π, P:
647                            //      πb = π b = b P,
648                            // which, when acts on a left row vector, permutes its entries normally, but
649                            // when acts on a right column vector, permutes its entries inversely.
650                            // Then,
651                            //      R πb = R b P = b P D(π) => R b = b PD(π)P^(-1).
652                            // Thus,
653                            //      D(π) = P^(-1)DP,
654                            // i.e., to obtain D(π), we permute the rows and columns of D normally
655                            // according to π.
656                            let perm = lex_cart_order
657                                .get_perm_of(cart_order)
658                                .unwrap_or_else(
659                                    || panic!("Unable to find a permutation to map `{lex_cart_order}` to `{cart_order}`.")
660                                );
661                            Ok(rl.select(Axis(0), perm.image())
662                                .select(Axis(1), perm.image()))
663                        } else {
664                            Ok(rl)
665                        }
666                    }
667                    ShellOrder::Spinor(_) => Err(format_err!("Spinor shells cannot have transformation matrices with spherical-harmonic bases."))
668                }
669            })
670            .collect::<Result<Vec<Array2<f64>>, _>>();
671        rmats_res
672    }).collect::<Result<Vec<_>, _>>();
673    rmatss_res
674}
675
676/// Assembles spinor rotation matrices for all shells, which also include the unitary part of time
677/// reversal, if any. This also handles whether a spinor shell is even or odd with respect to
678/// spatial inversion.
679///
680/// # Arguments
681///
682/// * `baos` - Structure specifying the angular order of the underlying basis, one for each
683/// explicit component per coefficient matrix.
684/// * `symop` - A symmetry operation representing the transformation.
685/// * `perm` - An optional permutation describing how any off-origin sites are permuted amongst
686/// each other under the transformation.
687///
688/// # Returns
689///
690/// A vector of vectors of spinor rotation matrices. Each inner vector is for each shells in `bao`,
691/// and each outer vector is for each explicit component per coefficient matrix. Non-standard
692/// orderings of functions in shells are taken into account.
693pub(crate) fn assemble_spinor_rotation_matrices(
694    baos: &[&BasisAngularOrder],
695    symop: &SymmetryOperation,
696    perm: Option<&Permutation<usize>>,
697) -> Result<Vec<Vec<Array2<Complex<f64>>>>, anyhow::Error> {
698    let rmatss_res = baos.iter().map(|&bao| {
699        let pbao = if let Some(p) = perm {
700            bao.permute(p)?
701        } else {
702            bao.clone()
703        };
704        let two_j_max = pbao
705            .basis_shells()
706            .map(|shl| match shl.shell_order {
707                ShellOrder::Pure(_) => 2 * shl.l,
708                ShellOrder::Cart(_) => 2 * shl.l,
709                ShellOrder::Spinor(_) => shl.l,
710            })
711            .max()
712            .expect("The maximum rank cannot be found.");
713        let r2js = (0..=two_j_max)
714            .map(|two_j| symop.get_wigner_matrix(two_j, true))
715            .collect::<Result<Vec<_>, _>>()?;
716
717        // All matrices in `rls` are in increasing-m order by default. See the function `get_wigner_matrix` for
718        // the origin of this order. Hence, conversion matrices must also honour this.
719        let cart2rss_lex: Vec<Vec<Array2<Complex<f64>>>> = (0..=two_j_max)
720            .step_by(2)
721            .map(|two_lcart| {
722                sh_cart2r(
723                    two_lcart.div_euclid(2),
724                    &CartOrder::lex(two_lcart.div_euclid(2)),
725                    true,
726                    PureOrder::increasingm,
727                )
728                .into_iter()
729                .map(|mat| mat.mapv(Complex::<f64>::from))
730                .collect_vec()
731            })
732            .collect();
733        let r2cartss_lex: Vec<Vec<Array2<Complex<f64>>>> = (0..=two_j_max)
734            .map(|two_lcart| {
735                sh_r2cart(
736                    two_lcart.div_euclid(2),
737                    &CartOrder::lex(two_lcart.div_euclid(2)),
738                    true,
739                    PureOrder::increasingm,
740                )
741                .into_iter()
742                .map(|mat| mat.mapv(Complex::<f64>::from))
743                .collect_vec()
744            })
745            .collect();
746
747        let rmats_res = pbao.basis_shells()
748            .map(|shl| {
749                let l =
750                    usize::try_from(shl.l).unwrap_or_else(|_| {
751                    panic!(
752                        "Unable to convert the rank `{}` to `usize`.",
753                        shl.l
754                    );
755                });
756                match &shl.shell_order {
757                    ShellOrder::Pure(pure_order) => {
758                        // Spherical functions.
759                        let po_il = PureOrder::increasingm(shl.l);
760                        let rl = r2js[2*l].clone();
761                        if *pure_order != po_il {
762                            // `rl` is in increasing-m order by default. See the function `rlmat` for
763                            // the origin of this order.
764                            let perm = pure_order
765                                .get_perm_of(&po_il)
766                                .expect("Unable to obtain the permutation that maps `pureorder` to the increasing order.");
767                            Ok(rl.select(Axis(0), &perm.image()).select(Axis(1), &perm.image()))
768                        } else {
769                            Ok(rl)
770                        }
771                    }
772                    ShellOrder::Cart(cart_order) => {
773                        // Cartesian functions. Convert them to real solid harmonics first, then
774                        // applying the transformation, then convert back.
775                        // The actual Cartesian order will be taken into account.
776
777                        // Perform the conversion using lexicographic order first. This allows for the
778                        // conversion matrices to be computed only once in the lexicographic order.
779                        let cart2rs = &cart2rss_lex[l];
780                        let r2carts = &r2cartss_lex[l];
781                        let rl = cart2rs.iter().zip(r2carts.iter()).enumerate().fold(
782                            Array2::zeros((cart_order.ncomps(), cart_order.ncomps())),
783                            |acc, (i, (xmat, wmat))| {
784                                let lpure = l - 2 * i;
785                                acc + wmat.dot(&r2js[2*lpure]).dot(xmat)
786                            },
787                        );
788                        let lex_cart_order = CartOrder::lex(shl.l);
789
790                        // Now deal with the actual Cartesian order by permutations.
791                        if *cart_order != lex_cart_order {
792                            // `rl` is in lexicographic order (because of `wmat` and `xmat`) by default.
793                            // Consider a transformation R and its representation matrix D in a
794                            // lexicographically-ordered Cartesian basis b collected in a row vector.
795                            // Then,
796                            //      R b = b D.
797                            // If we now permute the basis functions in b by a permutation π, then the
798                            // representation matrix for R changes:
799                            //      R πb = πb D(π).
800                            // To relate D(π) to D, we first note the representation matrix for π, P:
801                            //      πb = π b = b P,
802                            // which, when acts on a left row vector, permutes its entries normally, but
803                            // when acts on a right column vector, permutes its entries inversely.
804                            // Then,
805                            //      R πb = R b P = b P D(π) => R b = b PD(π)P^(-1).
806                            // Thus,
807                            //      D(π) = P^(-1)DP,
808                            // i.e., to obtain D(π), we permute the rows and columns of D normally
809                            // according to π.
810                            let perm = lex_cart_order
811                                .get_perm_of(cart_order)
812                                .unwrap_or_else(
813                                    || panic!("Unable to find a permutation to map `{lex_cart_order}` to `{cart_order}`.")
814                                );
815                            Ok(rl.select(Axis(0), perm.image())
816                                .select(Axis(1), perm.image()))
817                        } else {
818                            Ok(rl)
819                        }
820                    }
821                    ShellOrder::Spinor(spinor_order) => {
822                        // Spinor functions. l = two_j.
823                        let so_il = SpinorOrder::increasingm(shl.l, spinor_order.even, spinor_order.particle_type.clone());
824                        let r2j_raw = r2js[l].clone();
825                        let r2j = if *spinor_order != so_il {
826                            // `rl` is in increasing-m order by default. See the function `rlmat` for
827                            // the origin of this order.
828                            let perm = spinor_order
829                                .get_perm_of(&so_il)
830                                .expect("Unable to obtain the permutation that maps `spinor_order` to the increasing order.");
831                            if !so_il.even && !symop.is_proper() {
832                                -r2j_raw.select(Axis(0), &perm.image()).select(Axis(1), &perm.image())
833                            } else {
834                                r2j_raw.select(Axis(0), &perm.image()).select(Axis(1), &perm.image())
835                            }
836                        } else {
837                            if !spinor_order.even && !symop.is_proper() {
838                                -r2j_raw
839                            } else {
840                                r2j_raw
841                            }
842                        };
843
844                        // Intrinsic parity of fermion is +1 and antifermion -1.
845                        match &spinor_order.particle_type {
846                            SpinorParticleType::Fermion(None)
847                            | SpinorParticleType::Antifermion(Some(SpinorBalanceSymmetry::KineticBalance)) => {
848                                Ok(r2j)
849                            },
850                            SpinorParticleType::Fermion(Some(SpinorBalanceSymmetry::KineticBalance))
851                            | SpinorParticleType::Antifermion(None) => {
852                                if symop.is_proper() {
853                                    Ok(r2j)
854                                } else {
855                                    Ok(-r2j)
856                                }
857                            },
858                        }
859                    }
860                }
861            })
862            .collect::<Result<Vec<Array2<Complex<f64>>>, _>>();
863        rmats_res
864    }).collect::<Result<Vec<_>, _>>();
865    rmatss_res
866}