Skip to main content

qsym2/target/determinant/
determinant_analysis.rs

1//! Implementation of symmetry analysis for Slater determinants.
2
3use std::fmt;
4use std::ops::Mul;
5
6use anyhow::{self, Context, ensure, format_err};
7use approx;
8use derive_builder::Builder;
9use itertools::{Itertools, izip};
10use log;
11use ndarray::{Array1, Array2, Axis, Ix2};
12use ndarray_linalg::{
13    UPLO,
14    eig::Eig,
15    eigh::Eigh,
16    solve::Determinant,
17    types::{Lapack, Scalar},
18};
19use num_complex::{Complex, ComplexFloat};
20use num_traits::{Float, ToPrimitive, Zero};
21
22use crate::analysis::{
23    EigenvalueComparisonMode, Orbit, OrbitIterator, Overlap, RepAnalysis, fn_calc_xmat_complex,
24    fn_calc_xmat_real,
25};
26use crate::angmom::spinor_rotation_3d::StructureConstraint;
27use crate::auxiliary::misc::complex_modified_gram_schmidt;
28use crate::chartab::chartab_group::CharacterProperties;
29use crate::chartab::{DecompositionError, SubspaceDecomposable};
30use crate::group::GroupType;
31use crate::io::format::{QSym2Output, log_subtitle, qsym2_output};
32use crate::symmetry::symmetry_element::symmetry_operation::SpecialSymmetryTransformation;
33use crate::symmetry::symmetry_group::SymmetryGroupProperties;
34use crate::symmetry::symmetry_transformation::{SymmetryTransformable, SymmetryTransformationKind};
35use crate::target::determinant::SlaterDeterminant;
36
37// =======
38// Overlap
39// =======
40
41impl<'a, T, SC> Overlap<T, Ix2> for SlaterDeterminant<'a, T, SC>
42where
43    T: Lapack
44        + ComplexFloat<Real = <T as Scalar>::Real>
45        + fmt::Debug
46        + Mul<<T as ComplexFloat>::Real, Output = T>,
47    <T as ComplexFloat>::Real: fmt::Debug
48        + approx::RelativeEq<<T as ComplexFloat>::Real>
49        + approx::AbsDiffEq<Epsilon = <T as Scalar>::Real>,
50    SC: StructureConstraint + Eq + fmt::Display,
51{
52    fn complex_symmetric(&self) -> bool {
53        self.complex_symmetric
54    }
55
56    /// Computes the overlap between two Slater determinants.
57    ///
58    /// Determinants with fractional electrons are currently not supported.
59    ///
60    /// When one or both of the Slater determinants have been acted on by an antiunitary operation,
61    /// the correct Hermitian or complex-symmetric metric will be chosen in the evalulation of the
62    /// overlap.
63    ///
64    /// # Arguments
65    ///
66    /// * `metric` - The atomic-orbital overlap matrix with respect to the conventional sesquilinear
67    ///   inner product.
68    /// * `metric_h` - The atomic-orbital overlap matrix with respect to the bilinear inner product.
69    ///
70    /// # Panics
71    ///
72    /// Panics if `self` and `other` have mismatched spin constraints or numbers of coefficient
73    /// matrices, or if fractional occupation numbers are detected.
74    fn overlap(
75        &self,
76        other: &Self,
77        metric: Option<&Array2<T>>,
78        metric_h: Option<&Array2<T>>,
79    ) -> Result<T, anyhow::Error> {
80        ensure!(
81            self.structure_constraint == other.structure_constraint,
82            "Inconsistent structure constraints between `self` and `other`."
83        );
84        ensure!(
85            self.coefficients.len() == other.coefficients.len(),
86            "Inconsistent numbers of coefficient matrices between `self` and `other`."
87        );
88        ensure!(
89            self.baos == other.baos,
90            "Inconsistent basis angular order between `self` and `other`."
91        );
92        ensure!(
93            self.complex_symmetric == other.complex_symmetric,
94            "Inconsistent `complex_symmetric` between `self` and `other`."
95        );
96
97        let thresh = Float::sqrt(self.threshold * other.threshold);
98        ensure!(
99            self.occupations
100                .iter()
101                .chain(other.occupations.iter())
102                .all(|occs| occs.iter().all(|&occ| approx::relative_eq!(
103                    occ,
104                    occ.round(),
105                    epsilon = thresh,
106                    max_relative = thresh
107                ))),
108            "Overlaps between determinants with fractional occupation numbers are currently not supported."
109        );
110
111        let sao = metric.ok_or_else(|| format_err!("No atomic-orbital metric found."))?;
112        let sao_h = metric_h.unwrap_or(sao);
113
114        let ov = izip!(
115            &self.coefficients,
116            &self.occupations,
117            &other.coefficients,
118            &other.occupations
119        )
120        .map(|(cw, occw, cx, occx)| {
121            let nonzero_occ_w = occw.iter().positions(|&occ| occ > thresh).collect_vec();
122            let cw_o = cw.select(Axis(1), &nonzero_occ_w);
123            let nonzero_occ_x = occx.iter().positions(|&occ| occ > thresh).collect_vec();
124            let cx_o = cx.select(Axis(1), &nonzero_occ_x);
125
126            let mo_ov_mat = if self.complex_symmetric() {
127                match (self.complex_conjugated, other.complex_conjugated) {
128                    (false, false) => cw_o.t().dot(sao_h).dot(&cx_o),
129                    (true, false) => cw_o.t().dot(sao).dot(&cx_o),
130                    (false, true) => cx_o.t().dot(sao).dot(&cw_o),
131                    (true, true) => cw_o.t().dot(&sao_h.t()).dot(&cx_o),
132                }
133            } else {
134                match (self.complex_conjugated, other.complex_conjugated) {
135                    (false, false) => cw_o.t().mapv(|x| x.conj()).dot(sao).dot(&cx_o),
136                    (true, false) => cw_o.t().mapv(|x| x.conj()).dot(sao_h).dot(&cx_o),
137                    (false, true) => cx_o
138                        .t()
139                        .mapv(|x| x.conj())
140                        .dot(sao_h)
141                        .dot(&cw_o)
142                        .mapv(|x| x.conj()),
143                    (true, true) => cw_o.t().mapv(|x| x.conj()).dot(&sao.t()).dot(&cx_o),
144                }
145            };
146            mo_ov_mat
147                .det()
148                .expect("The determinant of the MO overlap matrix could not be found.")
149        })
150        .fold(T::one(), |acc, x| acc * x);
151
152        let implicit_factor = self.structure_constraint.implicit_factor()?;
153        if implicit_factor > 1 {
154            let p_i32 = i32::try_from(implicit_factor)?;
155            Ok(ComplexFloat::powi(ov, p_i32))
156        } else {
157            Ok(ov)
158        }
159    }
160
161    /// Returns the mathematical definition of the overlap between two Slater determinants.
162    fn overlap_definition(&self) -> String {
163        let k = if self.complex_symmetric() { "κ " } else { "" };
164        format!("⟨{k}Ψ_1|Ψ_2⟩ = ∫ [{k}Ψ_1(x^Ne)]* Ψ_2(x^Ne) dx^Ne")
165    }
166}
167
168// ==============================
169// SlaterDeterminantSymmetryOrbit
170// ==============================
171
172// -----------------
173// Struct definition
174// -----------------
175
176/// Structure to manage symmetry orbits (*i.e.* orbits generated by symmetry groups) of Slater
177/// determinants.
178#[derive(Builder, Clone)]
179pub struct SlaterDeterminantSymmetryOrbit<'g, 'a, G, T, SC>
180where
181    G: SymmetryGroupProperties,
182    T: ComplexFloat + fmt::Debug + Lapack,
183    SC: StructureConstraint + fmt::Display,
184    SlaterDeterminant<'a, T, SC>: SymmetryTransformable,
185{
186    /// The generating symmetry group.
187    group: &'g G,
188
189    /// The origin Slater determinant of the orbit.
190    origin: &'a SlaterDeterminant<'a, T, SC>,
191
192    /// The threshold for determining zero eigenvalues in the orbit overlap matrix.
193    linear_independence_threshold: <T as ComplexFloat>::Real,
194
195    /// The threshold for checking symmetry of the orbit overlap matrix.
196    orbit_overlap_symmetry_threshold: <T as ComplexFloat>::Real,
197
198    /// The threshold for determining if calculated multiplicities in representation analysis are
199    /// integral.
200    integrality_threshold: <T as ComplexFloat>::Real,
201
202    /// The kind of transformation determining the way the symmetry operations in `group` act on
203    /// [`Self::origin`].
204    symmetry_transformation_kind: SymmetryTransformationKind,
205
206    /// The overlap matrix between the symmetry-equivalent Slater determinants in the orbit.
207    #[builder(setter(skip), default = "None")]
208    smat: Option<Array2<T>>,
209
210    /// The eigenvalues of the overlap matrix between the symmetry-equivalent Slater determinants in
211    /// the orbit.
212    #[builder(setter(skip), default = "None")]
213    pub(crate) smat_eigvals: Option<Array1<T>>,
214
215    /// The $`\mathbf{X}`$ matrix for the overlap matrix between the symmetry-equivalent Slater
216    /// determinants in the orbit.
217    ///
218    /// See [`RepAnalysis::xmat`] for further information.
219    #[builder(setter(skip), default = "None")]
220    xmat: Option<Array2<T>>,
221
222    /// An enumerated type specifying the comparison mode for filtering out orbit overlap
223    /// eigenvalues.
224    eigenvalue_comparison_mode: EigenvalueComparisonMode,
225}
226
227// ----------------------------
228// Struct method implementation
229// ----------------------------
230
231impl<'g, 'a, G, T, SC> SlaterDeterminantSymmetryOrbit<'g, 'a, G, T, SC>
232where
233    G: SymmetryGroupProperties + Clone,
234    T: ComplexFloat + fmt::Debug + Lapack,
235    SC: StructureConstraint + Clone + fmt::Display,
236    SlaterDeterminant<'a, T, SC>: SymmetryTransformable,
237{
238    /// Returns a builder for constructing a new Slater determinant symmetry orbit.
239    pub fn builder() -> SlaterDeterminantSymmetryOrbitBuilder<'g, 'a, G, T, SC> {
240        SlaterDeterminantSymmetryOrbitBuilder::default()
241    }
242}
243
244impl<'g, 'a, G, T, SC> SlaterDeterminantSymmetryOrbit<'g, 'a, G, T, SC>
245where
246    G: SymmetryGroupProperties,
247    T: ComplexFloat + fmt::Debug + Lapack,
248    SC: StructureConstraint + fmt::Display,
249    SlaterDeterminant<'a, T, SC>: SymmetryTransformable,
250{
251    #[allow(clippy::type_complexity)]
252    pub fn action(
253        &self,
254    ) -> fn(
255        &G::GroupElement,
256        &SlaterDeterminant<'a, T, SC>,
257    ) -> Result<SlaterDeterminant<'a, T, SC>, anyhow::Error> {
258        match self.symmetry_transformation_kind {
259            SymmetryTransformationKind::Spatial => |op, det| {
260                det.sym_transform_spatial(op).with_context(|| {
261                    format!("Unable to apply `{op}` spatially on the origin determinant")
262                })
263            },
264            SymmetryTransformationKind::SpatialWithSpinTimeReversal => |op, det| {
265                det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
266                    format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin determinant")
267                })
268            },
269            SymmetryTransformationKind::Spin => |op, det| {
270                det.sym_transform_spin(op).with_context(|| {
271                    format!("Unable to apply `{op}` spin-wise on the origin determinant")
272                })
273            },
274            SymmetryTransformationKind::SpinSpatial => |op, det| {
275                det.sym_transform_spin_spatial(op).with_context(|| {
276                    format!("Unable to apply `{op}` spin-spatially on the origin determinant")
277                })
278            },
279        }
280    }
281}
282
283impl<'g, 'a, G, SC> SlaterDeterminantSymmetryOrbit<'g, 'a, G, f64, SC>
284where
285    G: SymmetryGroupProperties,
286    SC: StructureConstraint + fmt::Display,
287    SlaterDeterminant<'a, f64, SC>: SymmetryTransformable,
288{
289    fn_calc_xmat_real!(
290        /// Calculates the $`\mathbf{X}`$ matrix for real and symmetric overlap matrix
291        /// $`\mathbf{S}`$ between the symmetry-equivalent Slater determinants in the orbit.
292        ///
293        /// The resulting $`\mathbf{X}`$ is stored in the orbit.
294        ///
295        /// # Arguments
296        ///
297        /// * `preserves_full_rank` - If `true`, when $`\mathbf{S}`$ is already of full rank, then
298        /// $`\mathbf{X}`$ is set to be the identity matrix to avoid mixing the orbit determinants.
299        /// If `false`, $`\mathbf{X}`$ also orthogonalises $`\mathbf{S}`$ even when it is already of
300        /// full rank.
301        pub calc_xmat
302    );
303}
304
305impl<'g, 'a, G, T, SC> SlaterDeterminantSymmetryOrbit<'g, 'a, G, Complex<T>, SC>
306where
307    G: SymmetryGroupProperties,
308    T: Float + Scalar<Complex = Complex<T>>,
309    Complex<T>: ComplexFloat<Real = T> + Scalar<Real = T, Complex = Complex<T>> + Lapack,
310    SC: StructureConstraint + fmt::Display,
311    SlaterDeterminant<'a, Complex<T>, SC>: SymmetryTransformable + Overlap<Complex<T>, Ix2>,
312{
313    fn_calc_xmat_complex!(
314        /// Calculates the $`\mathbf{X}`$ matrix for complex and symmetric or Hermitian overlap
315        /// matrix $`\mathbf{S}`$ between the symmetry-equivalent Slater determinants in the orbit.
316        ///
317        /// The resulting $`\mathbf{X}`$ is stored in the orbit.
318        ///
319        /// # Arguments
320        ///
321        /// * `preserves_full_rank` - If `true`, when $`\mathbf{S}`$ is already of full rank, then
322        /// $`\mathbf{X}`$ is set to be the identity matrix to avoid mixing the orbit determinants.
323        /// If `false`, $`\mathbf{X}`$ also orthogonalises $`\mathbf{S}`$ even when it is already of
324        /// full rank.
325        pub calc_xmat
326    );
327}
328
329// ---------------------
330// Trait implementations
331// ---------------------
332
333// ~~~~~
334// Orbit
335// ~~~~~
336
337impl<'g, 'a, G, T, SC> Orbit<G, SlaterDeterminant<'a, T, SC>>
338    for SlaterDeterminantSymmetryOrbit<'g, 'a, G, T, SC>
339where
340    G: SymmetryGroupProperties,
341    T: ComplexFloat + fmt::Debug + Lapack,
342    SC: StructureConstraint + fmt::Display,
343    SlaterDeterminant<'a, T, SC>: SymmetryTransformable,
344{
345    type OrbitIter = OrbitIterator<'a, G, SlaterDeterminant<'a, T, SC>>;
346
347    fn group(&self) -> &G {
348        self.group
349    }
350
351    fn origin(&self) -> &SlaterDeterminant<'a, T, SC> {
352        self.origin
353    }
354
355    fn iter(&self) -> Self::OrbitIter {
356        OrbitIterator::new(
357            self.group,
358            self.origin,
359            self.action(),
360            // match self.symmetry_transformation_kind {
361            //     SymmetryTransformationKind::Spatial => |op, det| {
362            //         det.sym_transform_spatial(op).with_context(|| {
363            //             format!("Unable to apply `{op}` spatially on the origin determinant")
364            //         })
365            //     },
366            //     SymmetryTransformationKind::SpatialWithSpinTimeReversal => |op, det| {
367            //         det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
368            //             format!("Unable to apply `{op}` spatially (with spin-including time reversal) on the origin determinant")
369            //         })
370            //     },
371            //     SymmetryTransformationKind::Spin => |op, det| {
372            //         det.sym_transform_spin(op).with_context(|| {
373            //             format!("Unable to apply `{op}` spin-wise on the origin determinant")
374            //         })
375            //     },
376            //     SymmetryTransformationKind::SpinSpatial => |op, det| {
377            //         det.sym_transform_spin_spatial(op).with_context(|| {
378            //             format!("Unable to apply `{op}` spin-spatially on the origin determinant")
379            //         })
380            //     },
381            // },
382        )
383    }
384}
385
386// ~~~~~~~~~~~
387// RepAnalysis
388// ~~~~~~~~~~~
389
390impl<'g, 'a, G, T, SC> RepAnalysis<G, SlaterDeterminant<'a, T, SC>, T, Ix2>
391    for SlaterDeterminantSymmetryOrbit<'g, 'a, G, T, SC>
392where
393    G: SymmetryGroupProperties,
394    G::CharTab: SubspaceDecomposable<T>,
395    T: Lapack
396        + ComplexFloat<Real = <T as Scalar>::Real>
397        + fmt::Debug
398        + Mul<<T as ComplexFloat>::Real, Output = T>,
399    <T as ComplexFloat>::Real: fmt::Debug
400        + Zero
401        + From<u16>
402        + ToPrimitive
403        + approx::RelativeEq<<T as ComplexFloat>::Real>
404        + approx::AbsDiffEq<Epsilon = <T as Scalar>::Real>,
405    SC: StructureConstraint + Eq + fmt::Display,
406    SlaterDeterminant<'a, T, SC>: SymmetryTransformable,
407{
408    fn set_smat(&mut self, smat: Array2<T>) {
409        self.smat = Some(smat)
410    }
411
412    fn smat(&self) -> Option<&Array2<T>> {
413        self.smat.as_ref()
414    }
415
416    fn xmat(&self) -> &Array2<T> {
417        self.xmat
418            .as_ref()
419            .expect("Orbit overlap orthogonalisation matrix not found.")
420    }
421
422    fn norm_preserving_scalar_map(&self, i: usize) -> Result<fn(T) -> T, anyhow::Error> {
423        if self.origin.complex_symmetric {
424            Err(format_err!(
425                "`norm_preserving_scalar_map` is currently not implemented for complex-symmetric overlaps. This thus precludes the use of the Cayley table to speed up the computation of the orbit overlap matrix."
426            ))
427        } else if self
428            .group
429            .get_index(i)
430            .unwrap_or_else(|| panic!("Group operation index `{i}` not found."))
431            .contains_time_reversal()
432        {
433            Ok(ComplexFloat::conj)
434        } else {
435            Ok(|x| x)
436        }
437    }
438
439    fn orbit_overlap_symmetry_threshold(&self) -> <T as ComplexFloat>::Real {
440        self.orbit_overlap_symmetry_threshold
441    }
442
443    fn integrality_threshold(&self) -> <T as ComplexFloat>::Real {
444        self.integrality_threshold
445    }
446
447    fn eigenvalue_comparison_mode(&self) -> &EigenvalueComparisonMode {
448        &self.eigenvalue_comparison_mode
449    }
450
451    /// Reduces the representation or corepresentation spanned by the determinants in the orbit to
452    /// a direct sum of the irreducible representations or corepresentations of the generating
453    /// symmetry group.
454    ///
455    /// # Returns
456    ///
457    /// The decomposed result.
458    ///
459    /// # Errors
460    ///
461    /// Errors if the decomposition fails, *e.g.* because one or more calculated multiplicities
462    /// are non-integral, or also because the combination of group type, transformation type, and
463    /// oddity of the number of electrons would not give sensible symmetry results. In particular,
464    /// spin or spin-spatial symmetry analysis of odd-electron systems in unitary-represented
465    /// magnetic groups is not valid.
466    fn analyse_rep(
467        &self,
468    ) -> Result<
469        <<G as CharacterProperties>::CharTab as SubspaceDecomposable<T>>::Decomposition,
470        DecompositionError,
471    > {
472        log::debug!("Analysing representation symmetry for a Slater determinant...");
473        let nelectrons_float = self.origin().nelectrons();
474        if approx::relative_eq!(
475            nelectrons_float.round(),
476            nelectrons_float,
477            epsilon = self.integrality_threshold,
478            max_relative = self.integrality_threshold
479        ) {
480            let nelectrons_usize = nelectrons_float.round().to_usize().unwrap_or_else(|| {
481                panic!(
482                    "Unable to convert the number of electrons `{nelectrons_float:.7}` to `usize`."
483                );
484            });
485            let (valid_symmetry, err_str) = if nelectrons_usize.rem_euclid(2) == 0 {
486                // Even number of electrons; always valid
487                (true, String::new())
488            } else {
489                // Odd number of electrons; validity depends on group and orbit type
490                match self.symmetry_transformation_kind {
491                    SymmetryTransformationKind::Spatial => (true, String::new()),
492                    SymmetryTransformationKind::SpatialWithSpinTimeReversal
493                        | SymmetryTransformationKind::Spin
494                        | SymmetryTransformationKind::SpinSpatial => {
495                        match self.group().group_type() {
496                            GroupType::Ordinary(_) => (true, String::new()),
497                            GroupType::MagneticGrey(_) | GroupType::MagneticBlackWhite(_) => {
498                                (!self.group().unitary_represented(),
499                                "Unitary-represented magnetic groups cannot be used for symmetry analysis of odd-electron systems where spin is treated explicitly.".to_string())
500                            }
501                        }
502                    }
503                }
504            };
505
506            if valid_symmetry {
507                let chis = self
508                    .calc_characters()
509                    .map_err(|err| DecompositionError(err.to_string()))?;
510                log::debug!("Characters calculated.");
511
512                log_subtitle("Determinant orbit characters");
513                qsym2_output!("");
514                self.characters_to_string(&chis, self.integrality_threshold)
515                    .log_output_display();
516                qsym2_output!("");
517
518                let res = self.group().character_table().reduce_characters(
519                    &chis.iter().map(|(cc, chi)| (cc, *chi)).collect::<Vec<_>>(),
520                    self.integrality_threshold(),
521                );
522                log::debug!("Characters reduced.");
523                log::debug!("Analysing representation symmetry for a Slater determinant... Done.");
524                res
525            } else {
526                Err(DecompositionError(err_str))
527            }
528        } else {
529            Err(DecompositionError(format!(
530                "Symmetry analysis for determinant with non-integer number of electrons `{nelectrons_float:.7}` (threshold = {:.3e}) not supported.",
531                self.integrality_threshold
532            )))
533        }
534    }
535}