Skip to main content

qsym2/target/density/
density_analysis.rs

1//! Implementation of symmetry analysis for electron densities.
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;
10use log;
11use ndarray::{Array1, Array2, Array4, Axis, Ix4};
12use ndarray_einsum::*;
13use ndarray_linalg::{
14    UPLO,
15    eig::Eig,
16    eigh::Eigh,
17    norm::Norm,
18    types::{Lapack, Scalar},
19};
20use num_complex::{Complex, ComplexFloat};
21use num_traits::{Float, ToPrimitive, Zero};
22
23use crate::analysis::{
24    EigenvalueComparisonMode, Orbit, OrbitIterator, Overlap, RepAnalysis, fn_calc_xmat_complex,
25    fn_calc_xmat_real,
26};
27use crate::auxiliary::misc::complex_modified_gram_schmidt;
28use crate::chartab::chartab_group::CharacterProperties;
29use crate::chartab::{DecompositionError, SubspaceDecomposable};
30use crate::symmetry::symmetry_element::symmetry_operation::SpecialSymmetryTransformation;
31use crate::symmetry::symmetry_group::SymmetryGroupProperties;
32use crate::symmetry::symmetry_transformation::{SymmetryTransformable, SymmetryTransformationKind};
33use crate::target::density::Density;
34
35// =======
36// Overlap
37// =======
38
39impl<'a, T> Overlap<T, Ix4> for Density<'a, T>
40where
41    T: Lapack
42        + ComplexFloat<Real = <T as Scalar>::Real>
43        + fmt::Debug
44        + Mul<<T as ComplexFloat>::Real, Output = T>,
45    <T as ComplexFloat>::Real: fmt::Debug
46        + approx::RelativeEq<<T as ComplexFloat>::Real>
47        + approx::AbsDiffEq<Epsilon = <T as Scalar>::Real>,
48{
49    fn complex_symmetric(&self) -> bool {
50        self.complex_symmetric
51    }
52
53    /// Computes the overlap between two densities.
54    ///
55    /// When one or both of the densities have been acted on by an antiunitary operation, the
56    /// correct Hermitian or complex-symmetric metric will be chosen in the evalulation of the
57    /// overlap.
58    ///
59    /// # Panics
60    ///
61    /// Panics if `self` and `other` have mismatched spin constraints.
62    fn overlap(
63        &self,
64        other: &Self,
65        metric: Option<&Array4<T>>,
66        metric_h: Option<&Array4<T>>,
67    ) -> Result<T, anyhow::Error> {
68        ensure!(
69            self.density_matrix.shape() == other.density_matrix.shape(),
70            "Inconsistent shapes of density matrices between `self` and `other`."
71        );
72        ensure!(
73            self.bao == other.bao,
74            "Inconsistent basis angular order between `self` and `other`."
75        );
76        let dennorm = self.density_matrix().norm_l2();
77        ensure!(
78            approx::abs_diff_ne!(
79                dennorm,
80                <T as ndarray_linalg::Scalar>::Real::zero(),
81                epsilon = self.threshold()
82            ),
83            "Zero density (density matrix has Frobenius norm {dennorm:.3e})"
84        );
85
86        let sao_4c = metric
87            .ok_or_else(|| format_err!("No atomic-orbital four-centre overlap tensor found for density overlap calculation."))?;
88        let sao_4c_h = metric_h.unwrap_or(sao_4c);
89
90        if self.complex_symmetric() {
91            match (self.complex_conjugated, other.complex_conjugated) {
92                (false, false) => einsum(
93                    "ijkl,ji,lk->",
94                    &[
95                        &sao_4c_h.view(),
96                        &self.density_matrix().view(),
97                        &other.density_matrix().view(),
98                    ],
99                )
100                .map_err(|err| format_err!(err))?
101                .into_iter()
102                .next()
103                .ok_or(format_err!("Unable to extract the density overlap scalar.")),
104                (true, false) => einsum(
105                    "ijkl,ji,lk->",
106                    &[
107                        &sao_4c.view(),
108                        &self.density_matrix().view(),
109                        &other.density_matrix().view(),
110                    ],
111                )
112                .map_err(|err| format_err!(err))?
113                .into_iter()
114                .next()
115                .ok_or(format_err!("Unable to extract the density overlap scalar.")),
116                (false, true) => einsum(
117                    "ijkl,ji,lk->",
118                    &[
119                        &sao_4c.view(),
120                        &other.density_matrix().view(),
121                        &self.density_matrix().view(),
122                    ],
123                )
124                .map_err(|err| format_err!(err))?
125                .into_iter()
126                .next()
127                .ok_or(format_err!("Unable to extract the density overlap scalar.")),
128                (true, true) => einsum(
129                    "klij,ji,lk->",
130                    &[
131                        &sao_4c_h.view(),
132                        &self.density_matrix().view(),
133                        &other.density_matrix().view(),
134                    ],
135                )
136                .map_err(|err| format_err!(err))?
137                .into_iter()
138                .next()
139                .ok_or(format_err!("Unable to extract the density overlap scalar.")),
140            }
141        } else {
142            match (self.complex_conjugated, other.complex_conjugated) {
143                (false, false) => einsum(
144                    "ijkl,ji,lk->",
145                    &[
146                        &sao_4c.view(),
147                        &self.density_matrix().mapv(|x| x.conj()).view(),
148                        &other.density_matrix().view(),
149                    ],
150                )
151                .map_err(|err| format_err!(err))?
152                .into_iter()
153                .next()
154                .ok_or(format_err!("Unable to extract the density overlap scalar.")),
155                (true, false) => einsum(
156                    "ijkl,ji,lk->",
157                    &[
158                        &sao_4c_h.view(),
159                        &self.density_matrix().mapv(|x| x.conj()).view(),
160                        &other.density_matrix().view(),
161                    ],
162                )
163                .map_err(|err| format_err!(err))?
164                .into_iter()
165                .next()
166                .ok_or(format_err!("Unable to extract the density overlap scalar.")),
167                (false, true) => einsum(
168                    "ijkl,ji,lk->",
169                    &[
170                        &sao_4c_h.view(),
171                        &other.density_matrix().mapv(|x| x.conj()).view(),
172                        &self.density_matrix().view(),
173                    ],
174                )
175                .map_err(|err| format_err!(err))?
176                .into_iter()
177                .next()
178                .ok_or(format_err!("Unable to extract the density overlap scalar."))
179                .map(|x| x.conj()),
180                (true, true) => einsum(
181                    "klij,ji,lk->",
182                    &[
183                        &sao_4c.view(),
184                        &self.density_matrix().mapv(|x| x.conj()).view(),
185                        &other.density_matrix().view(),
186                    ],
187                )
188                .map_err(|err| format_err!(err))?
189                .into_iter()
190                .next()
191                .ok_or(format_err!("Unable to extract the density overlap scalar.")),
192            }
193        }
194    }
195
196    /// Returns the mathematical definition of the overlap between two densities.
197    fn overlap_definition(&self) -> String {
198        let k = if self.complex_symmetric() { "κ " } else { "" };
199        format!("⟨{k}ρ_1|ρ_2⟩ = ∫ [{k}ρ_1(r)]* ρ_2(r) dr")
200    }
201}
202
203// ====================
204// DensitySymmetryOrbit
205// ====================
206
207// -----------------
208// Struct definition
209// -----------------
210
211/// Structure to manage symmetry orbits (*i.e.* orbits generated by symmetry groups) of
212/// densities.
213#[derive(Builder, Clone)]
214pub struct DensitySymmetryOrbit<'a, G, T>
215where
216    G: SymmetryGroupProperties,
217    T: ComplexFloat + fmt::Debug + Lapack,
218    Density<'a, T>: SymmetryTransformable,
219{
220    /// The generating symmetry group.
221    group: &'a G,
222
223    /// The origin density of the orbit.
224    origin: &'a Density<'a, T>,
225
226    /// The threshold for determining zero eigenvalues in the orbit overlap matrix.
227    pub(crate) linear_independence_threshold: <T as ComplexFloat>::Real,
228
229    /// The threshold for checking symmetry of the orbit overlap matrix.
230    orbit_overlap_symmetry_threshold: <T as ComplexFloat>::Real,
231
232    /// The threshold for determining if calculated multiplicities in representation analysis are
233    /// integral.
234    integrality_threshold: <T as ComplexFloat>::Real,
235
236    /// The kind of transformation determining the way the symmetry operations in `group` act on
237    /// [`Self::origin`].
238    symmetry_transformation_kind: SymmetryTransformationKind,
239
240    /// The overlap matrix between the symmetry-equivalent densities in the orbit.
241    #[builder(setter(skip), default = "None")]
242    smat: Option<Array2<T>>,
243
244    /// The eigenvalues of the overlap matrix between the symmetry-equivalent densities in
245    /// the orbit.
246    #[builder(setter(skip), default = "None")]
247    pub(crate) smat_eigvals: Option<Array1<T>>,
248
249    /// The $`\mathbf{X}`$ matrix for the overlap matrix between the symmetry-equivalent densities
250    /// in the orbit.
251    ///
252    /// See [`RepAnalysis::xmat`] for further information.
253    #[builder(setter(skip), default = "None")]
254    xmat: Option<Array2<T>>,
255
256    /// An enumerated type specifying the comparison mode for filtering out orbit overlap
257    /// eigenvalues.
258    eigenvalue_comparison_mode: EigenvalueComparisonMode,
259}
260
261// ----------------------------
262// Struct method implementation
263// ----------------------------
264
265impl<'a, G, T> DensitySymmetryOrbit<'a, G, T>
266where
267    G: SymmetryGroupProperties + Clone,
268    T: ComplexFloat + fmt::Debug + Lapack,
269    Density<'a, T>: SymmetryTransformable,
270{
271    /// Returns a builder for constructing a new density symmetry orbit.
272    pub fn builder() -> DensitySymmetryOrbitBuilder<'a, G, T> {
273        DensitySymmetryOrbitBuilder::default()
274    }
275}
276
277impl<'a, G> DensitySymmetryOrbit<'a, G, f64>
278where
279    G: SymmetryGroupProperties,
280{
281    fn_calc_xmat_real!(
282        /// Calculates the $`\mathbf{X}`$ matrix for real and symmetric overlap matrix
283        /// $`\mathbf{S}`$ between the symmetry-equivalent densities in the orbit.
284        ///
285        /// The resulting $`\mathbf{X}`$ is stored in the orbit.
286        ///
287        /// # Arguments
288        ///
289        /// * `preserves_full_rank` - If `true`, when $`\mathbf{S}`$ is already of full rank, then
290        /// $`\mathbf{X}`$ is set to be the identity matrix to avoid mixing the orbit densities. If
291        /// `false`, $`\mathbf{X}`$ also orthogonalises $`\mathbf{S}`$ even when it is already of
292        /// full rank.
293        pub calc_xmat
294    );
295}
296
297impl<'a, G, T> DensitySymmetryOrbit<'a, G, Complex<T>>
298where
299    G: SymmetryGroupProperties,
300    T: Float + Scalar<Complex = Complex<T>>,
301    Complex<T>: ComplexFloat<Real = T> + Scalar<Real = T, Complex = Complex<T>> + Lapack,
302    Density<'a, Complex<T>>: SymmetryTransformable + Overlap<Complex<T>, Ix4>,
303{
304    fn_calc_xmat_complex!(
305        /// Calculates the $`\mathbf{X}`$ matrix for complex and symmetric or Hermitian overlap
306        /// matrix $`\mathbf{S}`$ between the symmetry-equivalent densities in the orbit.
307        ///
308        /// The resulting $`\mathbf{X}`$ is stored in the orbit.
309        ///
310        /// # Arguments
311        ///
312        /// * `preserves_full_rank` - If `true`, when $`\mathbf{S}`$ is already of full rank, then
313        /// $`\mathbf{X}`$ is set to be the identity matrix to avoid mixing the orbit densities. If
314        /// `false`, $`\mathbf{X}`$ also orthogonalises $`\mathbf{S}`$ even when it is already of
315        /// full rank.
316        pub calc_xmat
317    );
318}
319
320// ---------------------
321// Trait implementations
322// ---------------------
323
324// ~~~~~
325// Orbit
326// ~~~~~
327
328impl<'a, G, T> Orbit<G, Density<'a, T>> for DensitySymmetryOrbit<'a, G, T>
329where
330    G: SymmetryGroupProperties,
331    T: ComplexFloat + fmt::Debug + Lapack,
332    Density<'a, T>: SymmetryTransformable,
333{
334    type OrbitIter = OrbitIterator<'a, G, Density<'a, T>>;
335
336    fn group(&self) -> &G {
337        self.group
338    }
339
340    fn origin(&self) -> &Density<'a, T> {
341        self.origin
342    }
343
344    fn iter(&self) -> Self::OrbitIter {
345        OrbitIterator::new(
346            self.group,
347            self.origin,
348            match self.symmetry_transformation_kind {
349                SymmetryTransformationKind::Spatial => |op, det| {
350                    det.sym_transform_spatial(op).with_context(|| {
351                        format!("Unable to apply `{op}` spatially on the origin density")
352                    })
353                },
354                SymmetryTransformationKind::SpatialWithSpinTimeReversal => |op, det| {
355                    det.sym_transform_spatial_with_spintimerev(op).with_context(|| {
356                        format!("Unable to apply `{op}` spatially (with spin-including time-reversal) on the origin density")
357                    })
358                },
359                SymmetryTransformationKind::Spin => |op, det| {
360                    det.sym_transform_spin(op).with_context(|| {
361                        format!("Unable to apply `{op}` spin-wise on the origin density")
362                    })
363                },
364                SymmetryTransformationKind::SpinSpatial => |op, det| {
365                    det.sym_transform_spin_spatial(op).with_context(|| {
366                        format!("Unable to apply `{op}` spin-spatially on the origin density")
367                    })
368                },
369            },
370        )
371    }
372}
373
374// ~~~~~~~~~~~
375// RepAnalysis
376// ~~~~~~~~~~~
377
378impl<'a, G, T> RepAnalysis<G, Density<'a, T>, T, Ix4> for DensitySymmetryOrbit<'a, G, T>
379where
380    G: SymmetryGroupProperties,
381    G::CharTab: SubspaceDecomposable<T>,
382    T: Lapack
383        + ComplexFloat<Real = <T as Scalar>::Real>
384        + fmt::Debug
385        + Mul<<T as ComplexFloat>::Real, Output = T>,
386    <T as ComplexFloat>::Real: fmt::Debug
387        + Zero
388        + From<u16>
389        + ToPrimitive
390        + approx::RelativeEq<<T as ComplexFloat>::Real>
391        + approx::AbsDiffEq<Epsilon = <T as Scalar>::Real>,
392    Density<'a, T>: SymmetryTransformable,
393{
394    fn set_smat(&mut self, smat: Array2<T>) {
395        self.smat = Some(smat)
396    }
397
398    fn smat(&self) -> Option<&Array2<T>> {
399        self.smat.as_ref()
400    }
401
402    fn xmat(&self) -> &Array2<T> {
403        self.xmat
404            .as_ref()
405            .expect("Orbit overlap orthogonalisation matrix not found.")
406    }
407
408    fn norm_preserving_scalar_map(&self, i: usize) -> Result<fn(T) -> T, anyhow::Error> {
409        if self.origin.complex_symmetric {
410            Err(format_err!(
411                "`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."
412            ))
413        } else if self
414            .group
415            .get_index(i)
416            .unwrap_or_else(|| panic!("Group operation index `{i}` not found."))
417            .contains_time_reversal()
418        {
419            Ok(ComplexFloat::conj)
420        } else {
421            Ok(|x| x)
422        }
423    }
424
425    fn orbit_overlap_symmetry_threshold(&self) -> <T as ComplexFloat>::Real {
426        self.orbit_overlap_symmetry_threshold
427    }
428
429    fn integrality_threshold(&self) -> <T as ComplexFloat>::Real {
430        self.integrality_threshold
431    }
432
433    fn eigenvalue_comparison_mode(&self) -> &EigenvalueComparisonMode {
434        &self.eigenvalue_comparison_mode
435    }
436
437    /// Reduces the representation or corepresentation spanned by the densities in the orbit to
438    /// a direct sum of the irreducible representations or corepresentations of the generating
439    /// symmetry group.
440    ///
441    /// # Returns
442    ///
443    /// The decomposed result.
444    ///
445    /// # Errors
446    ///
447    /// Errors if the decomposition fails, *e.g.* because one or more calculated multiplicities
448    /// are non-integral.
449    fn analyse_rep(
450        &self,
451    ) -> Result<
452        <<G as CharacterProperties>::CharTab as SubspaceDecomposable<T>>::Decomposition,
453        DecompositionError,
454    > {
455        log::debug!("Analysing representation symmetry for a density...");
456        let chis = self
457            .calc_characters()
458            .map_err(|err| DecompositionError(err.to_string()))?;
459        log::debug!("Characters calculated.");
460        let res = self.group().character_table().reduce_characters(
461            &chis.iter().map(|(cc, chi)| (cc, *chi)).collect::<Vec<_>>(),
462            self.integrality_threshold(),
463        );
464        log::debug!("Characters reduced.");
465        log::debug!("Analysing representation symmetry for a density... Done.");
466        res
467    }
468}