qsym2/target/determinant/
determinant_transformation.rs

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