qsym2/target/orbital/
orbital_transformation.rs

1//! Implementation of symmetry transformations for orbitals.
2
3use std::ops::Mul;
4
5use approx;
6use nalgebra::Vector3;
7use ndarray::{array, concatenate, s, Array2, Axis, LinalgScalar, ScalarOperand};
8use ndarray_linalg::types::Lapack;
9use num_complex::{Complex, ComplexFloat};
10
11use crate::angmom::spinor_rotation_3d::{SpinConstraint, SpinOrbitCoupled, StructureConstraint};
12use crate::permutation::{IntoPermutation, PermutableCollection, Permutation};
13use crate::symmetry::symmetry_element::symmetry_operation::SpecialSymmetryTransformation;
14use crate::symmetry::symmetry_element::{RotationGroup, SymmetryElement, SymmetryOperation, TRROT};
15use crate::symmetry::symmetry_element_order::ElementOrder;
16use crate::symmetry::symmetry_transformation::{
17    assemble_sh_rotation_3d_matrices, assemble_spinor_rotation_matrices, permute_array_by_atoms,
18    ComplexConjugationTransformable, DefaultTimeReversalTransformable, SpatialUnitaryTransformable,
19    SpinUnitaryTransformable, SymmetryTransformable, TimeReversalTransformable,
20    TransformationError,
21};
22use crate::target::orbital::MolecularOrbital;
23
24// ======================================
25// Uncoupled spin and spatial coordinates
26// ======================================
27
28// ---------------------------
29// SpatialUnitaryTransformable
30// ---------------------------
31impl<'a, T> SpatialUnitaryTransformable for MolecularOrbital<'a, T, SpinConstraint>
32where
33    T: ComplexFloat + LinalgScalar + ScalarOperand + Copy + Lapack,
34    f64: Into<T>,
35{
36    fn transform_spatial_mut(
37        &mut self,
38        rmat: &Array2<f64>,
39        perm: Option<&Permutation<usize>>,
40    ) -> Result<&mut Self, TransformationError> {
41        let tmats: Vec<Array2<T>> = assemble_sh_rotation_3d_matrices(self.bao, rmat, perm)
42            .map_err(|err| TransformationError(err.to_string()))?
43            .iter()
44            .map(|tmat| tmat.map(|&x| x.into()))
45            .collect();
46        let pbao = if let Some(p) = perm {
47            self.bao
48                .permute(p)
49                .map_err(|err| TransformationError(err.to_string()))?
50        } else {
51            self.bao.clone()
52        };
53        let old_coeff = &self.coefficients;
54        let new_coefficients = match self.structure_constraint {
55            SpinConstraint::Restricted(_) | SpinConstraint::Unrestricted(_, _) => {
56                let p_coeff = if let Some(p) = perm {
57                    permute_array_by_atoms(old_coeff, p, &[Axis(0)], self.bao)
58                } else {
59                    old_coeff.clone()
60                };
61                let t_p_blocks = pbao
62                    .shell_boundary_indices()
63                    .into_iter()
64                    .zip(tmats.iter())
65                    .map(|((shl_start, shl_end), tmat)| {
66                        tmat.dot(&p_coeff.slice(s![shl_start..shl_end]))
67                    })
68                    .collect::<Vec<_>>();
69                concatenate(
70                    Axis(0),
71                    &t_p_blocks
72                        .iter()
73                        .map(|t_p_block| t_p_block.view())
74                        .collect::<Vec<_>>(),
75                )
76                .expect("Unable to concatenate the transformed rows for the various shells.")
77            }
78            SpinConstraint::Generalised(nspins, _) => {
79                let nspatial = self.bao.n_funcs();
80                let t_p_spin_blocks =
81                    (0..nspins)
82                        .map(|ispin| {
83                            // Extract spin block ispin.
84                            let spin_start = usize::from(ispin) * nspatial;
85                            let spin_end = (usize::from(ispin) + 1) * nspatial;
86                            let spin_block = old_coeff.slice(s![spin_start..spin_end]).to_owned();
87
88                            // Permute within spin block ispin.
89                            let p_spin_block = if let Some(p) = perm {
90                                permute_array_by_atoms(&spin_block, p, &[Axis(0)], self.bao)
91                            } else {
92                                spin_block
93                            };
94
95                            // Transform within spin block ispin.
96                            let t_p_blocks = pbao
97                                .shell_boundary_indices()
98                                .into_iter()
99                                .zip(tmats.iter())
100                                .map(|((shl_start, shl_end), tmat)| {
101                                    tmat.dot(&p_spin_block.slice(s![shl_start..shl_end]))
102                                })
103                                .collect::<Vec<_>>();
104
105                            // Concatenate blocks for various shells within spin block ispin.
106                            concatenate(
107                        Axis(0),
108                        &t_p_blocks.iter().map(|t_p_block| t_p_block.view()).collect::<Vec<_>>(),
109                    )
110                    .expect("Unable to concatenate the transformed rows for the various shells.")
111                        })
112                        .collect::<Vec<_>>();
113
114                // Concatenate spin blocks.
115                concatenate(
116                    Axis(0),
117                    &t_p_spin_blocks
118                        .iter()
119                        .map(|t_p_spin_block| t_p_spin_block.view())
120                        .collect::<Vec<_>>(),
121                )
122                .expect("Unable to concatenate the transformed spin blocks.")
123            }
124        };
125        self.coefficients = new_coefficients;
126        Ok(self)
127    }
128}
129
130// ------------------------
131// SpinUnitaryTransformable
132// ------------------------
133
134// ~~~~~~~~~~~~~~~~~
135// For real orbitals
136// ~~~~~~~~~~~~~~~~~
137
138impl<'a> SpinUnitaryTransformable for MolecularOrbital<'a, f64, SpinConstraint> {
139    fn transform_spin_mut(
140        &mut self,
141        dmat: &Array2<Complex<f64>>,
142    ) -> Result<&mut Self, TransformationError> {
143        let cdmat = dmat.view().split_complex();
144        if approx::relative_ne!(
145            cdmat.im.map(|x| x.powi(2)).sum().sqrt(),
146            0.0,
147            epsilon = 1e-14,
148            max_relative = 1e-14,
149        ) {
150            log::error!("Spin transformation matrix is complex-valued:\n{dmat}");
151            Err(TransformationError(
152                "Complex spin transformations cannot be performed with real coefficients."
153                    .to_string(),
154            ))
155        } else {
156            let rdmat = cdmat.re.to_owned();
157            match self.structure_constraint {
158                SpinConstraint::Restricted(_) => {
159                    if approx::relative_eq!(
160                        (&rdmat - Array2::<f64>::eye(2))
161                            .map(|x| x.abs().powi(2))
162                            .sum()
163                            .sqrt(),
164                        0.0,
165                        epsilon = 1e-14,
166                        max_relative = 1e-14,
167                    ) {
168                        // Identity spin rotation
169                        Ok(self)
170                    } else if approx::relative_eq!(
171                        (&rdmat + Array2::<f64>::eye(2))
172                            .map(|x| x.abs().powi(2))
173                            .sum()
174                            .sqrt(),
175                        0.0,
176                        epsilon = 1e-14,
177                        max_relative = 1e-14,
178                    ) {
179                        // Negative identity spin rotation
180                        self.coefficients *= -1.0;
181                        Ok(self)
182                    } else {
183                        log::error!("Unsupported spin transformation matrix:\n{}", &rdmat);
184                        Err(TransformationError(
185                            "Only the identity or negative identity spin transformations are possible with restricted spin constraint."
186                                .to_string(),
187                        ))
188                    }
189                }
190                SpinConstraint::Unrestricted(nspins, increasingm) => {
191                    // Only spin flip possible, so the order of the basis in which `dmat` is
192                    // expressed and the order of the spin blocks do not need to match.
193                    if nspins != 2 {
194                        return Err(TransformationError(
195                            "Only two-component spinor transformations are supported for now."
196                                .to_string(),
197                        ));
198                    }
199                    let dmat_y = array![[0.0, -1.0], [1.0, 0.0]];
200                    if approx::relative_eq!(
201                        (&rdmat - Array2::<f64>::eye(2))
202                            .map(|x| x.abs().powi(2))
203                            .sum()
204                            .sqrt(),
205                        0.0,
206                        epsilon = 1e-14,
207                        max_relative = 1e-14,
208                    ) {
209                        // Identity spin rotation
210                        Ok(self)
211                    } else if approx::relative_eq!(
212                        (&rdmat + Array2::<f64>::eye(2))
213                            .map(|x| x.abs().powi(2))
214                            .sum()
215                            .sqrt(),
216                        0.0,
217                        epsilon = 1e-14,
218                        max_relative = 1e-14,
219                    ) {
220                        // Negative identity spin rotation
221                        self.coefficients *= -1.0;
222                        Ok(self)
223                    } else if approx::relative_eq!(
224                        (&rdmat - &dmat_y).map(|x| x.abs().powi(2)).sum().sqrt(),
225                        0.0,
226                        epsilon = 1e-14,
227                        max_relative = 1e-14,
228                    ) {
229                        // π-rotation about y-axis, effectively spin-flip
230                        if increasingm {
231                            if self.component_index == 0 {
232                                self.component_index = 1;
233                                self.coefficients *= -1.0;
234                            } else {
235                                assert_eq!(self.component_index, 1);
236                                self.component_index = 0;
237                            }
238                        } else if self.component_index == 0 {
239                            self.component_index = 1;
240                        } else {
241                            assert_eq!(self.component_index, 1);
242                            self.component_index = 0;
243                            self.coefficients *= -1.0;
244                        }
245                        Ok(self)
246                    } else if approx::relative_eq!(
247                        (&rdmat + &dmat_y).map(|x| x.abs().powi(2)).sum().sqrt(),
248                        0.0,
249                        epsilon = 1e-14,
250                        max_relative = 1e-14,
251                    ) {
252                        // 3π-rotation about y-axis, effectively negative spin-flip
253                        if increasingm {
254                            if self.component_index == 0 {
255                                self.component_index = 1;
256                            } else {
257                                assert_eq!(self.component_index, 1);
258                                self.component_index = 0;
259                                self.coefficients *= -1.0;
260                            }
261                        } else if self.component_index == 0 {
262                            self.component_index = 1;
263                            self.coefficients *= -1.0;
264                        } else {
265                            assert_eq!(self.component_index, 1);
266                            self.component_index = 0;
267                        }
268                        Ok(self)
269                    } else {
270                        log::error!("Unsupported spin transformation matrix:\n{rdmat}");
271                        Err(TransformationError(
272                            "Only the identity or πy spin transformations are possible with unrestricted spin constraint."
273                                .to_string(),
274                        ))
275                    }
276                }
277                SpinConstraint::Generalised(nspins, increasingm) => {
278                    if nspins != 2 {
279                        return Err(TransformationError(
280                            "Only two-component spinor transformations are supported for now."
281                                .to_string(),
282                        ));
283                    }
284
285                    let nspatial = self.bao.n_funcs();
286                    let old_coeff = &self.coefficients;
287                    let new_coefficients = if increasingm {
288                        let b_coeff = old_coeff.slice(s![0..nspatial]).to_owned();
289                        let a_coeff = old_coeff.slice(s![nspatial..2 * nspatial]).to_owned();
290                        let t_a_coeff = &a_coeff * rdmat[[0, 0]] + &b_coeff * rdmat[[0, 1]];
291                        let t_b_coeff = &a_coeff * rdmat[[1, 0]] + &b_coeff * rdmat[[1, 1]];
292                        concatenate(Axis(0), &[t_b_coeff.view(), t_a_coeff.view()]).expect(
293                            "Unable to concatenate the transformed rows for the various shells.",
294                        )
295                    } else {
296                        let a_coeff = old_coeff.slice(s![0..nspatial]).to_owned();
297                        let b_coeff = old_coeff.slice(s![nspatial..2 * nspatial]).to_owned();
298                        let t_a_coeff = &a_coeff * rdmat[[0, 0]] + &b_coeff * rdmat[[0, 1]];
299                        let t_b_coeff = &a_coeff * rdmat[[1, 0]] + &b_coeff * rdmat[[1, 1]];
300                        concatenate(Axis(0), &[t_a_coeff.view(), t_b_coeff.view()]).expect(
301                            "Unable to concatenate the transformed rows for the various shells.",
302                        )
303                    };
304                    self.coefficients = new_coefficients;
305                    Ok(self)
306                }
307            }
308        }
309    }
310}
311
312// ~~~~~~~~~~~~~~~~~~~~
313// For complex orbitals
314// ~~~~~~~~~~~~~~~~~~~~
315
316impl<'a, T> SpinUnitaryTransformable for MolecularOrbital<'a, Complex<T>, SpinConstraint>
317where
318    T: Clone,
319    Complex<T>: ComplexFloat<Real = T>
320        + LinalgScalar
321        + ScalarOperand
322        + Lapack
323        + Mul<Complex<T>, Output = Complex<T>>
324        + Mul<Complex<f64>, Output = Complex<T>>,
325{
326    fn transform_spin_mut(
327        &mut self,
328        dmat: &Array2<Complex<f64>>,
329    ) -> Result<&mut Self, TransformationError> {
330        match self.structure_constraint {
331            SpinConstraint::Restricted(_) => {
332                if approx::relative_eq!(
333                    (dmat - Array2::<Complex<f64>>::eye(2))
334                        .map(|x| x.abs().powi(2))
335                        .sum()
336                        .sqrt(),
337                    0.0,
338                    epsilon = 1e-14,
339                    max_relative = 1e-14,
340                ) {
341                    // Identity spin rotation
342                    Ok(self)
343                } else if approx::relative_eq!(
344                    (dmat + Array2::<Complex<f64>>::eye(2))
345                        .map(|x| x.abs().powi(2))
346                        .sum()
347                        .sqrt(),
348                    0.0,
349                    epsilon = 1e-14,
350                    max_relative = 1e-14,
351                ) {
352                    // Negative identity spin rotation
353                    self.coefficients.map_inplace(|x| *x = -*x);
354                    Ok(self)
355                } else {
356                    log::error!("Unsupported spin transformation matrix:\n{}", dmat);
357                    Err(TransformationError(
358                        "Only the identity or negative identity spin transformations are possible with restricted spin constraint."
359                            .to_string(),
360                    ))
361                }
362            }
363            SpinConstraint::Unrestricted(nspins, increasingm) => {
364                // Only spin flip possible, so the order of the basis in which `dmat` is
365                // expressed and the order of the spin blocks do not need to match.
366                if nspins != 2 {
367                    return Err(TransformationError(
368                        "Only two-component spinor transformations are supported for now."
369                            .to_string(),
370                    ));
371                }
372                let dmat_y = array![
373                    [Complex::from(0.0), Complex::from(-1.0)],
374                    [Complex::from(1.0), Complex::from(0.0)],
375                ];
376                if approx::relative_eq!(
377                    (dmat - Array2::<Complex<f64>>::eye(2))
378                        .map(|x| x.abs().powi(2))
379                        .sum()
380                        .sqrt(),
381                    0.0,
382                    epsilon = 1e-14,
383                    max_relative = 1e-14,
384                ) {
385                    // Identity spin rotation
386                    Ok(self)
387                } else if approx::relative_eq!(
388                    (dmat + Array2::<Complex<f64>>::eye(2))
389                        .map(|x| x.abs().powi(2))
390                        .sum()
391                        .sqrt(),
392                    0.0,
393                    epsilon = 1e-14,
394                    max_relative = 1e-14,
395                ) {
396                    // Negative identity spin rotation
397                    self.coefficients.map_inplace(|x| *x = -*x);
398                    Ok(self)
399                } else if approx::relative_eq!(
400                    (dmat - &dmat_y).map(|x| x.abs().powi(2)).sum().sqrt(),
401                    0.0,
402                    epsilon = 1e-14,
403                    max_relative = 1e-14,
404                ) {
405                    // π-rotation about y-axis, effectively spin-flip
406                    if increasingm {
407                        if self.component_index == 0 {
408                            self.component_index = 1;
409                            self.coefficients.map_inplace(|x| *x = -*x);
410                        } else {
411                            assert_eq!(self.component_index, 1);
412                            self.component_index = 0;
413                        }
414                    } else if self.component_index == 0 {
415                        self.component_index = 1;
416                    } else {
417                        assert_eq!(self.component_index, 1);
418                        self.component_index = 0;
419                        self.coefficients.map_inplace(|x| *x = -*x);
420                    }
421                    Ok(self)
422                } else if approx::relative_eq!(
423                    (dmat + &dmat_y).map(|x| x.abs().powi(2)).sum().sqrt(),
424                    0.0,
425                    epsilon = 1e-14,
426                    max_relative = 1e-14,
427                ) {
428                    // 3π-rotation about y-axis, effectively negative spin-flip
429                    if increasingm {
430                        if self.component_index == 0 {
431                            self.component_index = 1;
432                        } else {
433                            assert_eq!(self.component_index, 1);
434                            self.component_index = 0;
435                            self.coefficients.map_inplace(|x| *x = -*x);
436                        }
437                    } else if self.component_index == 0 {
438                        self.component_index = 1;
439                        self.coefficients.map_inplace(|x| *x = -*x);
440                    } else {
441                        assert_eq!(self.component_index, 1);
442                        self.component_index = 0;
443                    }
444                    Ok(self)
445                } else {
446                    log::error!("Unsupported spin transformation matrix:\n{dmat}");
447                    Err(TransformationError(
448                        "Only the identity or πy spin transformations are possible with unrestricted spin constraint."
449                            .to_string(),
450                    ))
451                }
452            }
453            SpinConstraint::Generalised(nspins, increasingm) => {
454                if nspins != 2 {
455                    panic!("Only two-component spinor transformations are supported for now.");
456                }
457
458                let nspatial = self.bao.n_funcs();
459
460                let old_coeff = &self.coefficients;
461                let new_coefficients = if increasingm {
462                    let b_coeff = old_coeff.slice(s![0..nspatial]).to_owned();
463                    let a_coeff = old_coeff.slice(s![nspatial..2 * nspatial]).to_owned();
464                    let t_a_coeff = &a_coeff * dmat[[0, 0]] + &b_coeff * dmat[[0, 1]];
465                    let t_b_coeff = &a_coeff * dmat[[1, 0]] + &b_coeff * dmat[[1, 1]];
466                    concatenate(Axis(0), &[t_b_coeff.view(), t_a_coeff.view()]).expect(
467                        "Unable to concatenate the transformed rows for the various shells.",
468                    )
469                } else {
470                    let a_coeff = old_coeff.slice(s![0..nspatial]).to_owned();
471                    let b_coeff = old_coeff.slice(s![nspatial..2 * nspatial]).to_owned();
472                    let t_a_coeff = &a_coeff * dmat[[0, 0]] + &b_coeff * dmat[[0, 1]];
473                    let t_b_coeff = &a_coeff * dmat[[1, 0]] + &b_coeff * dmat[[1, 1]];
474                    concatenate(Axis(0), &[t_a_coeff.view(), t_b_coeff.view()]).expect(
475                        "Unable to concatenate the transformed rows for the various shells.",
476                    )
477                };
478                self.coefficients = new_coefficients;
479                Ok(self)
480            }
481        }
482    }
483}
484
485// ---------------------
486// SymmetryTransformable
487// ---------------------
488impl<'a, T> SymmetryTransformable for MolecularOrbital<'a, T, SpinConstraint>
489where
490    T: ComplexFloat + Lapack,
491    MolecularOrbital<'a, T, SpinConstraint>:
492        SpatialUnitaryTransformable + SpinUnitaryTransformable + TimeReversalTransformable,
493{
494    fn sym_permute_sites_spatial(
495        &self,
496        symop: &SymmetryOperation,
497    ) -> Result<Permutation<usize>, TransformationError> {
498        symop
499            .act_permute(&self.mol.molecule_ordinary_atoms())
500            .ok_or(TransformationError(format!(
501            "Unable to determine the atom permutation corresponding to the operation `{symop}`."
502        )))
503    }
504}
505
506// --------------------------------
507// DefaultTimeReversalTransformable
508// --------------------------------
509impl<'a, T> DefaultTimeReversalTransformable for MolecularOrbital<'a, T, SpinConstraint> where
510    T: ComplexFloat + Lapack
511{
512}
513
514// ====================================
515// Coupled spin and spatial coordinates
516// ====================================
517
518// ---------------------------
519// SpatialUnitaryTransformable
520// ---------------------------
521
522impl<'a, T> SpatialUnitaryTransformable for MolecularOrbital<'a, T, SpinOrbitCoupled>
523where
524    T: ComplexFloat + LinalgScalar + ScalarOperand + Copy + Lapack,
525    f64: Into<T>,
526{
527    fn transform_spatial_mut(
528        &mut self,
529        _rmat: &Array2<f64>,
530        _perm: Option<&Permutation<usize>>,
531    ) -> Result<&mut Self, TransformationError> {
532        Err(TransformationError(
533            "Unable to apply only spatial transformations to a spin--orbit-coupled molecular orbital."
534                .to_string(),
535        ))
536    }
537}
538
539// ------------------------
540// SpinUnitaryTransformable
541// ------------------------
542
543impl<'a, T> SpinUnitaryTransformable for MolecularOrbital<'a, T, SpinOrbitCoupled>
544where
545    T: ComplexFloat + LinalgScalar + ScalarOperand + Copy + Lapack,
546    f64: Into<T>,
547{
548    fn transform_spin_mut(
549        &mut self,
550        _dmat: &Array2<Complex<f64>>,
551    ) -> Result<&mut Self, TransformationError> {
552        Err(TransformationError(
553            "Unable to apply only spin transformations to a spin--orbit-coupled molecular orbital."
554                .to_string(),
555        ))
556    }
557}
558
559// -------------------------
560// TimeReversalTransformable
561// -------------------------
562impl<'a> TimeReversalTransformable for MolecularOrbital<'a, Complex<f64>, SpinOrbitCoupled> {
563    fn transform_timerev_mut(&mut self) -> Result<&mut Self, TransformationError> {
564        let t_element = SymmetryElement::builder()
565            .threshold(1e-12)
566            .proper_order(ElementOrder::Int(1))
567            .proper_power(1)
568            .raw_axis(Vector3::new(0.0, 0.0, 1.0))
569            .kind(TRROT)
570            .rotation_group(RotationGroup::SU2(true))
571            .build()
572            .unwrap();
573        let t = SymmetryOperation::builder()
574            .generating_element(t_element)
575            .power(1)
576            .build()
577            .unwrap();
578        let tmats: Vec<Array2<Complex<f64>>> =
579            assemble_spinor_rotation_matrices(self.bao, &t, None)
580                .map_err(|err| TransformationError(err.to_string()))?
581                .iter()
582                .map(|tmat| tmat.map(|&x| x.into()))
583                .collect();
584
585        let new_coefficients = {
586            let old_coeff = self.coefficients();
587            match self.structure_constraint {
588                SpinOrbitCoupled::JAdapted(ncomps) => {
589                    let nfuncs_per_comp = self.bao.n_funcs();
590                    let t_comp_blocks = (0..ncomps)
591                        .map(|icomp| {
592                            // Extract component block icomp.
593                            let comp_start = usize::from(icomp) * nfuncs_per_comp;
594                            let comp_end = (usize::from(icomp) + 1) * nfuncs_per_comp;
595                            let comp_block = old_coeff.slice(s![comp_start..comp_end]).to_owned();
596
597                            // Transform within spin block ispin.
598                            let t_blocks = self
599                                .bao
600                                .shell_boundary_indices()
601                                .iter()
602                                .zip(tmats.iter())
603                                .map(|((shl_start, shl_end), tmat)| {
604                                    tmat.dot(&comp_block.slice(s![*shl_start..*shl_end]))
605                                })
606                                .collect::<Vec<_>>();
607
608                            // Concatenate blocks for various shells within spin block ispin.
609                            concatenate(
610                        Axis(0),
611                        &t_blocks.iter().map(|t_block| t_block.view()).collect::<Vec<_>>(),
612                    )
613                    .expect("Unable to concatenate the transformed rows for the various shells.")
614                        })
615                        .collect::<Vec<_>>();
616
617                    // Concatenate component blocks.
618                    concatenate(
619                        Axis(0),
620                        &t_comp_blocks
621                            .iter()
622                            .map(|t_spin_block| t_spin_block.view())
623                            .collect::<Vec<_>>(),
624                    )
625                    .expect("Unable to concatenate the transformed component blocks.")
626                }
627            }
628        };
629        self.coefficients = new_coefficients;
630
631        self.transform_cc_mut()?;
632        Ok(self)
633    }
634}
635
636// ---------------------
637// SymmetryTransformable
638// ---------------------
639impl<'a> SymmetryTransformable for MolecularOrbital<'a, Complex<f64>, SpinOrbitCoupled> {
640    // ----------------
641    // Required methods
642    // ----------------
643    fn sym_permute_sites_spatial(
644        &self,
645        symop: &SymmetryOperation,
646    ) -> Result<Permutation<usize>, TransformationError> {
647        if (symop.generating_element.threshold().log10() - self.mol.threshold.log10()).abs() >= 3.0
648        {
649            log::warn!(
650                "Symmetry operation threshold ({:.3e}) and molecule threshold ({:.3e}) \
651                differ by more than three orders of magnitudes.",
652                symop.generating_element.threshold(),
653                self.mol.threshold
654            )
655        }
656        symop
657            .act_permute(&self.mol.molecule_ordinary_atoms())
658            .ok_or(TransformationError(format!(
659            "Unable to determine the atom permutation corresponding to the operation `{symop}`.",
660        )))
661    }
662
663    // ----------------------------
664    // Overwritten provided methods
665    // ----------------------------
666    fn sym_transform_spin_spatial_mut(
667        &mut self,
668        symop: &SymmetryOperation,
669    ) -> Result<&mut Self, TransformationError> {
670        let perm = self.sym_permute_sites_spatial(symop)?;
671        let pbao = self
672            .bao
673            .permute(&perm)
674            .map_err(|err| TransformationError(err.to_string()))?;
675
676        let tmats: Vec<Array2<Complex<f64>>> =
677            assemble_spinor_rotation_matrices(&pbao, symop, Some(&perm))
678                .map_err(|err| TransformationError(err.to_string()))?
679                .iter()
680                .map(|tmat| tmat.map(|&x| x.into()))
681                .collect();
682
683        // Time reversal, if any.
684        if symop.contains_time_reversal() {
685            // The unitary part of time reversal has already been included in the `tmats` generated
686            // by `assemble_spinor_rotation_matrices`. We therefore only need to take care of the
687            // complex conjugation of the coefficients. This must be done before the transformation
688            // matrices are applied.
689            self.transform_cc_mut()?;
690        }
691
692        let new_coefficients = {
693            let old_coeffs = self.coefficients();
694            match self.structure_constraint {
695                SpinOrbitCoupled::JAdapted(ncomps) => {
696                    let nfuncs_per_comp = self.bao.n_funcs();
697                    let t_p_comp_blocks = (0..ncomps).map(|icomp| {
698                        // Extract component block icomp.
699                        let comp_start = usize::from(icomp) * nfuncs_per_comp;
700                        let comp_end = (usize::from(icomp) + 1) * nfuncs_per_comp;
701                        let comp_block = old_coeffs.slice(s![comp_start..comp_end]).to_owned();
702
703                        // Permute within comp block icomp.
704                        let p_comp_block = permute_array_by_atoms(&comp_block, &perm, &[Axis(0)], self.bao);
705
706                        // Transform within comp block icomp.
707                        let t_p_blocks = pbao
708                            .shell_boundary_indices()
709                            .into_iter()
710                            .zip(tmats.iter())
711                            .map(|((shl_start, shl_end), tmat)| {
712                                tmat.dot(&p_comp_block.slice(s![shl_start..shl_end]))
713                            })
714                            .collect::<Vec<_>>();
715
716                        // Concatenate blocks for various shells within comp block icomp.
717                        concatenate(
718                            Axis(0),
719                            &t_p_blocks.iter().map(|t_p_block| t_p_block.view()).collect::<Vec<_>>(),
720                        )
721                        .expect("Unable to concatenate the transformed rows for the various shells.")
722                    }).collect::<Vec<_>>();
723
724                    // Concatenate comp blocks.
725                    concatenate(
726                        Axis(0),
727                        &t_p_comp_blocks
728                            .iter()
729                            .map(|t_p_comp_block| t_p_comp_block.view())
730                            .collect::<Vec<_>>(),
731                    )
732                    .expect("Unable to concatenate the transformed comp blocks.")
733                }
734            }
735        };
736        self.coefficients = new_coefficients;
737
738        Ok(self)
739    }
740}
741
742// =========================
743// All structure constraints
744// =========================
745
746// -------------------------------
747// ComplexConjugationTransformable
748// -------------------------------
749impl<'a, T, SC> ComplexConjugationTransformable for MolecularOrbital<'a, T, SC>
750where
751    T: ComplexFloat + Lapack,
752    SC: StructureConstraint + Clone,
753{
754    fn transform_cc_mut(&mut self) -> Result<&mut Self, TransformationError> {
755        self.coefficients.mapv_inplace(|x| x.conj());
756        self.complex_conjugated = !self.complex_conjugated;
757        Ok(self)
758    }
759}