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!(tmatss.len(), self.structure_constraint.n_explicit_comps_per_coefficient_matrix());
65
66        let new_coefficients = self
67            .coefficients
68            .iter()
69            .map(|old_coeff| {
70                let t_p_comp_blocks = component_boundary_indices
71                    .iter()
72                    .zip(self.baos.iter())
73                    .zip(tmatss.iter())
74                    .map(|(((comp_start, comp_end), bao), tmats)| {
75                        let old_coeff_comp: Array2<T> =
76                            old_coeff.slice(s![*comp_start..*comp_end, ..]).to_owned();
77                        let p_coeff = if let Some(p) = perm {
78                            permute_array_by_atoms(&old_coeff_comp, p, &[Axis(0)], *bao)
79                        } else {
80                            old_coeff_comp.clone()
81                        };
82                        let pbao = if let Some(p) = perm {
83                            bao.permute(p)
84                                .map_err(|err| TransformationError(err.to_string()))?
85                        } else {
86                            (*bao).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                        Ok(t_p_blocks)
97                    })
98                    .collect::<Result<Vec<Vec<Array2<T>>>, _>>()?
99                    .into_iter()
100                    .flatten()
101                    .collect_vec();
102
103                concatenate(
104                    Axis(0),
105                    &t_p_comp_blocks
106                        .iter()
107                        .map(|t_p_block| t_p_block.view())
108                        .collect::<Vec<_>>(),
109                )
110                .map_err(|err| {
111                    TransformationError(format!(
112                        "Unable to concatenate the transformed blocks: {err}."
113                    ))
114                })
115            })
116            .collect::<Result<Vec<Array2<T>>, _>>()?;
117        self.coefficients = new_coefficients;
118        Ok(self)
119    }
120}
121
122// ------------------------
123// SpinUnitaryTransformable
124// ------------------------
125
126// ~~~~~~~~~~~~~~~~~~~~~
127// For real determinants
128// ~~~~~~~~~~~~~~~~~~~~~
129
130impl<'a> SpinUnitaryTransformable for SlaterDeterminant<'a, f64, SpinConstraint> {
131    /// Performs a spin transformation in-place.
132    ///
133    /// # Arguments
134    ///
135    /// * `dmat` - The two-dimensional representation matrix of the transformation in the basis of
136    /// the $`\{ \alpha, \beta \}`$ spinors (*i.e.* decreasing $`m`$ order).
137    fn transform_spin_mut(
138        &mut self,
139        dmat: &Array2<Complex<f64>>,
140    ) -> Result<&mut Self, TransformationError> {
141        let cdmat = dmat.view().split_complex();
142        if approx::relative_ne!(
143            cdmat.im.map(|x| x.powi(2)).sum().sqrt(),
144            0.0,
145            epsilon = 1e-14,
146            max_relative = 1e-14,
147        ) {
148            log::error!("Spin transformation matrix is complex-valued:\n{dmat}");
149            Err(TransformationError(
150                "Complex spin transformations cannot be performed with real coefficients."
151                    .to_string(),
152            ))
153        } else {
154            let rdmat = cdmat.re.to_owned();
155            match self.structure_constraint {
156                SpinConstraint::Restricted(_) => {
157                    if approx::relative_eq!(
158                        (&rdmat - Array2::<f64>::eye(2))
159                            .map(|x| x.abs().powi(2))
160                            .sum()
161                            .sqrt(),
162                        0.0,
163                        epsilon = 1e-14,
164                        max_relative = 1e-14,
165                    ) {
166                        // Identity spin rotation
167                        Ok(self)
168                    } else if approx::relative_eq!(
169                        (&rdmat + Array2::<f64>::eye(2))
170                            .map(|x| x.abs().powi(2))
171                            .sum()
172                            .sqrt(),
173                        0.0,
174                        epsilon = 1e-14,
175                        max_relative = 1e-14,
176                    ) {
177                        // Negative identity spin rotation
178                        self.coefficients
179                            .iter_mut()
180                            .for_each(|coeff| *coeff *= -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
222                            .iter_mut()
223                            .for_each(|coeff| *coeff = -coeff.clone());
224                        Ok(self)
225                    } else if approx::relative_eq!(
226                        (&rdmat - &dmat_y).map(|x| x.abs().powi(2)).sum().sqrt(),
227                        0.0,
228                        epsilon = 1e-14,
229                        max_relative = 1e-14,
230                    ) {
231                        // π-rotation about y-axis, effectively spin-flip
232                        let new_coefficients = if increasingm {
233                            vec![self.coefficients[1].clone(), -self.coefficients[0].clone()]
234                        } else {
235                            vec![-self.coefficients[1].clone(), self.coefficients[0].clone()]
236                        };
237                        let new_occupations =
238                            vec![self.occupations[1].clone(), self.occupations[0].clone()];
239                        self.coefficients = new_coefficients;
240                        self.occupations = new_occupations;
241                        Ok(self)
242                    } else if approx::relative_eq!(
243                        (&rdmat + &dmat_y).map(|x| x.abs().powi(2)).sum().sqrt(),
244                        0.0,
245                        epsilon = 1e-14,
246                        max_relative = 1e-14,
247                    ) {
248                        // 3π-rotation about y-axis, effectively negative spin-flip
249                        let new_coefficients = if increasingm {
250                            vec![-self.coefficients[1].clone(), self.coefficients[0].clone()]
251                        } else {
252                            vec![self.coefficients[1].clone(), -self.coefficients[0].clone()]
253                        };
254                        let new_occupations =
255                            vec![self.occupations[1].clone(), self.occupations[0].clone()];
256                        self.coefficients = new_coefficients;
257                        self.occupations = new_occupations;
258                        Ok(self)
259                    } else {
260                        log::error!("Unsupported spin transformation matrix:\n{rdmat}");
261                        Err(TransformationError(
262                            "Only the identity or πy spin transformations are possible with unrestricted spin constraint."
263                                .to_string(),
264                        ))
265                    }
266                }
267                SpinConstraint::Generalised(nspins, increasingm) => {
268                    if nspins != 2 {
269                        return Err(TransformationError(
270                            "Only two-component spinor transformations are supported for now."
271                                .to_string(),
272                        ));
273                    }
274
275                    let nspatial_set = self
276                        .baos
277                        .iter()
278                        .map(|bao| bao.n_funcs())
279                        .collect::<HashSet<usize>>();
280                    if nspatial_set.len() != 1 {
281                        return Err(TransformationError("Both explicit components in the generalised spin constraint must have the same number of spatial AO basis functions.".to_string()));
282                    }
283                    let nspatial = *nspatial_set.iter().next().ok_or_else(|| {
284                        TransformationError(
285                            "Unable to obtain the number of spatial AO basis functions."
286                                .to_string(),
287                        )
288                    })?;
289                    let new_coefficients = self
290                        .coefficients
291                        .iter()
292                        .map(|old_coeff| {
293                            if !increasingm {
294                                let a_coeff = old_coeff.slice(s![0..nspatial, ..]).to_owned();
295                                let b_coeff = old_coeff.slice(s![nspatial..2 * nspatial, ..]).to_owned();
296                                let t_a_coeff = &a_coeff * rdmat[[0, 0]] + &b_coeff * rdmat[[0, 1]];
297                                let t_b_coeff = &a_coeff * rdmat[[1, 0]] + &b_coeff * rdmat[[1, 1]];
298                                concatenate(Axis(0), &[t_a_coeff.view(), t_b_coeff.view()]).expect(
299                                    "Unable to concatenate the transformed rows for the various shells.",
300                                )
301                            } else {
302                                let b_coeff = old_coeff.slice(s![0..nspatial, ..]).to_owned();
303                                let a_coeff = old_coeff.slice(s![nspatial..2 * nspatial, ..]).to_owned();
304                                let t_a_coeff = &a_coeff * rdmat[[0, 0]] + &b_coeff * rdmat[[0, 1]];
305                                let t_b_coeff = &a_coeff * rdmat[[1, 0]] + &b_coeff * rdmat[[1, 1]];
306                                concatenate(Axis(0), &[t_b_coeff.view(), t_a_coeff.view()]).expect(
307                                    "Unable to concatenate the transformed rows for the various shells.",
308                                )
309                            }
310                        })
311                        .collect::<Vec<Array2<f64>>>();
312                    self.coefficients = new_coefficients;
313                    Ok(self)
314                }
315            }
316        }
317    }
318}
319
320// ~~~~~~~~~~~~~~~~~~~~~~~~
321// For complex determinants
322// ~~~~~~~~~~~~~~~~~~~~~~~~
323
324impl<'a, T> SpinUnitaryTransformable for SlaterDeterminant<'a, Complex<T>, SpinConstraint>
325where
326    T: Clone,
327    Complex<T>: ComplexFloat<Real = T>
328        + LinalgScalar
329        + ScalarOperand
330        + Lapack
331        + Mul<Complex<T>, Output = Complex<T>>
332        + Mul<Complex<f64>, Output = Complex<T>>,
333{
334    /// Performs a spin transformation in-place.
335    ///
336    /// # Arguments
337    ///
338    /// * `dmat` - The two-dimensional representation matrix of the transformation in the basis of
339    /// the $`\{ \alpha, \beta \}`$ spinors (*i.e.* decreasing $`m`$ order).
340    ///
341    /// # Panics
342    ///
343    /// Panics if the spin constraint is not generalised. Spin transformations can only be
344    /// performed with generalised spin constraint.
345    fn transform_spin_mut(
346        &mut self,
347        dmat: &Array2<Complex<f64>>,
348    ) -> Result<&mut Self, TransformationError> {
349        match self.structure_constraint {
350            SpinConstraint::Restricted(_) => {
351                if approx::relative_eq!(
352                    (dmat - Array2::<Complex<f64>>::eye(2))
353                        .map(|x| x.abs().powi(2))
354                        .sum()
355                        .sqrt(),
356                    0.0,
357                    epsilon = 1e-14,
358                    max_relative = 1e-14,
359                ) {
360                    // Identity spin rotation
361                    Ok(self)
362                } else if approx::relative_eq!(
363                    (dmat + Array2::<Complex<f64>>::eye(2))
364                        .map(|x| x.abs().powi(2))
365                        .sum()
366                        .sqrt(),
367                    0.0,
368                    epsilon = 1e-14,
369                    max_relative = 1e-14,
370                ) {
371                    // Negative identity spin rotation
372                    self.coefficients
373                        .iter_mut()
374                        .for_each(|coeff| *coeff = -coeff.clone());
375                    Ok(self)
376                } else {
377                    log::error!("Unsupported spin transformation matrix:\n{}", dmat);
378                    Err(TransformationError(
379                        "Only the identity or negative identity spin transformations are possible with restricted spin constraint."
380                            .to_string(),
381                    ))
382                }
383            }
384            SpinConstraint::Unrestricted(nspins, increasingm) => {
385                // Only spin flip possible, so the order of the basis in which `dmat` is
386                // expressed and the order of the spin blocks do not need to match.
387                if nspins != 2 {
388                    return Err(TransformationError(
389                        "Only two-component spinor transformations are supported for now."
390                            .to_string(),
391                    ));
392                }
393                let dmat_y = array![
394                    [Complex::from(0.0), Complex::from(-1.0)],
395                    [Complex::from(1.0), Complex::from(0.0)],
396                ];
397                if approx::relative_eq!(
398                    (dmat - Array2::<Complex<f64>>::eye(2))
399                        .map(|x| x.abs().powi(2))
400                        .sum()
401                        .sqrt(),
402                    0.0,
403                    epsilon = 1e-14,
404                    max_relative = 1e-14,
405                ) {
406                    // Identity spin rotation
407                    Ok(self)
408                } else if approx::relative_eq!(
409                    (dmat + Array2::<Complex<f64>>::eye(2))
410                        .map(|x| x.abs().powi(2))
411                        .sum()
412                        .sqrt(),
413                    0.0,
414                    epsilon = 1e-14,
415                    max_relative = 1e-14,
416                ) {
417                    // Negative identity spin rotation
418                    self.coefficients
419                        .iter_mut()
420                        .for_each(|coeff| *coeff = -coeff.clone());
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                    // π-rotation about y-axis, effectively spin-flip
429                    let new_coefficients = if increasingm {
430                        vec![self.coefficients[1].clone(), -self.coefficients[0].clone()]
431                    } else {
432                        vec![-self.coefficients[1].clone(), self.coefficients[0].clone()]
433                    };
434                    let new_occupations =
435                        vec![self.occupations[1].clone(), self.occupations[0].clone()];
436                    self.coefficients = new_coefficients;
437                    self.occupations = new_occupations;
438                    Ok(self)
439                } else if approx::relative_eq!(
440                    (dmat + &dmat_y).map(|x| x.abs().powi(2)).sum().sqrt(),
441                    0.0,
442                    epsilon = 1e-14,
443                    max_relative = 1e-14,
444                ) {
445                    // 3π-rotation about y-axis, effectively negative spin-flip
446                    let new_coefficients = if increasingm {
447                        vec![-self.coefficients[1].clone(), self.coefficients[0].clone()]
448                    } else {
449                        vec![self.coefficients[1].clone(), -self.coefficients[0].clone()]
450                    };
451                    let new_occupations =
452                        vec![self.occupations[1].clone(), self.occupations[0].clone()];
453                    self.coefficients = new_coefficients;
454                    self.occupations = new_occupations;
455                    Ok(self)
456                } else {
457                    log::error!("Unsupported spin transformation matrix:\n{dmat}");
458                    Err(TransformationError(
459                        "Only the identity or πy spin transformations are possible with unrestricted spin constraint."
460                            .to_string(),
461                    ))
462                }
463            }
464            SpinConstraint::Generalised(nspins, increasingm) => {
465                if nspins != 2 {
466                    panic!("Only two-component spinor transformations are supported for now.");
467                }
468
469                let nspatial_set = self
470                    .baos
471                    .iter()
472                    .map(|bao| bao.n_funcs())
473                    .collect::<HashSet<usize>>();
474                if nspatial_set.len() != 1 {
475                    return Err(TransformationError("Both explicit components in the generalised spin constraint must have the same number of spatial AO basis functions.".to_string()));
476                }
477                let nspatial = *nspatial_set.iter().next().ok_or_else(|| {
478                    TransformationError(
479                        "Unable to obtain the number of spatial AO basis functions.".to_string(),
480                    )
481                })?;
482
483                let new_coefficients = self
484                    .coefficients
485                    .iter()
486                    .map(|old_coeff| {
487                        if increasingm {
488                            let b_coeff = old_coeff.slice(s![0..nspatial, ..]).to_owned();
489                            let a_coeff = old_coeff.slice(s![nspatial..2 * nspatial, ..]).to_owned();
490                            let t_a_coeff = &a_coeff * dmat[[0, 0]] + &b_coeff * dmat[[0, 1]];
491                            let t_b_coeff = &a_coeff * dmat[[1, 0]] + &b_coeff * dmat[[1, 1]];
492                            concatenate(Axis(0), &[t_b_coeff.view(), t_a_coeff.view()]).expect(
493                                "Unable to concatenate the transformed rows for the various shells.",
494                            )
495                        } else {
496                            let a_coeff = old_coeff.slice(s![0..nspatial, ..]).to_owned();
497                            let b_coeff = old_coeff.slice(s![nspatial..2 * nspatial, ..]).to_owned();
498                            let t_a_coeff = &a_coeff * dmat[[0, 0]] + &b_coeff * dmat[[0, 1]];
499                            let t_b_coeff = &a_coeff * dmat[[1, 0]] + &b_coeff * dmat[[1, 1]];
500                            concatenate(Axis(0), &[t_a_coeff.view(), t_b_coeff.view()]).expect(
501                                "Unable to concatenate the transformed rows for the various shells.",
502                            )
503                        }
504                    })
505                    .collect::<Vec<Array2<Complex<T>>>>();
506                self.coefficients = new_coefficients;
507                Ok(self)
508            }
509        }
510    }
511}
512
513// --------------------------------
514// DefaultTimeReversalTransformable
515// --------------------------------
516impl<'a, T> DefaultTimeReversalTransformable for SlaterDeterminant<'a, T, SpinConstraint> where
517    T: ComplexFloat + Lapack
518{
519}
520
521// ---------------------
522// SymmetryTransformable
523// ---------------------
524impl<'a, T> SymmetryTransformable for SlaterDeterminant<'a, T, SpinConstraint>
525where
526    T: ComplexFloat + Lapack,
527    SlaterDeterminant<'a, T, SpinConstraint>:
528        SpatialUnitaryTransformable + SpinUnitaryTransformable + TimeReversalTransformable,
529{
530    fn sym_permute_sites_spatial(
531        &self,
532        symop: &SymmetryOperation,
533    ) -> Result<Permutation<usize>, TransformationError> {
534        if (symop.generating_element.threshold().log10() - self.mol.threshold.log10()).abs() >= 3.0
535        {
536            log::warn!(
537                "Symmetry operation threshold ({:.3e}) and molecule threshold ({:.3e}) \
538                differ by more than three orders of magnitudes.",
539                symop.generating_element.threshold(),
540                self.mol.threshold
541            )
542        }
543        symop
544            .act_permute(&self.mol.molecule_ordinary_atoms())
545            .ok_or(TransformationError(format!(
546            "Unable to determine the atom permutation corresponding to the operation `{symop}`.",
547        )))
548    }
549}
550
551// ====================================
552// Coupled spin and spatial coordinates
553// ====================================
554
555// ---------------------------
556// SpatialUnitaryTransformable
557// ---------------------------
558impl<'a, T> SpatialUnitaryTransformable for SlaterDeterminant<'a, T, SpinOrbitCoupled>
559where
560    T: ComplexFloat + Lapack,
561{
562    fn transform_spatial_mut(
563        &mut self,
564        _rmat: &Array2<f64>,
565        _perm: Option<&Permutation<usize>>,
566    ) -> Result<&mut Self, TransformationError> {
567        Err(TransformationError(
568            "Unable to apply only spatial transformations to a spin--orbit-coupled determinant."
569                .to_string(),
570        ))
571    }
572}
573
574// ------------------------
575// SpinUnitaryTransformable
576// ------------------------
577impl<'a, T> SpinUnitaryTransformable for SlaterDeterminant<'a, T, SpinOrbitCoupled>
578where
579    T: ComplexFloat + Lapack,
580{
581    fn transform_spin_mut(
582        &mut self,
583        _dmat: &Array2<Complex<f64>>,
584    ) -> Result<&mut Self, TransformationError> {
585        Err(TransformationError(
586            "Unable to apply only spin transformations to a spin--orbit-coupled determinant."
587                .to_string(),
588        ))
589    }
590}
591
592// ---------------------------------------
593// TimeReversalTransformable (non-default)
594// ---------------------------------------
595impl<'a> TimeReversalTransformable for SlaterDeterminant<'a, Complex<f64>, SpinOrbitCoupled> {
596    fn transform_timerev_mut(&mut self) -> Result<&mut Self, TransformationError> {
597        let t_element = SymmetryElement::builder()
598            .threshold(1e-12)
599            .proper_order(ElementOrder::Int(1))
600            .proper_power(1)
601            .raw_axis(Vector3::new(0.0, 0.0, 1.0))
602            .kind(TRROT)
603            .rotation_group(RotationGroup::SU2(true))
604            .build()
605            .unwrap();
606        let t = SymmetryOperation::builder()
607            .generating_element(t_element)
608            .power(1)
609            .build()
610            .unwrap();
611        let tmatss: Vec<Vec<Array2<Complex<f64>>>> =
612            assemble_spinor_rotation_matrices(&self.baos, &t, None)
613                .map_err(|err| TransformationError(err.to_string()))?
614                .iter()
615                .map(|tmats| {
616                    tmats
617                        .iter()
618                        .map(|tmat| tmat.mapv(|x| x.into()))
619                        .collect::<Vec<_>>()
620                })
621                .collect::<Vec<_>>();
622
623        let component_boundary_indices = self
624            .baos
625            .iter()
626            .scan(0, |acc, bao| {
627                let start_index = *acc;
628                *acc += bao.n_funcs();
629                Some((start_index, *acc))
630            })
631            .collect::<Vec<_>>();
632        assert_eq!(tmatss.len(), component_boundary_indices.len());
633        assert_eq!(tmatss.len(), self.structure_constraint.n_explicit_comps_per_coefficient_matrix());
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                .iter()
722                .map(|tmats| {
723                    tmats
724                        .iter()
725                        .map(|tmat| tmat.mapv(|x| x.into()))
726                        .collect::<Vec<_>>()
727                })
728                .collect::<Vec<_>>();
729        let component_boundary_indices = self
730            .baos
731            .iter()
732            .scan(0, |acc, bao| {
733                let start_index = *acc;
734                *acc += bao.n_funcs();
735                Some((start_index, *acc))
736            })
737            .collect::<Vec<_>>();
738        assert_eq!(tmatss.len(), component_boundary_indices.len());
739        assert_eq!(tmatss.len(), self.structure_constraint.n_explicit_comps_per_coefficient_matrix());
740
741        // Time reversal, if any.
742        if symop.contains_time_reversal() {
743            // The unitary part of time reversal has already been included in the `tmatss` generated
744            // by `assemble_spinor_rotation_matrices`. We therefore only need to take care of the
745            // complex conjugation of the coefficients. This must be done before the transformation
746            // matrices are applied.
747            self.transform_cc_mut()?;
748        }
749
750        let new_coefficients = self
751            .coefficients
752            .iter()
753            .map(|old_coeff| {
754                let t_p_comp_blocks = component_boundary_indices
755                    .iter()
756                    .zip(self.baos.iter())
757                    .zip(tmatss.iter())
758                    .map(|(((comp_start, comp_end), bao), tmats)| {
759                        let old_coeff_comp: Array2<_> =
760                            old_coeff.slice(s![*comp_start..*comp_end, ..]).to_owned();
761                        let p_coeff =
762                            permute_array_by_atoms(&old_coeff_comp, &perm, &[Axis(0)], *bao);
763                        let pbao = bao
764                            .permute(&perm)
765                            .map_err(|err| TransformationError(err.to_string()))?;
766                        let t_p_blocks = pbao
767                            .shell_boundary_indices()
768                            .into_iter()
769                            .zip(tmats.iter())
770                            .map(|((shl_start, shl_end), tmat)| {
771                                tmat.dot(&p_coeff.slice(s![shl_start..shl_end, ..]))
772                            })
773                            .collect::<Vec<_>>();
774                        Ok(t_p_blocks)
775                    })
776                    .collect::<Result<Vec<Vec<Array2<_>>>, _>>()?
777                    .into_iter()
778                    .flatten()
779                    .collect_vec();
780
781                concatenate(
782                    Axis(0),
783                    &t_p_comp_blocks
784                        .iter()
785                        .map(|t_p_block| t_p_block.view())
786                        .collect::<Vec<_>>(),
787                )
788                .map_err(|err| {
789                    TransformationError(format!(
790                        "Unable to concatenate the transformed blocks: {err}."
791                    ))
792                })
793            })
794            .collect::<Result<Vec<Array2<Complex<f64>>>, _>>()?;
795        self.coefficients = new_coefficients;
796
797        Ok(self)
798    }
799}
800
801// =========================
802// All structure constraints
803// =========================
804
805// -------------------------------
806// ComplexConjugationTransformable
807// -------------------------------
808
809impl<'a, T, SC> ComplexConjugationTransformable for SlaterDeterminant<'a, T, SC>
810where
811    T: ComplexFloat + Lapack,
812    SC: StructureConstraint + Clone + fmt::Display,
813{
814    /// Performs a complex conjugation in-place.
815    fn transform_cc_mut(&mut self) -> Result<&mut Self, TransformationError> {
816        self.coefficients
817            .iter_mut()
818            .for_each(|coeff| coeff.mapv_inplace(|x| x.conj()));
819        self.complex_conjugated = !self.complex_conjugated;
820        Ok(self)
821    }
822}