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