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/// * `use_magnetic_group` - An option indicating if the magnetic group is to be used for symmetry
229/// analysis, and if so, whether unitary representations or unitary-antiunitary corepresentations
230/// should be used.
231/// * `use_double_group` - A boolean indicating if the double group of the prevailing symmetry
232/// group is to be used for representation analysis instead.
233/// * `use_cayley_table` - A boolean indicating if the Cayley table for the group, if available,
234/// should be used to speed up the calculation of orbit overlap matrices.
235/// * `symmetry_transformation_kind` - An enumerated type indicating the type of symmetry
236/// transformations to be performed on the origin electron density to generate the orbit.
237/// * `eigenvalue_comparison_mode` - An enumerated type indicating the mode of comparison of orbit
238/// overlap eigenvalues with the specified `linear_independence_threshold`.
239/// * `sao_spatial_4c` - The atomic-orbital four-centre overlap matrix whose elements are of type
240/// `float64` or `complex128`.
241/// * `sao_spatial_4c_h` - The optional complex-symmetric atomic-orbital four-centre overlap matrix
242/// whose elements are of type `float64` or `complex128`. This is required if antiunitary symmetry
243/// operations are involved.
244/// * `write_character_table` - A boolean indicating if the character table of the prevailing
245/// symmetry group is to be printed out.
246/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
247/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
248/// will be used for symmetry analysis.
249/// * `angular_function_integrality_threshold` - The threshold for verifying if subspace
250/// multiplicities are integral for the symmetry analysis of angular functions.
251/// * `angular_function_linear_independence_threshold` - The threshold for determining the linear
252/// independence subspace via the non-zero eigenvalues of the orbit overlap matrix for the symmetry
253/// analysis of angular functions.
254/// * `angular_function_max_angular_momentum` - The maximum angular momentum order to be used in
255/// angular function symmetry analysis.
256#[allow(clippy::too_many_arguments)]
257#[pyfunction]
258#[pyo3(signature = (
259    inp_sym,
260    pydens,
261    pybao,
262    integrality_threshold,
263    linear_independence_threshold,
264    use_magnetic_group,
265    use_double_group,
266    use_cayley_table,
267    symmetry_transformation_kind,
268    eigenvalue_comparison_mode,
269    sao_spatial_4c,
270    sao_spatial_4c_h=None,
271    write_character_table=true,
272    infinite_order_to_finite=None,
273    angular_function_integrality_threshold=1e-7,
274    angular_function_linear_independence_threshold=1e-7,
275    angular_function_max_angular_momentum=2
276))]
277pub fn rep_analyse_densities(
278    py: Python<'_>,
279    inp_sym: PathBuf,
280    pydens: Vec<(String, PyDensity)>,
281    pybao: &PyBasisAngularOrder,
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    sao_spatial_4c: PyArray4RC,
290    sao_spatial_4c_h: Option<PyArray4RC>,
291    write_character_table: bool,
292    infinite_order_to_finite: Option<u32>,
293    angular_function_integrality_threshold: f64,
294    angular_function_linear_independence_threshold: f64,
295    angular_function_max_angular_momentum: u32,
296) -> PyResult<()> {
297    let pd_res: SymmetryGroupDetectionResult =
298        read_qsym2_binary(inp_sym.clone(), QSym2FileType::Sym)
299            .map_err(|err| PyIOError::new_err(err.to_string()))?;
300
301    let mut file_name = inp_sym.to_path_buf();
302    file_name.set_extension(QSym2FileType::Sym.ext());
303    qsym2_output!(
304        "Symmetry-group detection results read in from {}.",
305        file_name.display(),
306    );
307    qsym2_output!("");
308
309    let mol = &pd_res.pre_symmetry.recentred_molecule;
310    let bao = pybao
311        .to_qsym2(mol)
312        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
313    let afa_params = AngularFunctionRepAnalysisParams::builder()
314        .integrality_threshold(angular_function_integrality_threshold)
315        .linear_independence_threshold(angular_function_linear_independence_threshold)
316        .max_angular_momentum(angular_function_max_angular_momentum)
317        .build()
318        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
319    let sda_params = DensityRepAnalysisParams::<f64>::builder()
320        .integrality_threshold(integrality_threshold)
321        .linear_independence_threshold(linear_independence_threshold)
322        .use_magnetic_group(use_magnetic_group.clone())
323        .use_double_group(use_double_group)
324        .use_cayley_table(use_cayley_table)
325        .symmetry_transformation_kind(symmetry_transformation_kind)
326        .eigenvalue_comparison_mode(eigenvalue_comparison_mode)
327        .write_character_table(if write_character_table {
328            Some(CharacterTableDisplay::Symbolic)
329        } else {
330            None
331        })
332        .infinite_order_to_finite(infinite_order_to_finite)
333        .build()
334        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
335
336    let any_complex = pydens
337        .iter()
338        .any(|(_, pyden)| matches!(pyden, PyDensity::Complex(_)));
339
340    match (any_complex, &sao_spatial_4c) {
341        (false, PyArray4RC::Real(pysao4c_r)) => {
342            // Both coefficients and sao_4c are real.
343            let dens = pydens
344                .iter()
345                .map(|(_, pyden)| match pyden {
346                    PyDensity::Real(pyden_r) => pyden_r.to_qsym2(&bao, mol),
347                    PyDensity::Complex(_) => panic!("Unexpected complex density."),
348                })
349                .collect::<Result<Vec<_>, _>>()
350                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
351            let dens_ref = dens
352                .iter()
353                .zip(pydens.iter())
354                .map(|(den, (desc, _))| (desc.clone(), den))
355                .collect::<Vec<_>>();
356
357            let sao_spatial_4c = pysao4c_r.to_owned_array();
358
359            match &use_magnetic_group {
360                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
361                    let mut da_driver =
362                        DensityRepAnalysisDriver::<MagneticRepresentedSymmetryGroup, f64>::builder(
363                        )
364                        .parameters(&sda_params)
365                        .angular_function_parameters(&afa_params)
366                        .densities(dens_ref)
367                        .sao_spatial_4c(&sao_spatial_4c)
368                        .sao_spatial_4c_h(None)
369                        .symmetry_group(&pd_res)
370                        .build()
371                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
372                    py.detach(|| {
373                        da_driver
374                            .run()
375                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
376                    })?
377                }
378                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
379                    let mut da_driver =
380                        DensityRepAnalysisDriver::<UnitaryRepresentedSymmetryGroup, f64>::builder()
381                            .parameters(&sda_params)
382                            .angular_function_parameters(&afa_params)
383                            .densities(dens_ref)
384                            .sao_spatial_4c(&sao_spatial_4c)
385                            .sao_spatial_4c_h(None)
386                            .symmetry_group(&pd_res)
387                            .build()
388                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
389                    py.detach(|| {
390                        da_driver
391                            .run()
392                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
393                    })?
394                }
395            };
396        }
397        (_, _) => {
398            // At least one of coefficients or sao_4c are not real.
399            let dens: Vec<Density<C128>> = pydens
400                .iter()
401                .map(|(_, pyden)| match pyden {
402                    PyDensity::Real(pyden_r) => {
403                        pyden_r.to_qsym2(&bao, mol).map(|den_r| den_r.into())
404                    }
405                    PyDensity::Complex(pyden_c) => pyden_c.to_qsym2(&bao, mol),
406                })
407                .collect::<Result<Vec<_>, _>>()
408                .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
409            let dens_ref = dens
410                .iter()
411                .zip(pydens.iter())
412                .map(|(den, (desc, _))| (desc.clone(), den))
413                .collect::<Vec<_>>();
414
415            let (sao_spatial_4c_c, sao_spatial_4c_h_c): (Array4<C128>, Option<Array4<C128>>) =
416                match sao_spatial_4c {
417                    PyArray4RC::Real(pysao4c_r) => {
418                        (pysao4c_r.to_owned_array().mapv(Complex::from), None)
419                    }
420                    PyArray4RC::Complex(pysao4c_c) => (
421                        pysao4c_c.to_owned_array(),
422                        sao_spatial_4c_h.map(|v| match v {
423                            PyArray4RC::Real(pysao4c_h_r) => {
424                                pysao4c_h_r.to_owned_array().mapv(Complex::from)
425                            }
426                            PyArray4RC::Complex(pysao4c_h_c) => pysao4c_h_c.to_owned_array(),
427                        }),
428                    ),
429                };
430
431            match &use_magnetic_group {
432                Some(MagneticSymmetryAnalysisKind::Corepresentation) => {
433                    let mut da_driver = DensityRepAnalysisDriver::<
434                        MagneticRepresentedSymmetryGroup,
435                        C128,
436                    >::builder()
437                    .parameters(&sda_params)
438                    .angular_function_parameters(&afa_params)
439                    .densities(dens_ref)
440                    .sao_spatial_4c(&sao_spatial_4c_c)
441                    .sao_spatial_4c_h(sao_spatial_4c_h_c.as_ref())
442                    .symmetry_group(&pd_res)
443                    .build()
444                    .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
445                    py.detach(|| {
446                        da_driver
447                            .run()
448                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
449                    })?
450                }
451                Some(MagneticSymmetryAnalysisKind::Representation) | None => {
452                    let mut da_driver =
453                        DensityRepAnalysisDriver::<UnitaryRepresentedSymmetryGroup, C128>::builder(
454                        )
455                        .parameters(&sda_params)
456                        .angular_function_parameters(&afa_params)
457                        .densities(dens_ref)
458                        .sao_spatial_4c(&sao_spatial_4c_c)
459                        .sao_spatial_4c_h(sao_spatial_4c_h_c.as_ref())
460                        .symmetry_group(&pd_res)
461                        .build()
462                        .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
463                    py.detach(|| {
464                        da_driver
465                            .run()
466                            .map_err(|err| PyRuntimeError::new_err(err.to_string()))
467                    })?
468                }
469            };
470        }
471    }
472    Ok(())
473}