qsym2/target/determinant/
determinant_transformation.rs

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