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