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}