qsym2/bindings/python/
molecule_symmetrisation.rs

1//! Python bindings for QSym² molecule symmetrisation by bootstrapping.
2//!
3//! See [`crate::drivers::molecule_symmetrisation_bootstrap`] for more information.
4
5use std::path::PathBuf;
6
7use anyhow::format_err;
8use pyo3::exceptions::PyRuntimeError;
9use pyo3::prelude::*;
10
11use crate::auxiliary::atom::AtomKind;
12use crate::auxiliary::molecule::Molecule;
13use crate::bindings::python::symmetry_group_detection::PyMolecule;
14use crate::drivers::QSym2Driver;
15use crate::drivers::molecule_symmetrisation_bootstrap::{
16    MoleculeSymmetrisationBootstrapDriver, MoleculeSymmetrisationBootstrapParams,
17};
18use crate::io::QSym2FileType;
19
20/// Python-exposed function to perform molecule symmetrisation by bootstrapping and log the result
21/// via the `qsym2-output` logger at the `INFO` level.
22///
23/// See [`crate::drivers::molecule_symmetrisation_bootstrap`] for more information.
24///
25/// # Arguments
26///
27/// * `inp_xyz` - An optional string providing the path to an XYZ file containing the molecule to
28/// be symmetrised. Only one of `inp_xyz` or `inp_mol` can be specified.
29/// * `inp_mol` - An optional `PyMolecule` structure containing the molecule to be symmetrised. Only
30/// one of `inp_xyz` or `inp_mol` can be specified.
31/// * `out_target_sym` - An optional path for a [`QSym2FileType::Sym`] file to be saved that
32/// contains the symmetry-group detection results of the symmetrised molecule at the target
33/// thresholds.
34/// * `loose_moi_threshold` - The loose MoI threshold.
35/// * `loose_distance_threshold` - The loose distance threshold.
36/// * `target_moi_threshold` - The target (tight) MoI threshold.
37/// * `target_distance_threshold` - The target (tight) distance threshold.
38/// * `use_magnetic_group` - A boolean indicating if the magnetic group (*i.e.* the group including
39/// time-reversed operations) is to be used for the symmetrisation.
40/// * `reorientate_molecule` - A boolean indicating if the molecule is also reoriented to align its
41/// principal axes with the Cartesian axes.
42/// * `max_iterations` - The maximum number of iterations for the symmetrisation process.
43/// * `consistent_target_symmetry_iterations` - The number of consecutive iterations during which
44/// the symmetry group at the target level of threshold must be consistently found for convergence
45/// to be reached, if this group cannot become identical to the symmetry group at the loose level
46/// of threshold.
47/// * `verbose` - The print-out level.
48/// * `infinite_order_to_finite` - The finite order with which infinite-order generators are to be
49/// interpreted to form a finite subgroup of the prevailing infinite group. This finite subgroup
50/// will be used for the symmetrisation.
51///
52/// # Returns
53///
54/// The symmetrised molecule.
55///
56/// # Errors
57///
58/// Errors if any intermediate step in the symmetrisation procedure fails.
59#[pyfunction]
60#[pyo3(signature = (
61    inp_xyz,
62    inp_mol,
63    out_target_sym=None,
64    loose_moi_threshold=1e-2,
65    loose_distance_threshold=1e-2,
66    target_moi_threshold=1e-7,
67    target_distance_threshold=1e-7,
68    use_magnetic_group=false,
69    reorientate_molecule=true,
70    max_iterations=50,
71    consistent_target_symmetry_iterations=10,
72    verbose=0,
73    infinite_order_to_finite=None
74))]
75pub fn symmetrise_molecule(
76    py: Python<'_>,
77    inp_xyz: Option<PathBuf>,
78    inp_mol: Option<PyMolecule>,
79    out_target_sym: Option<PathBuf>,
80    loose_moi_threshold: f64,
81    loose_distance_threshold: f64,
82    target_moi_threshold: f64,
83    target_distance_threshold: f64,
84    use_magnetic_group: bool,
85    reorientate_molecule: bool,
86    max_iterations: usize,
87    consistent_target_symmetry_iterations: usize,
88    verbose: u8,
89    infinite_order_to_finite: Option<u32>,
90) -> PyResult<PyMolecule> {
91    py.detach(|| {
92        let mol = match (inp_xyz, inp_mol) {
93            (Some(xyz_path), None) => Molecule::from_xyz(xyz_path, 1e-7),
94            (None, Some(pymol)) => Molecule::from(pymol),
95            _ => {
96                return Err(PyRuntimeError::new_err(
97                    "One and only one of `inp_xyz` or `inp_mol` must be specified.",
98                ));
99            }
100        };
101
102        let msb_params = MoleculeSymmetrisationBootstrapParams::builder()
103            .reorientate_molecule(reorientate_molecule)
104            .use_magnetic_group(use_magnetic_group)
105            .loose_moi_threshold(loose_moi_threshold)
106            .loose_distance_threshold(loose_distance_threshold)
107            .target_moi_threshold(target_moi_threshold)
108            .target_distance_threshold(target_distance_threshold)
109            .infinite_order_to_finite(infinite_order_to_finite)
110            .max_iterations(max_iterations)
111            .consistent_target_symmetry_iterations(consistent_target_symmetry_iterations)
112            .verbose(verbose)
113            .symmetrised_result_save_name(out_target_sym)
114            .build()
115            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
116
117        let mut msb_driver = MoleculeSymmetrisationBootstrapDriver::builder()
118            .parameters(&msb_params)
119            .molecule(&mol)
120            .build()
121            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
122        msb_driver
123            .run()
124            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
125
126        let symmol = &msb_driver
127            .result()
128            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
129            .symmetrised_molecule;
130
131        // Note that the magnetic field here will not have the original magnitude as it is back-deduded
132        // from the magnetic atoms which have been added using the normalised version of the original
133        // magnetic field.
134        let magnetic_field = symmol
135            .magnetic_atoms
136            .as_ref()
137            .map(|mag_atoms| {
138                if mag_atoms.len() != 2 {
139                    Err(format_err!("Only a uniform magnetic field is supported."))
140                } else {
141                    match (&mag_atoms[0].kind, &mag_atoms[1].kind) {
142                        (AtomKind::Magnetic(true), AtomKind::Magnetic(false)) => {
143                            let bvec = mag_atoms[0].coordinates - mag_atoms[1].coordinates;
144                            Ok([bvec[0], bvec[1], bvec[2]])
145                        }
146                        (AtomKind::Magnetic(false), AtomKind::Magnetic(true)) => {
147                            let bvec = mag_atoms[1].coordinates - mag_atoms[0].coordinates;
148                            Ok([bvec[0], bvec[1], bvec[2]])
149                        }
150                        _ => Err(format_err!("Invalid fictitious magnetic atoms detected.")),
151                    }
152                }
153            })
154            .transpose()
155            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
156
157        // Note that the electric field here will not have the original magnitude as it is back-deduded
158        // from the electric atoms which have been added using the normalised version of the original
159        // electric field.
160        let electric_field = symmol
161            .electric_atoms
162            .as_ref()
163            .map(|elec_atoms| {
164                if elec_atoms.len() != 1 {
165                    Err(format_err!("Only a uniform electric field is supported."))
166                } else {
167                    match &elec_atoms[0].kind {
168                        AtomKind::Electric(pos) => {
169                            let evec = if *pos {
170                                elec_atoms[0].coordinates
171                            } else {
172                                -elec_atoms[0].coordinates
173                            };
174                            Ok([evec[0], evec[1], evec[2]])
175                        }
176                        _ => Err(format_err!("Invalid fictitious electric atoms detected.")),
177                    }
178                }
179            })
180            .transpose()
181            .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
182
183        let pymol = PyMolecule::new(
184            symmol
185                .atoms
186                .iter()
187                .map(|atom| {
188                    (
189                        atom.atomic_symbol.clone(),
190                        [
191                            atom.coordinates[0],
192                            atom.coordinates[1],
193                            atom.coordinates[2],
194                        ],
195                    )
196                })
197                .collect::<Vec<_>>(),
198            symmol.threshold,
199            magnetic_field,
200            electric_field,
201        );
202        Ok(pymol)
203    })
204}