Skip to main content

qsym2/target/vibration/
vibration_analysis.rs

1//! Implementation of symmetry analysis for vibrational coordinates.
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 ndarray::{Array1, Array2, Axis, Ix2};
11use ndarray_linalg::{
12    UPLO,
13    eig::Eig,
14    eigh::Eigh,
15    types::{Lapack, Scalar},
16};
17use num_complex::{Complex, ComplexFloat};
18use num_traits::{Float, Zero};
19
20use crate::analysis::{
21    EigenvalueComparisonMode, Orbit, OrbitIterator, Overlap, RepAnalysis, fn_calc_xmat_complex,
22    fn_calc_xmat_real,
23};
24use crate::auxiliary::misc::complex_modified_gram_schmidt;
25use crate::chartab::SubspaceDecomposable;
26use crate::symmetry::symmetry_element::symmetry_operation::SpecialSymmetryTransformation;
27use crate::symmetry::symmetry_group::SymmetryGroupProperties;
28use crate::symmetry::symmetry_transformation::{SymmetryTransformable, SymmetryTransformationKind};
29use crate::target::vibration::VibrationalCoordinate;
30
31// -------
32// Overlap
33// -------
34
35impl<'a, T> Overlap<T, Ix2> for VibrationalCoordinate<'a, T>
36where
37    T: Lapack
38        + ComplexFloat<Real = <T as Scalar>::Real>
39        + fmt::Debug
40        + Mul<<T as ComplexFloat>::Real, Output = T>,
41    <T as ComplexFloat>::Real: fmt::Debug
42        + approx::RelativeEq<<T as ComplexFloat>::Real>
43        + approx::AbsDiffEq<Epsilon = <T as Scalar>::Real>,
44{
45    fn complex_symmetric(&self) -> bool {
46        false
47    }
48
49    /// Computes the overlap between two vibrational coordinates.
50    ///
51    /// Typically, `metric` is `None`, which means that the local orthogonal Cartesian coordinates
52    /// are non-overlapping.
53    ///
54    /// # Panics
55    ///
56    /// Panics if `self` and `other` have mismatched coefficient array lengths.
57    fn overlap(
58        &self,
59        other: &Self,
60        metric: Option<&Array2<T>>,
61        _: Option<&Array2<T>>,
62    ) -> Result<T, anyhow::Error> {
63        ensure!(
64            self.coefficients.len() == other.coefficients.len(),
65            "Inconsistent numbers of coefficient matrices between `self` and `other`."
66        );
67
68        let ov = if let Some(s) = metric {
69            self.coefficients
70                .t()
71                .mapv(|x| x.conj())
72                .dot(s)
73                .dot(&other.coefficients)
74        } else {
75            self.coefficients
76                .t()
77                .mapv(|x| x.conj())
78                .dot(&other.coefficients)
79        };
80        Ok(ov)
81    }
82
83    /// Returns the mathematical definition of the overlap between two vibrational coordinates.
84    fn overlap_definition(&self) -> String {
85        let k = if self.complex_symmetric() { "κ " } else { "" };
86        format!("⟨{k}v_1|v_2⟩ = [{k}v_1]† g v_2    where g is an optional metric")
87    }
88}
89
90// ==================================
91// VibrationalCoordinateSymmetryOrbit
92// ==================================
93
94// -----------------
95// Struct definition
96// -----------------
97
98/// Structure to manage symmetry orbits (*i.e.* orbits generated by symmetry groups) of
99/// vibrational coordinates.
100#[derive(Builder, Clone)]
101pub struct VibrationalCoordinateSymmetryOrbit<'a, G, T>
102where
103    G: SymmetryGroupProperties,
104    T: ComplexFloat + fmt::Debug + Lapack,
105    VibrationalCoordinate<'a, T>: SymmetryTransformable,
106{
107    /// The generating symmetry group.
108    group: &'a G,
109
110    /// The origin vibrational coordinate of the orbit.
111    origin: &'a VibrationalCoordinate<'a, T>,
112
113    /// The threshold for determining if calculated multiplicities in representation analysis are
114    /// integral.
115    integrality_threshold: <T as ComplexFloat>::Real,
116
117    /// The threshold for determining zero eigenvalues in the orbit overlap matrix.
118    pub(crate) linear_independence_threshold: <T as ComplexFloat>::Real,
119
120    /// The threshold for checking symmetry of the orbit overlap matrix.
121    orbit_overlap_symmetry_threshold: <T as ComplexFloat>::Real,
122
123    /// The kind of transformation determining the way the symmetry operations in `group` act on
124    /// [`Self::origin`].
125    symmetry_transformation_kind: SymmetryTransformationKind,
126
127    /// The overlap matrix between the symmetry-equivalent vibrational coordinates in the orbit.
128    #[builder(setter(skip), default = "None")]
129    smat: Option<Array2<T>>,
130
131    /// The eigenvalues of the overlap matrix between the symmetry-equivalent vibrational
132    /// coordinates in the orbit.
133    #[builder(setter(skip), default = "None")]
134    pub(crate) smat_eigvals: Option<Array1<T>>,
135
136    /// The $`\mathbf{X}`$ matrix for the overlap matrix between the symmetry-equivalent
137    /// vibrational coordinates in the orbit.
138    ///
139    /// See [`RepAnalysis::xmat`] for further information.
140    #[builder(setter(skip), default = "None")]
141    xmat: Option<Array2<T>>,
142
143    /// An enumerated type specifying the comparison mode for filtering out orbit overlap
144    /// eigenvalues.
145    pub(crate) eigenvalue_comparison_mode: EigenvalueComparisonMode,
146}
147
148// ----------------------------
149// Struct method implementation
150// ----------------------------
151
152impl<'a, G, T> VibrationalCoordinateSymmetryOrbit<'a, G, T>
153where
154    G: SymmetryGroupProperties + Clone,
155    T: ComplexFloat + fmt::Debug + Lapack,
156    VibrationalCoordinate<'a, T>: SymmetryTransformable,
157{
158    /// Returns a builder to construct a new [`VibrationalCoordinateSymmetryOrbit`].
159    pub fn builder() -> VibrationalCoordinateSymmetryOrbitBuilder<'a, G, T> {
160        VibrationalCoordinateSymmetryOrbitBuilder::default()
161    }
162}
163
164impl<'a, G> VibrationalCoordinateSymmetryOrbit<'a, G, f64>
165where
166    G: SymmetryGroupProperties,
167{
168    fn_calc_xmat_real!(
169        /// Calculates the $`\mathbf{X}`$ matrix for real and symmetric overlap matrix
170        /// $`\mathbf{S}`$ between the symmetry-equivalent vibrational coordinates in the orbit.
171        ///
172        /// The resulting $`\mathbf{X}`$ is stored in the orbit.
173        ///
174        /// # Arguments
175        ///
176        /// * `preserves_full_rank` - If `true`, when $`\mathbf{S}`$ is already of full rank, then
177        /// $`\mathbf{X}`$ is set to be the identity matrix to avoid mixing the orbit molecular
178        /// orbitals. If `false`, $`\mathbf{X}`$ also orthogonalises $`\mathbf{S}`$ even when it is
179        /// already of full rank.
180        pub calc_xmat
181    );
182}
183
184impl<'a, G, T> VibrationalCoordinateSymmetryOrbit<'a, G, Complex<T>>
185where
186    G: SymmetryGroupProperties,
187    T: Float + Scalar<Complex = Complex<T>>,
188    Complex<T>: ComplexFloat<Real = T> + Scalar<Real = T, Complex = Complex<T>> + Lapack,
189    VibrationalCoordinate<'a, Complex<T>>: SymmetryTransformable + Overlap<Complex<T>, Ix2>,
190{
191    fn_calc_xmat_complex!(
192        /// Calculates the $`\mathbf{X}`$ matrix for complex and symmetric or Hermitian overlap
193        /// matrix $`\mathbf{S}`$ between the symmetry-equivalent vibrational coordinates in the
194        /// orbit.
195        ///
196        /// The resulting $`\mathbf{X}`$ is stored in the orbit.
197        ///
198        /// # Arguments
199        ///
200        /// * `preserves_full_rank` - If `true`, when $`\mathbf{S}`$ is already of full rank, then
201        /// $`\mathbf{X}`$ is set to be the identity matrix to avoid mixing the orbit molecular
202        /// orbitals. If `false`, $`\mathbf{X}`$ also orthogonalises $`\mathbf{S}`$ even when it is
203        /// already of full rank.
204        pub calc_xmat
205    );
206}
207
208// ---------------------
209// Trait implementations
210// ---------------------
211
212// ~~~~~
213// Orbit
214// ~~~~~
215
216impl<'a, G, T> Orbit<G, VibrationalCoordinate<'a, T>>
217    for VibrationalCoordinateSymmetryOrbit<'a, G, T>
218where
219    G: SymmetryGroupProperties,
220    T: ComplexFloat + fmt::Debug + Lapack,
221    VibrationalCoordinate<'a, T>: SymmetryTransformable,
222{
223    type OrbitIter = OrbitIterator<'a, G, VibrationalCoordinate<'a, T>>;
224
225    fn group(&self) -> &G {
226        self.group
227    }
228
229    fn origin(&self) -> &VibrationalCoordinate<'a, T> {
230        self.origin
231    }
232
233    fn iter(&self) -> Self::OrbitIter {
234        OrbitIterator::new(
235            self.group,
236            self.origin,
237            match self.symmetry_transformation_kind {
238                SymmetryTransformationKind::Spatial
239                | SymmetryTransformationKind::SpatialWithSpinTimeReversal => |op, vib| {
240                    // Vibrational coordinates are time-even, so both `sym_transform_spatial` and
241                    // `sym_transform_spatial_with_spintimerev` would give the same thing.
242                    vib.sym_transform_spatial(op).with_context(|| {
243                        format_err!(
244                            "Unable to apply `{op}` spatially on the origin vibrational coordinate"
245                        )
246                    })
247                },
248                SymmetryTransformationKind::Spin => |op, vib| {
249                    vib.sym_transform_spin(op).with_context(|| {
250                        format_err!(
251                            "Unable to apply `{op}` spin-wise on the origin vibrational coordinate"
252                        )
253                    })
254                },
255                SymmetryTransformationKind::SpinSpatial => |op, vib| {
256                    vib.sym_transform_spin_spatial(op).with_context(|| {
257                        format_err!(
258                            "Unable to apply `{op}` spin-spatially on the origin vibrational coordinate"
259                        )
260                    })
261                },
262            },
263        )
264    }
265}
266
267// ~~~~~~~~~~~
268// RepAnalysis
269// ~~~~~~~~~~~
270
271impl<'a, G, T> RepAnalysis<G, VibrationalCoordinate<'a, T>, T, Ix2>
272    for VibrationalCoordinateSymmetryOrbit<'a, G, T>
273where
274    G: SymmetryGroupProperties,
275    G::CharTab: SubspaceDecomposable<T>,
276    T: Lapack
277        + ComplexFloat<Real = <T as Scalar>::Real>
278        + fmt::Debug
279        + Mul<<T as ComplexFloat>::Real, Output = T>,
280    <T as ComplexFloat>::Real: fmt::Debug
281        + Zero
282        + approx::RelativeEq<<T as ComplexFloat>::Real>
283        + approx::AbsDiffEq<Epsilon = <T as Scalar>::Real>,
284    VibrationalCoordinate<'a, T>: SymmetryTransformable,
285{
286    fn set_smat(&mut self, smat: Array2<T>) {
287        self.smat = Some(smat)
288    }
289
290    fn smat(&self) -> Option<&Array2<T>> {
291        self.smat.as_ref()
292    }
293
294    fn xmat(&self) -> &Array2<T> {
295        self.xmat
296            .as_ref()
297            .expect("Orbit overlap orthogonalisation matrix not found.")
298    }
299
300    fn norm_preserving_scalar_map(&self, i: usize) -> Result<fn(T) -> T, anyhow::Error> {
301        if self.origin.complex_symmetric() {
302            Err(format_err!(
303                "`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."
304            ))
305        } else if self
306            .group
307            .get_index(i)
308            .unwrap_or_else(|| panic!("Group operation index `{i}` not found."))
309            .contains_time_reversal()
310        {
311            Ok(ComplexFloat::conj)
312        } else {
313            Ok(|x| x)
314        }
315    }
316
317    fn orbit_overlap_symmetry_threshold(&self) -> <T as ComplexFloat>::Real {
318        self.orbit_overlap_symmetry_threshold
319    }
320
321    fn integrality_threshold(&self) -> <T as ComplexFloat>::Real {
322        self.integrality_threshold
323    }
324
325    fn eigenvalue_comparison_mode(&self) -> &EigenvalueComparisonMode {
326        &self.eigenvalue_comparison_mode
327    }
328}