qsym2/bindings/python/representation_analysis/
vibrational_coordinate.rs

1//! Python bindings for QSym² symmetry analysis of vibrational coordinates.
2
3use std::path::PathBuf;
4
5use anyhow::format_err;
6use ndarray::{Array1, Array2};
7use num_complex::Complex;
8use numpy::{PyArray1, PyArray2, PyArrayMethods, ToPyArray};
9use pyo3::exceptions::{PyIOError, PyRuntimeError};
10use pyo3::prelude::*;
11
12use crate::analysis::EigenvalueComparisonMode;
13use crate::auxiliary::molecule::Molecule;
14use crate::drivers::QSym2Driver;
15use crate::drivers::representation_analysis::angular_function::AngularFunctionRepAnalysisParams;
16use crate::drivers::representation_analysis::vibrational_coordinate::{
17    VibrationalCoordinateRepAnalysisDriver, VibrationalCoordinateRepAnalysisParams,
18};
19use crate::drivers::representation_analysis::{
20    CharacterTableDisplay, MagneticSymmetryAnalysisKind,
21};
22use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
23use crate::io::format::qsym2_output;
24use crate::io::{QSym2FileType, read_qsym2_binary};
25use crate::symmetry::symmetry_group::{
26    MagneticRepresentedSymmetryGroup, UnitaryRepresentedSymmetryGroup,
27};
28use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
29use crate::target::vibration::VibrationalCoordinateCollection;
30
31type C128 = Complex<f64>;
32
33// ==================
34// Struct definitions
35// ==================
36
37/// Python-exposed structure to marshall real vibrational coordinate collections between Rust and
38/// Python.
39#[pyclass]
40#[derive(Clone)]
41pub struct PyVibrationalCoordinateCollectionReal {
42    /// The real coefficients for the vibrational coordinates of this collection.
43    coefficients: Array2<f64>,
44
45    /// The real vibrational frequencies.
46    frequencies: Array1<f64>,
47
48    /// The threshold for comparisons.
49    #[pyo3(get)]
50    threshold: f64,
51}
52
53#[pymethods]
54impl PyVibrationalCoordinateCollectionReal {
55    /// Constructs a real vibrational coordinate collection.
56    ///
57    /// # Arguments
58    ///
59    /// * `coefficients` - The real coefficients for the vibrational coordinates of this collection.
60    /// * `frequencies` - The real vibrational frequencies.
61    /// * `threshold` - The threshold for comparisons.
62    #[new]
63    fn new(
64        coefficients: Bound<'_, PyArray2<f64>>,
65        frequencies: Bound<'_, PyArray1<f64>>,
66        threshold: f64,
67    ) -> Self {
68        Self {
69            coefficients: coefficients.to_owned_array(),
70            frequencies: frequencies.to_owned_array(),
71            threshold,
72        }
73    }
74
75    #[getter]
76    fn coefficients<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<f64>>> {
77        Ok(self.coefficients.to_pyarray(py))
78    }
79
80    #[getter]
81    fn frequencies<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray1<f64>>> {
82        Ok(self.frequencies.to_pyarray(py))
83    }
84}
85
86impl PyVibrationalCoordinateCollectionReal {
87    /// Extracts the information in the [`PyVibrationalCoordinateCollectionReal`] structure into
88    /// `QSym2`'s native [`VibrationalCoordinateCollection`] structure.
89    ///
90    /// # Arguments
91    ///
92    /// * `mol` - The molecule with which the vibrational coordinates are associated.
93    ///
94    /// # Returns
95    ///
96    /// The [`VibrationalCoordinateCollection`] structure with the same information.
97    ///
98    /// # Errors
99    ///
100    /// Errors if the [`VibrationalCoordinateCollection`] fails to build.
101    fn to_qsym2<'b, 'a: 'b>(
102        &'b self,
103        mol: &'a Molecule,
104    ) -> Result<VibrationalCoordinateCollection<'b, f64>, anyhow::Error> {
105        VibrationalCoordinateCollection::<f64>::builder()
106            .mol(mol)
107            .coefficients(self.coefficients.clone())
108            .frequencies(self.frequencies.clone())
109            .threshold(self.threshold)
110            .build()
111            .map_err(|err| format_err!(err))
112    }
113}
114
115/// Python-exposed structure to marshall complex vibrational coordinate collections between Rust
116/// and Python.
117///
118/// # Constructor arguments
119///
120/// * `coefficients` - The complex coefficients for the vibrational coordinates of this collection.
121/// * `frequencies` - The complex vibrational frequencies.
122/// * `threshold` - The threshold for comparisons.
123#[pyclass]
124#[derive(Clone)]
125pub struct PyVibrationalCoordinateCollectionComplex {
126    /// The complex coefficients for the vibrational coordinates of this collection.
127    coefficients: Array2<C128>,
128
129    /// The complex vibrational frequencies.
130    frequencies: Array1<C128>,
131
132    /// The threshold for comparisons.
133    #[pyo3(get)]
134    threshold: f64,
135}
136
137#[pymethods]
138impl PyVibrationalCoordinateCollectionComplex {
139    /// Constructs a complex vibrational coordinate collection.
140    ///
141    /// # Arguments
142    ///
143    /// * `coefficients` - The complex coefficients for the vibrational coordinates of this
144    /// collection.
145    /// * `frequencies` - The complex vibrational frequencies.
146    /// * `threshold` - The threshold for comparisons.
147    #[new]
148    fn new(
149        coefficients: Bound<'_, PyArray2<C128>>,
150        frequencies: Bound<'_, PyArray1<C128>>,
151        threshold: f64,
152    ) -> Self {
153        Self {
154            coefficients: coefficients.to_owned_array(),
155            frequencies: frequencies.to_owned_array(),
156            threshold,
157        }
158    }
159
160    #[getter]
161    fn coefficients<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<C128>>> {
162        Ok(self.coefficients.to_pyarray(py))
163    }
164
165    #[getter]
166    fn frequencies<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray1<C128>>> {
167        Ok(self.frequencies.to_pyarray(py))
168    }
169}
170
171impl PyVibrationalCoordinateCollectionComplex {
172    /// Extracts the information in the [`PyVibrationalCoordinateCollectionComplex`] structure into
173    /// `QSym2`'s native [`VibrationalCoordinateCollection`] structure.
174    ///
175    /// # Arguments
176    ///
177    /// * `mol` - The molecule with which the vibrational coordinates are associated.
178    ///
179    /// # Returns
180    ///
181    /// The [`VibrationalCoordinateCollection`] structure with the same information.
182    ///
183    /// # Errors
184    ///
185    /// Errors if the [`VibrationalCoordinateCollection`] fails to build.
186    fn to_qsym2<'b, 'a: 'b>(
187        &'b self,
188        mol: &'a Molecule,
189    ) -> Result<VibrationalCoordinateCollection<'b, C128>, anyhow::Error> {
190        VibrationalCoordinateCollection::<C128>::builder()
191            .mol(mol)
192            .coefficients(self.coefficients.clone())
193            .frequencies(self.frequencies.clone())
194            .threshold(self.threshold)
195            .build()
196            .map_err(|err| format_err!(err))
197    }
198}
199
200// ================
201// Enum definitions
202// ================
203
204/// Python-exposed enumerated type to handle the union type
205/// `PyVibrationalCoordinateCollectionReal | PyVibrationalCoordinateCollectionComplex` in Python.
206#[derive(FromPyObject)]
207pub enum PyVibrationalCoordinateCollection {
208    /// Variant for complex Python-exposed vibrational coordinate collection.
209    Real(PyVibrationalCoordinateCollectionReal),
210
211    /// Variant for complex Python-exposed vibrational coordinate collection.
212    Complex(PyVibrationalCoordinateCollectionComplex),
213}
214
215// =====================
216// Functions definitions
217// =====================
218
219/// Python-exposed function to perform representation symmetry analysis for real and complex
220/// vibrational coordinate collections and log the result via the `qsym2-output` logger at the
221/// `INFO` level.
222///
223/// # Arguments
224///
225/// * `inp_sym` - A path to the [`QSym2FileType::Sym`] file containing the symmetry-group detection
226/// result for the system. This will be used to construct abstract groups and character tables for
227/// representation analysis.
228/// * `pyvibs` - A Python-exposed vibrational coordinate collection whose coefficients are of type
229/// `float64` or `complex128`.
230/// * `integrality_threshold` - The threshold for verifying if subspace multiplicities are
231/// integral.
232/// * `linear_independence_threshold` - The threshold for determining the linear independence
233/// subspace via the non-zero eigenvalues of the orbit overlap matrix.
234/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
235/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
236/// should be used.
237/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
238/// group is to be used for representation analysis instead.
239/// * `use_cayley_table` - A boolean indicating if the Cayley table for the group, if available,
240/// should be used to speed up the calculation of orbit overlap matrices.
241/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
242/// transformations to be performed on the origin vibrational coordinate to generate the orbit.
243/// * `eigenvalue_comparison_mode` - An enumerated type indicating the mode of comparison of orbit
244/// overlap eigenvalues with the specified `linear_independence_threshold`.
245/// * `write_character_table` - A boolean indicating if the character table of the prevailing
246/// symmetry group is to be printed out.
247/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
248/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
249/// will be used for symmetry analysis.
250/// * `angular_function_integrality_threshold` - The threshold for verifying if subspace
251/// multiplicities are integral for the symmetry analysis of angular functions.
252/// * `angular_function_linear_independence_threshold` - The threshold for determining the linear
253/// independence subspace via the non-zero eigenvalues of the orbit overlap matrix for the symmetry
254/// analysis of angular functions.
255/// * `angular_function_max_angular_momentum` - The maximum angular momentum order to be used in
256/// angular function symmetry analysis.
257#[allow(clippy::too_many_arguments)]
258#[pyfunction]
259#[pyo3(signature = (
260    inp_sym,
261    pyvibs,
262    integrality_threshold,
263    linear_independence_threshold,
264    use_magnetic_group,
265    use_double_group,
266    use_cayley_table,
267    symmetry_transformation_kind,
268    eigenvalue_comparison_mode,
269    write_character_table=true,
270    infinite_order_to_finite=None,
271    angular_function_integrality_threshold=1e-7,
272    angular_function_linear_independence_threshold=1e-7,
273    angular_function_max_angular_momentum=2
274))]
275pub fn rep_analyse_vibrational_coordinate_collection(
276    py: Python<'_>,
277    inp_sym: PathBuf,
278    pyvibs: PyVibrationalCoordinateCollection,
279    integrality_threshold: f64,
280    linear_independence_threshold: f64,
281    use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
282    use_double_group: bool,
283    use_cayley_table: bool,
284    symmetry_transformation_kind: SymmetryTransformationKind,
285    eigenvalue_comparison_mode: EigenvalueComparisonMode,
286    write_character_table: bool,
287    infinite_order_to_finite: Option<u32>,
288    angular_function_integrality_threshold: f64,
289    angular_function_linear_independence_threshold: f64,
290    angular_function_max_angular_momentum: u32,
291) -> PyResult<()> {
292    py.detach(|| {
293        let pd_res: SymmetryGroupDetectionResult =
294            read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
295                .map_err(|err| PyIOError::new_err(err.to_string()))?;
296
297        let mut file_name = inp_sym.to_path_buf();
298        file_name.set_extension(QSym2FileType::Sym.ext());
299        qsym2_output!(
300            "Symmetry-group detection results read in from {}.",
301            file_name.display(),
302        );
303        qsym2_output!("");
304
305        let mol = &pd_res.pre_symmetry.recentred_molecule;
306
307        let afa_params = AngularFunctionRepAnalysisParams::builder()
308            .integrality_threshold(angular_function_integrality_threshold)
309            .linear_independence_threshold(angular_function_linear_independence_threshold)
310            .max_angular_momentum(angular_function_max_angular_momentum)
311            .build()
312            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
313        match &pyvibs {
314            PyVibrationalCoordinateCollection::Real(pyvibs_r) => {
315                let vibs_r = pyvibs_r
316                    .to_qsym2(mol)
317                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
318                let vca_params = VibrationalCoordinateRepAnalysisParams::<f64>::builder()
319                    .integrality_threshold(integrality_threshold)
320                    .linear_independence_threshold(linear_independence_threshold)
321                    .use_magnetic_group(use_magnetic_group.clone())
322                    .use_double_group(use_double_group)
323                    .use_cayley_table(use_cayley_table)
324                    .symmetry_transformation_kind(symmetry_transformation_kind)
325                    .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
326                    .write_character_table(if write_character_table {
327                        Some(CharacterTableDisplay::Symbolic)
328                    } else {
329                        None
330                    })
331                    .infinite_order_to_finite(infinite_order_to_finite)
332                    .build()
333                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
334                match &use_magnetic_group {
335                    Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
336                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
337                            MagneticRepresentedSymmetryGroup,
338                            f64,
339                        >::builder()
340                        .parameters(&vca_params)
341                        .angular_function_parameters(&afa_params)
342                        .vibrational_coordinate_collection(&vibs_r)
343                        .symmetry_group(&pd_res)
344                        .build()
345                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
346                        vca_driver
347                            .run()
348                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
349                    }
350                    Some(MagneticSymmetryAnalysisKind::Representation) | None => {
351                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
352                            UnitaryRepresentedSymmetryGroup,
353                            f64,
354                        >::builder()
355                        .parameters(&vca_params)
356                        .angular_function_parameters(&afa_params)
357                        .vibrational_coordinate_collection(&vibs_r)
358                        .symmetry_group(&pd_res)
359                        .build()
360                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
361                        vca_driver
362                            .run()
363                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
364                    }
365                };
366            }
367            PyVibrationalCoordinateCollection::Complex(pyvibs_c) => {
368                let vibs_c = pyvibs_c
369                    .to_qsym2(mol)
370                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
371                let vca_params = VibrationalCoordinateRepAnalysisParams::<f64>::builder()
372                    .integrality_threshold(integrality_threshold)
373                    .linear_independence_threshold(linear_independence_threshold)
374                    .use_magnetic_group(use_magnetic_group.clone())
375                    .use_double_group(use_double_group)
376                    .use_cayley_table(use_cayley_table)
377                    .symmetry_transformation_kind(symmetry_transformation_kind)
378                    .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
379                    .write_character_table(if write_character_table {
380                        Some(CharacterTableDisplay::Symbolic)
381                    } else {
382                        None
383                    })
384                    .infinite_order_to_finite(infinite_order_to_finite)
385                    .build()
386                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
387                match &use_magnetic_group {
388                    Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
389                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
390                            MagneticRepresentedSymmetryGroup,
391                            C128,
392                        >::builder()
393                        .parameters(&vca_params)
394                        .angular_function_parameters(&afa_params)
395                        .vibrational_coordinate_collection(&vibs_c)
396                        .symmetry_group(&pd_res)
397                        .build()
398                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
399                        vca_driver
400                            .run()
401                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
402                    }
403                    Some(MagneticSymmetryAnalysisKind::Representation) | None => {
404                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
405                            UnitaryRepresentedSymmetryGroup,
406                            C128,
407                        >::builder()
408                        .parameters(&vca_params)
409                        .angular_function_parameters(&afa_params)
410                        .vibrational_coordinate_collection(&vibs_c)
411                        .symmetry_group(&pd_res)
412                        .build()
413                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
414                        vca_driver
415                            .run()
416                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
417                    }
418                };
419            }
420        }
421        Ok(())
422    })
423}