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}