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}