Skip to main content

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/// * `orbit_overlap_symmetry_threshold` - The threshold for checking the symmetry of the orbit
235/// overlap matrix.
236/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
237/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
238/// should be used.
239/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
240/// group is to be used for representation analysis instead.
241/// * `use_cayley_table` - A boolean indicating if the Cayley table for the group, if available,
242/// should be used to speed up the calculation of orbit overlap matrices.
243/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
244/// transformations to be performed on the origin vibrational coordinate to generate the orbit.
245/// * `eigenvalue_comparison_mode` - An enumerated type indicating the mode of comparison of orbit
246/// overlap eigenvalues with the specified `linear_independence_threshold`.
247/// * `write_character_table` - A boolean indicating if the character table of the prevailing
248/// symmetry group is to be printed out.
249/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
250/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
251/// will be used for symmetry analysis.
252/// * `angular_function_integrality_threshold` - The threshold for verifying if subspace
253/// multiplicities are integral for the symmetry analysis of angular functions.
254/// * `angular_function_linear_independence_threshold` - The threshold for determining the linear
255/// independence subspace via the non-zero eigenvalues of the orbit overlap matrix for the symmetry
256/// analysis of angular functions.
257/// * `angular_function_orbit_overlap_symmetry_threshold` - The threshold for checking the symmetry
258/// of the orbit overlap matrices for the symmetry 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#[allow(clippy::too_many_arguments)]
262#[pyfunction]
263#[pyo3(signature = (
264    inp_sym,
265    pyvibs,
266    integrality_threshold,
267    linear_independence_threshold,
268    orbit_overlap_symmetry_threshold,
269    use_magnetic_group,
270    use_double_group,
271    use_cayley_table,
272    symmetry_transformation_kind,
273    eigenvalue_comparison_mode,
274    write_character_table=true,
275    infinite_order_to_finite=None,
276    angular_function_integrality_threshold=1e-7,
277    angular_function_linear_independence_threshold=1e-7,
278    angular_function_orbit_overlap_symmetry_threshold=1e-7,
279    angular_function_max_angular_momentum=2
280))]
281pub fn rep_analyse_vibrational_coordinate_collection(
282    py: Python<'_>,
283    inp_sym: PathBuf,
284    pyvibs: PyVibrationalCoordinateCollection,
285    integrality_threshold: f64,
286    linear_independence_threshold: f64,
287    orbit_overlap_symmetry_threshold: f64,
288    use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
289    use_double_group: bool,
290    use_cayley_table: bool,
291    symmetry_transformation_kind: SymmetryTransformationKind,
292    eigenvalue_comparison_mode: EigenvalueComparisonMode,
293    write_character_table: bool,
294    infinite_order_to_finite: Option<u32>,
295    angular_function_integrality_threshold: f64,
296    angular_function_linear_independence_threshold: f64,
297    angular_function_orbit_overlap_symmetry_threshold: f64,
298    angular_function_max_angular_momentum: u32,
299) -> PyResult<()> {
300    py.detach(|| {
301        let pd_res: SymmetryGroupDetectionResult =
302            read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
303                .map_err(|err| PyIOError::new_err(err.to_string()))?;
304
305        let mut file_name = inp_sym.to_path_buf();
306        file_name.set_extension(QSym2FileType::Sym.ext());
307        qsym2_output!(
308            "Symmetry-group detection results read in from {}.",
309            file_name.display(),
310        );
311        qsym2_output!("");
312
313        let mol = &pd_res.pre_symmetry.recentred_molecule;
314
315        let afa_params = AngularFunctionRepAnalysisParams::builder()
316            .integrality_threshold(angular_function_integrality_threshold)
317            .linear_independence_threshold(angular_function_linear_independence_threshold)
318            .orbit_overlap_symmetry_threshold(angular_function_orbit_overlap_symmetry_threshold)
319            .max_angular_momentum(angular_function_max_angular_momentum)
320            .build()
321            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
322        match &pyvibs {
323            PyVibrationalCoordinateCollection::Real(pyvibs_r) => {
324                let vibs_r = pyvibs_r
325                    .to_qsym2(mol)
326                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
327                let vca_params = VibrationalCoordinateRepAnalysisParams::<f64>::builder()
328                    .integrality_threshold(integrality_threshold)
329                    .linear_independence_threshold(linear_independence_threshold)
330                    .orbit_overlap_symmetry_threshold(orbit_overlap_symmetry_threshold)
331                    .use_magnetic_group(use_magnetic_group.clone())
332                    .use_double_group(use_double_group)
333                    .use_cayley_table(use_cayley_table)
334                    .symmetry_transformation_kind(symmetry_transformation_kind)
335                    .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
336                    .write_character_table(if write_character_table {
337                        Some(CharacterTableDisplay::Symbolic)
338                    } else {
339                        None
340                    })
341                    .infinite_order_to_finite(infinite_order_to_finite)
342                    .build()
343                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
344                match &use_magnetic_group {
345                    Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
346                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
347                            MagneticRepresentedSymmetryGroup,
348                            f64,
349                        >::builder()
350                        .parameters(&vca_params)
351                        .angular_function_parameters(&afa_params)
352                        .vibrational_coordinate_collection(&vibs_r)
353                        .symmetry_group(&pd_res)
354                        .build()
355                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
356                        vca_driver
357                            .run()
358                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
359                    }
360                    Some(MagneticSymmetryAnalysisKind::Representation) | None => {
361                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
362                            UnitaryRepresentedSymmetryGroup,
363                            f64,
364                        >::builder()
365                        .parameters(&vca_params)
366                        .angular_function_parameters(&afa_params)
367                        .vibrational_coordinate_collection(&vibs_r)
368                        .symmetry_group(&pd_res)
369                        .build()
370                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
371                        vca_driver
372                            .run()
373                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
374                    }
375                };
376            }
377            PyVibrationalCoordinateCollection::Complex(pyvibs_c) => {
378                let vibs_c = pyvibs_c
379                    .to_qsym2(mol)
380                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
381                let vca_params = VibrationalCoordinateRepAnalysisParams::<f64>::builder()
382                    .integrality_threshold(integrality_threshold)
383                    .linear_independence_threshold(linear_independence_threshold)
384                    .orbit_overlap_symmetry_threshold(orbit_overlap_symmetry_threshold)
385                    .use_magnetic_group(use_magnetic_group.clone())
386                    .use_double_group(use_double_group)
387                    .use_cayley_table(use_cayley_table)
388                    .symmetry_transformation_kind(symmetry_transformation_kind)
389                    .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
390                    .write_character_table(if write_character_table {
391                        Some(CharacterTableDisplay::Symbolic)
392                    } else {
393                        None
394                    })
395                    .infinite_order_to_finite(infinite_order_to_finite)
396                    .build()
397                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
398                match &use_magnetic_group {
399                    Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
400                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
401                            MagneticRepresentedSymmetryGroup,
402                            C128,
403                        >::builder()
404                        .parameters(&vca_params)
405                        .angular_function_parameters(&afa_params)
406                        .vibrational_coordinate_collection(&vibs_c)
407                        .symmetry_group(&pd_res)
408                        .build()
409                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
410                        vca_driver
411                            .run()
412                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
413                    }
414                    Some(MagneticSymmetryAnalysisKind::Representation) | None => {
415                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
416                            UnitaryRepresentedSymmetryGroup,
417                            C128,
418                        >::builder()
419                        .parameters(&vca_params)
420                        .angular_function_parameters(&afa_params)
421                        .vibrational_coordinate_collection(&vibs_c)
422                        .symmetry_group(&pd_res)
423                        .build()
424                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
425                        vca_driver
426                            .run()
427                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
428                    }
429                };
430            }
431        }
432        Ok(())
433    })
434}