Skip to main content

qsym2/bindings/python/representation_analysis/
density.rs

1//! Python bindings for QSym² symmetry analysis of electron densities.
2
3use std::path::PathBuf;
4
5use anyhow::format_err;
6use ndarray::{Array2, Array4};
7use num_complex::Complex;
8use numpy::{PyArray2, PyArrayMethods, ToPyArray};
9use pyo3::exceptions::{PyIOError, PyRuntimeError};
10use pyo3::prelude::*;
11
12use crate::analysis::EigenvalueComparisonMode;
13use crate::auxiliary::molecule::Molecule;
14use crate::basis::ao::BasisAngularOrder;
15use crate::bindings::python::integrals::PyBasisAngularOrder;
16use crate::bindings::python::representation_analysis::PyArray4RC;
17use crate::drivers::QSym2Driver;
18use crate::drivers::representation_analysis::angular_function::AngularFunctionRepAnalysisParams;
19use crate::drivers::representation_analysis::density::{
20    DensityRepAnalysisDriver, DensityRepAnalysisParams,
21};
22use crate::drivers::representation_analysis::{
23    CharacterTableDisplay, MagneticSymmetryAnalysisKind,
24};
25use crate::drivers::symmetry_group_detection::SymmetryGroupDetectionResult;
26use crate::io::format::qsym2_output;
27use crate::io::{QSym2FileType, read_qsym2_binary};
28use crate::symmetry::symmetry_group::{
29    MagneticRepresentedSymmetryGroup, UnitaryRepresentedSymmetryGroup,
30};
31use crate::symmetry::symmetry_transformation::SymmetryTransformationKind;
32use crate::target::density::Density;
33
34type C128 = Complex<f64>;
35
36// ==================
37// Struct definitions
38// ==================
39
40/// Python-exposed structure to marshall real electron density information between Rust and Python.
41#[pyclass]
42#[derive(Clone)]
43pub struct PyDensityReal {
44    /// A boolean indicating if inner products involving this density should be the
45    /// complex-symmetric bilinear form, rather than the conventional Hermitian sesquilinear form.
46    #[pyo3(get)]
47    pub complex_symmetric: bool,
48
49    /// The real density matrix describing this density.
50    pub density_matrix: Array2<f64>,
51
52    /// The threshold for comparing densities.
53    #[pyo3(get)]
54    pub threshold: f64,
55}
56
57#[pymethods]
58impl PyDensityReal {
59    /// Constructs a real Python-exposed electron density.
60    ///
61    /// * `complex_symmetric` - A boolean indicating if inner products involving this density
62    /// are complex-symmetric.
63    /// * `density_matrix` - The real density matrix describing this density.
64    /// * `threshold` - The threshold for comparisons.
65    #[new]
66    fn new(
67        complex_symmetric: bool,
68        density_matrix: Bound<'_, PyArray2<f64>>,
69        threshold: f64,
70    ) -> Self {
71        Self {
72            complex_symmetric,
73            density_matrix: density_matrix.to_owned_array(),
74            threshold,
75        }
76    }
77
78    #[getter]
79    fn density_matrix<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<f64>>> {
80        Ok(self.density_matrix.to_pyarray(py))
81    }
82}
83
84impl PyDensityReal {
85    /// Extracts the information in the [`PyDensityReal`] structure into `QSym2`'s native
86    /// [`Density`] structure.
87    ///
88    /// # Arguments
89    ///
90    /// * `bao` - The [`BasisAngularOrder`] for the basis set in which the density is given.
91    /// * `mol` - The molecule with which the density is associated.
92    ///
93    /// # Returns
94    ///
95    /// The [`Density`] structure with the same information.
96    ///
97    /// # Errors
98    ///
99    /// Errors if the [`Density`] fails to build.
100    pub(crate) fn to_qsym2<'b, 'a: 'b>(
101        &'b self,
102        bao: &'a BasisAngularOrder,
103        mol: &'a Molecule,
104    ) -> Result<Density<'b, f64>, anyhow::Error> {
105        Density::<f64>::builder()
106            .bao(bao)
107            .complex_symmetric(self.complex_symmetric)
108            .mol(mol)
109            .density_matrix(self.density_matrix.clone())
110            .threshold(self.threshold)
111            .build()
112            .map_err(|err| format_err!(err))
113    }
114}
115
116/// Python-exposed structure to marshall complex electron density information between Rust and Python.
117#[pyclass]
118#[derive(Clone)]
119pub struct PyDensityComplex {
120    /// A boolean indicating if inner products involving this density should be the
121    /// complex-symmetric bilinear form, rather than the conventional Hermitian sesquilinear form.
122    #[pyo3(get)]
123    pub complex_symmetric: bool,
124
125    /// The complex density matrix describing this density.
126    pub density_matrix: Array2<C128>,
127
128    /// The threshold for comparing densities.
129    #[pyo3(get)]
130    pub threshold: f64,
131}
132
133#[pymethods]
134impl PyDensityComplex {
135    /// Constructs a complex Python-exposed electron density.
136    ///
137    /// # Arguments
138    ///
139    /// * `complex_symmetric` - A boolean indicating if inner products involving this density
140    /// are complex-symmetric.
141    /// * `density_matrix` - The complex density matrix describing this density.
142    /// * `threshold` - The threshold for comparisons.
143    #[new]
144    fn new(
145        complex_symmetric: bool,
146        density_matrix: Bound<'_, PyArray2<C128>>,
147        threshold: f64,
148    ) -> Self {
149        Self {
150            complex_symmetric,
151            density_matrix: density_matrix.to_owned_array(),
152            threshold,
153        }
154    }
155
156    #[getter]
157    fn density_matrix<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<C128>>> {
158        Ok(self.density_matrix.to_pyarray(py))
159    }
160}
161
162impl PyDensityComplex {
163    /// Extracts the information in the [`PyDensityComplex`] structure into `QSym2`'s native
164    /// [`Density`] structure.
165    ///
166    /// # Arguments
167    ///
168    /// * `bao` - The [`BasisAngularOrder`] for the basis set in which the density is given.
169    /// * `mol` - The molecule with which the density is associated.
170    ///
171    /// # Returns
172    ///
173    /// The [`Density`] structure with the same information.
174    ///
175    /// # Errors
176    ///
177    /// Errors if the [`Density`] fails to build.
178    pub(crate) fn to_qsym2<'b, 'a: 'b>(
179        &'b self,
180        bao: &'a BasisAngularOrder,
181        mol: &'a Molecule,
182    ) -> Result<Density<'b, C128>, anyhow::Error> {
183        Density::<C128>::builder()
184            .bao(bao)
185            .complex_symmetric(self.complex_symmetric)
186            .mol(mol)
187            .density_matrix(self.density_matrix.clone())
188            .threshold(self.threshold)
189            .build()
190            .map_err(|err| format_err!(err))
191    }
192}
193
194// ================
195// Enum definitions
196// ================
197
198/// Python-exposed enumerated type to handle the union type `PyDensityReal | PyDensityComplex` in
199/// Python.
200#[derive(FromPyObject)]
201pub enum PyDensity {
202    /// Variant for real Python-exposed electron density.
203    Real(PyDensityReal),
204
205    /// Variant for complex Python-exposed electron density.
206    Complex(PyDensityComplex),
207}
208
209// =====================
210// Functions definitions
211// =====================
212
213/// Python-exposed function to perform representation symmetry analysis for real and complex
214/// electron densities and log the result via the `qsym2-output` logger at the `INFO` level.
215///
216/// # Arguments
217///
218/// * `inp_sym` - A path to the [`QSym2FileType::Sym`] file containing the symmetry-group detection
219/// result for the system. This will be used to construct abstract groups and character tables for
220/// representation analysis.
221/// * `pydens` - A sequence of Python-exposed electron densities whose density matrices are of type
222/// `float64` or `complex128`. Each density is accompanied by a description string.
223/// * `pybao` - Python-exposed structure containing basis angular order information for the density
224/// matrices.
225/// * `integrality_threshold` - The threshold for verifying if subspace multiplicities are integral.
226/// * `linear_independence_threshold` - The threshold for determining the linear independence
227/// subspace via the non-zero eigenvalues of the orbit overlap matrix.
228/// * `orbit_overlap_symmetry_threshold` - The threshold for checking the symmetry of the orbit
229/// overlap matrix.
230/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
231/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
232/// should be used.
233/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
234/// group is to be used for representation analysis instead.
235/// * `use_cayley_table` - A boolean indicating if the Cayley table for the group, if available,
236/// should be used to speed up the calculation of orbit overlap matrices.
237/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
238/// transformations to be performed on the origin electron density to generate the orbit.
239/// * `eigenvalue_comparison_mode` - An enumerated type indicating the mode of comparison of orbit
240/// overlap eigenvalues with the specified `linear_independence_threshold`.
241/// * `sao_spatial_4c` - The atomic-orbital four-centre overlap matrix whose elements are of type
242/// `float64` or `complex128`.
243/// * `sao_spatial_4c_h` - The optional complex-symmetric atomic-orbital four-centre overlap matrix
244/// whose elements are of type `float64` or `complex128`. This is required if antiunitary symmetry
245/// operations are involved.
246/// * `write_character_table` - A boolean indicating if the character table of the prevailing
247/// symmetry group is to be printed out.
248/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
249/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
250/// will be used for symmetry analysis.
251/// * `angular_function_integrality_threshold` - The threshold for verifying if subspace
252/// multiplicities are integral for the symmetry analysis of angular functions.
253/// * `angular_function_linear_independence_threshold` - The threshold for determining the linear
254/// independence subspace via the non-zero eigenvalues of the orbit overlap matrix for the symmetry
255/// analysis of angular functions.
256/// * `angular_function_orbit_overlap_symmetry_threshold` - The threshold for checking the symmetry
257/// of the orbit overlap matrices for the symmetry analysis of angular functions.
258/// * `angular_function_max_angular_momentum` - The maximum angular momentum order to be used in
259/// angular function symmetry analysis.
260#[allow(clippy::too_many_arguments)]
261#[pyfunction]
262#[pyo3(signature = (
263    inp_sym,
264    pydens,
265    pybao,
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    sao_spatial_4c,
275    sao_spatial_4c_h=None,
276    write_character_table=true,
277    infinite_order_to_finite=None,
278    angular_function_integrality_threshold=1e-7,
279    angular_function_linear_independence_threshold=1e-7,
280    angular_function_orbit_overlap_symmetry_threshold=1e-7,
281    angular_function_max_angular_momentum=2
282))]
283pub fn rep_analyse_densities(
284    py: Python<'_>,
285    inp_sym: PathBuf,
286    pydens: Vec<(String, PyDensity)>,
287    pybao: &PyBasisAngularOrder,
288    integrality_threshold: f64,
289    linear_independence_threshold: f64,
290    orbit_overlap_symmetry_threshold: f64,
291    use_magnetic_group: Option<MagneticSymmetryAnalysisKind>,
292    use_double_group: bool,
293    use_cayley_table: bool,
294    symmetry_transformation_kind: SymmetryTransformationKind,
295    eigenvalue_comparison_mode: EigenvalueComparisonMode,
296    sao_spatial_4c: PyArray4RC,
297    sao_spatial_4c_h: Option<PyArray4RC>,
298    write_character_table: bool,
299    infinite_order_to_finite: Option<u32>,
300    angular_function_integrality_threshold: f64,
301    angular_function_linear_independence_threshold: f64,
302    angular_function_orbit_overlap_symmetry_threshold: f64,
303    angular_function_max_angular_momentum: u32,
304) -> PyResult<()> {
305    let pd_res: SymmetryGroupDetectionResult =
306        read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
307            .map_err(|err| PyIOError::new_err(err.to_string()))?;
308
309    let mut file_name = inp_sym.to_path_buf();
310    file_name.set_extension(QSym2FileType::Sym.ext());
311    qsym2_output!(
312        "Symmetry-group detection results read in from {}.",
313        file_name.display(),
314    );
315    qsym2_output!("");
316
317    let mol = &pd_res.pre_symmetry.recentred_molecule;
318    let bao = pybao
319        .to_qsym2(mol)
320        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
321    let afa_params = AngularFunctionRepAnalysisParams::builder()
322        .integrality_threshold(angular_function_integrality_threshold)
323        .linear_independence_threshold(angular_function_linear_independence_threshold)
324        .orbit_overlap_symmetry_threshold(angular_function_orbit_overlap_symmetry_threshold)
325        .max_angular_momentum(angular_function_max_angular_momentum)
326        .build()
327        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
328    let sda_params = DensityRepAnalysisParams::<f64>::builder()
329        .integrality_threshold(integrality_threshold)
330        .linear_independence_threshold(linear_independence_threshold)
331        .orbit_overlap_symmetry_threshold(orbit_overlap_symmetry_threshold)
332        .use_magnetic_group(use_magnetic_group.clone())
333        .use_double_group(use_double_group)
334        .use_cayley_table(use_cayley_table)
335        .symmetry_transformation_kind(symmetry_transformation_kind)
336        .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
337        .write_character_table(if write_character_table {
338            Some(CharacterTableDisplay::Symbolic)
339        } else {
340            None
341        })
342        .infinite_order_to_finite(infinite_order_to_finite)
343        .build()
344        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
345
346    let any_complex = pydens
347        .iter()
348        .any(|(_, pyden)| matches!(pyden, PyDensity::Complex(_)));
349
350    match (any_complex, &sao_spatial_4c) {
351        (false, PyArray4RC::Real(pysao4c_r)) => {
352            // Both coefficients and sao_4c are real.
353            let dens = pydens
354                .iter()
355                .map(|(_, pyden)| match pyden {
356                    PyDensity::Real(pyden_r) => pyden_r.to_qsym2(&bao, mol),
357                    PyDensity::Complex(_) => panic!("Unexpected complex density."),
358                })
359                .collect::<Result<Vec<_>, _>>()
360                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
361            let dens_ref = dens
362                .iter()
363                .zip(pydens.iter())
364                .map(|(den, (desc, _))| (desc.clone(), den))
365                .collect::<Vec<_>>();
366
367            let sao_spatial_4c = pysao4c_r.to_owned_array();
368
369            match &use_magnetic_group {
370                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
371                    let mut da_driver =
372                        DensityRepAnalysisDriver::<MagneticRepresentedSymmetryGroup, f64>::builder(
373                        )
374                        .parameters(&sda_params)
375                        .angular_function_parameters(&afa_params)
376                        .densities(dens_ref)
377                        .sao_spatial_4c(&sao_spatial_4c)
378                        .sao_spatial_4c_h(None)
379                        .symmetry_group(&pd_res)
380                        .build()
381                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
382                    py.detach(|| {
383                        da_driver
384                            .run()
385                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
386                    })?
387                }
388                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
389                    let mut da_driver =
390                        DensityRepAnalysisDriver::<UnitaryRepresentedSymmetryGroup, f64>::builder()
391                            .parameters(&sda_params)
392                            .angular_function_parameters(&afa_params)
393                            .densities(dens_ref)
394                            .sao_spatial_4c(&sao_spatial_4c)
395                            .sao_spatial_4c_h(None)
396                            .symmetry_group(&pd_res)
397                            .build()
398                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
399                    py.detach(|| {
400                        da_driver
401                            .run()
402                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
403                    })?
404                }
405            };
406        }
407        (_, _) => {
408            // At least one of coefficients or sao_4c are not real.
409            let dens: Vec<Density<C128>> = pydens
410                .iter()
411                .map(|(_, pyden)| match pyden {
412                    PyDensity::Real(pyden_r) => {
413                        pyden_r.to_qsym2(&bao, mol).map(|den_r| den_r.into())
414                    }
415                    PyDensity::Complex(pyden_c) => pyden_c.to_qsym2(&bao, mol),
416                })
417                .collect::<Result<Vec<_>, _>>()
418                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
419            let dens_ref = dens
420                .iter()
421                .zip(pydens.iter())
422                .map(|(den, (desc, _))| (desc.clone(), den))
423                .collect::<Vec<_>>();
424
425            let (sao_spatial_4c_c, sao_spatial_4c_h_c): (Array4<C128>, Option<Array4<C128>>) =
426                match sao_spatial_4c {
427                    PyArray4RC::Real(pysao4c_r) => {
428                        (pysao4c_r.to_owned_array().mapv(Complex::from), None)
429                    }
430                    PyArray4RC::Complex(pysao4c_c) => (
431                        pysao4c_c.to_owned_array(),
432                        sao_spatial_4c_h.map(|v| match v {
433                            PyArray4RC::Real(pysao4c_h_r) => {
434                                pysao4c_h_r.to_owned_array().mapv(Complex::from)
435                            }
436                            PyArray4RC::Complex(pysao4c_h_c) => pysao4c_h_c.to_owned_array(),
437                        }),
438                    ),
439                };
440
441            match &use_magnetic_group {
442                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
443                    let mut da_driver = DensityRepAnalysisDriver::<
444                        MagneticRepresentedSymmetryGroup,
445                        C128,
446                    >::builder()
447                    .parameters(&sda_params)
448                    .angular_function_parameters(&afa_params)
449                    .densities(dens_ref)
450                    .sao_spatial_4c(&sao_spatial_4c_c)
451                    .sao_spatial_4c_h(sao_spatial_4c_h_c.as_ref())
452                    .symmetry_group(&pd_res)
453                    .build()
454                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
455                    py.detach(|| {
456                        da_driver
457                            .run()
458                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
459                    })?
460                }
461                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
462                    let mut da_driver =
463                        DensityRepAnalysisDriver::<UnitaryRepresentedSymmetryGroup, C128>::builder(
464                        )
465                        .parameters(&sda_params)
466                        .angular_function_parameters(&afa_params)
467                        .densities(dens_ref)
468                        .sao_spatial_4c(&sao_spatial_4c_c)
469                        .sao_spatial_4c_h(sao_spatial_4c_h_c.as_ref())
470                        .symmetry_group(&pd_res)
471                        .build()
472                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
473                    py.detach(|| {
474                        da_driver
475                            .run()
476                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
477                    })?
478                }
479            };
480        }
481    }
482    Ok(())
483}