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}