qsym2/target/orbital/
orbital_transformation.rs

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