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};
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///
40/// # Constructor arguments
41///
42/// * `coefficients` - The real coefficients for the vibrational coordinates of this collection.
43/// Python type: `list[numpy.2darray[float]]`.
44/// * `frequencies` - The real vibrational frequencies. Python type: `numpy.1darray[float]`.
45/// * `threshold` - The threshold for comparisons. Python type: `float`.
46#[pyclass]
47#[derive(Clone)]
48pub struct PyVibrationalCoordinateCollectionReal {
49    /// The real coefficients for the vibrational coordinates of this collection.
50    ///
51    /// Python type: `list[numpy.2darray[float]]`.
52    coefficients: Array2<f64>,
53
54    /// The real vibrational frequencies.
55    ///
56    /// Python type: `numpy.1darray[float]`.
57    frequencies: Array1<f64>,
58
59    /// The threshold for comparisons.
60    ///
61    /// Python type: `float`.
62    threshold: f64,
63}
64
65#[pymethods]
66impl PyVibrationalCoordinateCollectionReal {
67    /// Constructs a real vibrational coordinate collection.
68    ///
69    /// # Arguments
70    ///
71    /// * `coefficients` - The real coefficients for the vibrational coordinates of this collection.
72    /// Python type: `list[numpy.2darray[float]]`.
73    /// * `frequencies` - The real vibrational frequencies. Python type: `numpy.1darray[float]`.
74    /// * `threshold` - The threshold for comparisons. Python type: `float`.
75    #[new]
76    fn new(
77        coefficients: Bound<'_, PyArray2<f64>>,
78        frequencies: Bound<'_, PyArray1<f64>>,
79        threshold: f64,
80    ) -> Self {
81        let vibs = Self {
82            coefficients: coefficients.to_owned_array(),
83            frequencies: frequencies.to_owned_array(),
84            threshold,
85        };
86        vibs
87    }
88}
89
90impl PyVibrationalCoordinateCollectionReal {
91    /// Extracts the information in the [`PyVibrationalCoordinateCollectionReal`] structure into
92    /// `QSym2`'s native [`VibrationalCoordinateCollection`] structure.
93    ///
94    /// # Arguments
95    ///
96    /// * `mol` - The molecule with which the vibrational coordinates are associated.
97    ///
98    /// # Returns
99    ///
100    /// The [`VibrationalCoordinateCollection`] structure with the same information.
101    ///
102    /// # Errors
103    ///
104    /// Errors if the [`VibrationalCoordinateCollection`] fails to build.
105    fn to_qsym2<'b, 'a: 'b>(
106        &'b self,
107        mol: &'a Molecule,
108    ) -> Result<VibrationalCoordinateCollection<'b, f64>, anyhow::Error> {
109        let vibs = VibrationalCoordinateCollection::<f64>::builder()
110            .mol(mol)
111            .coefficients(self.coefficients.clone())
112            .frequencies(self.frequencies.clone())
113            .threshold(self.threshold)
114            .build()
115            .map_err(|err| format_err!(err));
116        vibs
117    }
118}
119
120/// Python-exposed structure to marshall complex vibrational coordinate collections between Rust
121/// and Python.
122///
123/// # Constructor arguments
124///
125/// * `coefficients` - The complex coefficients for the vibrational coordinates of this collection.
126/// Python type: `list[numpy.2darray[complex]]`.
127/// * `frequencies` - The complex vibrational frequencies. Python type: `numpy.1darray[complex]`.
128/// * `threshold` - The threshold for comparisons. Python type: `float`.
129#[pyclass]
130#[derive(Clone)]
131pub struct PyVibrationalCoordinateCollectionComplex {
132    /// The complex coefficients for the vibrational coordinates of this collection.
133    ///
134    /// Python type: `list[numpy.2darray[complex]]`.
135    coefficients: Array2<C128>,
136
137    /// The complex vibrational frequencies.
138    ///
139    /// Python type: `numpy.1darray[complex]`.
140    frequencies: Array1<C128>,
141
142    /// The threshold for comparisons.
143    ///
144    /// Python type: `float`.
145    threshold: f64,
146}
147
148#[pymethods]
149impl PyVibrationalCoordinateCollectionComplex {
150    /// Constructs a complex vibrational coordinate collection.
151    ///
152    /// # Arguments
153    ///
154    /// * `coefficients` - The complex coefficients for the vibrational coordinates of this
155    /// collection.
156    /// Python type: `list[numpy.2darray[complex]]`.
157    /// * `frequencies` - The complex vibrational frequencies. Python type: `numpy.1darray[complex]`.
158    /// * `threshold` - The threshold for comparisons. Python type: `float`.
159    #[new]
160    fn new(
161        coefficients: Bound<'_, PyArray2<C128>>,
162        frequencies: Bound<'_, PyArray1<C128>>,
163        threshold: f64,
164    ) -> Self {
165        let vibs = Self {
166            coefficients: coefficients.to_owned_array(),
167            frequencies: frequencies.to_owned_array(),
168            threshold,
169        };
170        vibs
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. Python type: `str`.
232/// * `pyvibs` - A Python-exposed vibrational coordinate collection whose coefficients are of type `
233/// float64` or `complex128`.
234/// Python type: `PyVibrationalCoordinateCollectionReal | PyVibrationalCoordinateCollectionComplex`
235/// * `integrality_threshold` - The threshold for verifying if subspace multiplicities are
236/// integral. Python type: `float`.
237/// * `linear_independence_threshold` - The threshold for determining the linear independence
238/// subspace via the non-zero eigenvalues of the orbit overlap matrix. Python type: `float`.
239/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
240/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
241/// should be used. Python type: `None | MagneticSymmetryAnalysisKind`.
242/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
243/// group is to be used for representation analysis instead. Python type: `bool`.
244/// * `use_cayley_table` - A boolean indicating if the Cayley table for the group, if available,
245/// should be used to speed up the calculation of orbit overlap matrices. Python type: `bool`.
246/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
247/// transformations to be performed on the origin determinant to generate the orbit. If this
248/// contains spin transformation, the determinant will be augmented to generalised spin constraint
249/// automatically. Python type: `SymmetryTransformationKind`.
250/// * `eigenvalue_comparison_mode` - An enumerated type indicating the mode of comparison of orbit
251/// overlap eigenvalues with the specified `linear_independence_threshold`.
252/// Python type: `EigenvalueComparisonMode`.
253/// * `write_character_table` - A boolean indicating if the character table of the prevailing
254/// symmetry group is to be printed out. Python type: `bool`.
255/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
256/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
257/// will be used for symmetry analysis. Python type: `Optional[int]`.
258/// * `angular_function_integrality_threshold` - The threshold for verifying if subspace
259/// multiplicities are integral for the symmetry analysis of angular functions. Python type:
260/// `float`.
261/// * `angular_function_linear_independence_threshold` - The threshold for determining the linear
262/// independence subspace via the non-zero eigenvalues of the orbit overlap matrix for the symmetry
263/// analysis of angular functions. Python type: `float`.
264/// * `angular_function_max_angular_momentum` - The maximum angular momentum order to be used in
265/// angular function symmetry analysis. Python type: `int`.
266#[pyfunction]
267#[pyo3(signature = (
268    inp_sym,
269    pyvibs,
270    integrality_threshold,
271    linear_independence_threshold,
272    use_magnetic_group,
273    use_double_group,
274    use_cayley_table,
275    symmetry_transformation_kind,
276    eigenvalue_comparison_mode,
277    write_character_table=true,
278    infinite_order_to_finite=None,
279    angular_function_integrality_threshold=1e-7,
280    angular_function_linear_independence_threshold=1e-7,
281    angular_function_max_angular_momentum=2
282))]
283pub fn rep_analyse_vibrational_coordinate_collection(
284    py: Python<'_>,
285    inp_sym: PathBuf,
286    pyvibs: PyVibrationalCoordinateCollection,
287    integrality_threshold: f64,
288    linear_independence_threshold: f64,
289    use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
290    use_double_group: bool,
291    use_cayley_table: bool,
292    symmetry_transformation_kind: SymmetryTransformationKind,
293    eigenvalue_comparison_mode: EigenvalueComparisonMode,
294    write_character_table: bool,
295    infinite_order_to_finite: Option<u32>,
296    angular_function_integrality_threshold: f64,
297    angular_function_linear_independence_threshold: f64,
298    angular_function_max_angular_momentum: u32,
299) -> PyResult<()> {
300    py.allow_threads(|| {
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            .max_angular_momentum(angular_function_max_angular_momentum)
319            .build()
320            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
321        match &pyvibs {
322            PyVibrationalCoordinateCollection::Real(pyvibs_r) => {
323                let vibs_r = pyvibs_r
324                    .to_qsym2(mol)
325                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
326                let vca_params = VibrationalCoordinateRepAnalysisParams::<f64>::builder()
327                    .integrality_threshold(integrality_threshold)
328                    .linear_independence_threshold(linear_independence_threshold)
329                    .use_magnetic_group(use_magnetic_group.clone())
330                    .use_double_group(use_double_group)
331                    .use_cayley_table(use_cayley_table)
332                    .symmetry_transformation_kind(symmetry_transformation_kind)
333                    .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
334                    .write_character_table(if write_character_table {
335                        Some(CharacterTableDisplay::Symbolic)
336                    } else {
337                        None
338                    })
339                    .infinite_order_to_finite(infinite_order_to_finite)
340                    .build()
341                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
342                match &use_magnetic_group {
343                    Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
344                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
345                            MagneticRepresentedSymmetryGroup,
346                            f64,
347                        >::builder()
348                        .parameters(&vca_params)
349                        .angular_function_parameters(&afa_params)
350                        .vibrational_coordinate_collection(&vibs_r)
351                        .symmetry_group(&pd_res)
352                        .build()
353                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
354                        vca_driver
355                            .run()
356                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
357                    }
358                    Some(MagneticSymmetryAnalysisKind::Representation) | None => {
359                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
360                            UnitaryRepresentedSymmetryGroup,
361                            f64,
362                        >::builder()
363                        .parameters(&vca_params)
364                        .angular_function_parameters(&afa_params)
365                        .vibrational_coordinate_collection(&vibs_r)
366                        .symmetry_group(&pd_res)
367                        .build()
368                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
369                        vca_driver
370                            .run()
371                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
372                    }
373                };
374            }
375            PyVibrationalCoordinateCollection::Complex(pyvibs_c) => {
376                let vibs_c = pyvibs_c
377                    .to_qsym2(mol)
378                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
379                let vca_params = VibrationalCoordinateRepAnalysisParams::<f64>::builder()
380                    .integrality_threshold(integrality_threshold)
381                    .linear_independence_threshold(linear_independence_threshold)
382                    .use_magnetic_group(use_magnetic_group.clone())
383                    .use_double_group(use_double_group)
384                    .use_cayley_table(use_cayley_table)
385                    .symmetry_transformation_kind(symmetry_transformation_kind)
386                    .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
387                    .write_character_table(if write_character_table {
388                        Some(CharacterTableDisplay::Symbolic)
389                    } else {
390                        None
391                    })
392                    .infinite_order_to_finite(infinite_order_to_finite)
393                    .build()
394                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
395                match &use_magnetic_group {
396                    Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
397                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
398                            MagneticRepresentedSymmetryGroup,
399                            C128,
400                        >::builder()
401                        .parameters(&vca_params)
402                        .angular_function_parameters(&afa_params)
403                        .vibrational_coordinate_collection(&vibs_c)
404                        .symmetry_group(&pd_res)
405                        .build()
406                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
407                        vca_driver
408                            .run()
409                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
410                    }
411                    Some(MagneticSymmetryAnalysisKind::Representation) | None => {
412                        let mut vca_driver = VibrationalCoordinateRepAnalysisDriver::<
413                            UnitaryRepresentedSymmetryGroup,
414                            C128,
415                        >::builder()
416                        .parameters(&vca_params)
417                        .angular_function_parameters(&afa_params)
418                        .vibrational_coordinate_collection(&vibs_c)
419                        .symmetry_group(&pd_res)
420                        .build()
421                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
422                        vca_driver
423                            .run()
424                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
425                    }
426                };
427            }
428        }
429        Ok(())
430    })
431}