1use std::collections::HashMap;
6use std::fmt;
7use std::path::PathBuf;
8
9use anyhow::{self, format_err};
10use nalgebra::{Point3, Vector3};
11use numpy::{PyArray1, ToPyArray};
12use pyo3::exceptions::PyRuntimeError;
13use pyo3::prelude::*;
14
15use crate::auxiliary::atom::{Atom, ElementMap};
16use crate::auxiliary::molecule::Molecule;
17use crate::drivers::QSym2Driver;
18use crate::drivers::symmetry_group_detection::{
19 SymmetryGroupDetectionDriver, SymmetryGroupDetectionParams,
20};
21#[allow(unused_imports)]
22use crate::io::QSym2FileType;
23use crate::symmetry::symmetry_core::Symmetry;
24use crate::symmetry::symmetry_element::{AntiunitaryKind, SymmetryElementKind};
25use crate::symmetry::symmetry_element_order::ElementOrder;
26
27#[pyclass]
37#[derive(Clone)]
38pub struct PyMolecule {
39 #[pyo3(get)]
43 pub atoms: Vec<(String, [f64; 3])>,
44
45 #[pyo3(get)]
49 pub magnetic_field: Option<[f64; 3]>,
50
51 #[pyo3(get)]
55 pub electric_field: Option<[f64; 3]>,
56
57 #[pyo3(get)]
61 pub threshold: f64,
62}
63
64#[pymethods]
65impl PyMolecule {
66 #[new]
75 #[pyo3(signature = (atoms, threshold, magnetic_field=None, electric_field=None))]
76 pub fn new(
77 atoms: Vec<(String, [f64; 3])>,
78 threshold: f64,
79 magnetic_field: Option<[f64; 3]>,
80 electric_field: Option<[f64; 3]>,
81 ) -> Self {
82 Self {
83 atoms,
84 threshold,
85 magnetic_field,
86 electric_field,
87 }
88 }
89}
90
91impl From<PyMolecule> for Molecule {
92 fn from(pymol: PyMolecule) -> Self {
93 let emap = ElementMap::new();
94 let mut mol = Self::from_atoms(
95 &pymol
96 .atoms
97 .iter()
98 .map(|(ele, r)| {
99 Atom::new_ordinary(ele, Point3::new(r[0], r[1], r[2]), &emap, pymol.threshold)
100 })
101 .collect::<Vec<_>>(),
102 pymol.threshold,
103 );
104 mol.set_magnetic_field(pymol.magnetic_field.map(Vector3::from_iterator));
105 mol.set_electric_field(pymol.electric_field.map(Vector3::from_iterator));
106 mol
107 }
108}
109
110#[pyclass(eq, eq_int)]
117#[derive(Clone, Hash, PartialEq, Eq)]
118pub enum PySymmetryElementKind {
119 Proper,
121
122 ProperTR,
124
125 ImproperMirrorPlane,
127
128 ImproperMirrorPlaneTR,
130}
131
132impl fmt::Display for PySymmetryElementKind {
133 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
134 match self {
135 PySymmetryElementKind::Proper => write!(f, "Proper"),
136 PySymmetryElementKind::ProperTR => write!(f, "Time-reversed proper"),
137 PySymmetryElementKind::ImproperMirrorPlane => {
138 write!(f, "Improper (mirror-plane convention)")
139 }
140 PySymmetryElementKind::ImproperMirrorPlaneTR => {
141 write!(f, "Time-reversed improper (mirror-plane convention)")
142 }
143 }
144 }
145}
146
147impl TryFrom<&SymmetryElementKind> for PySymmetryElementKind {
148 type Error = anyhow::Error;
149
150 fn try_from(symkind: &SymmetryElementKind) -> Result<Self, Self::Error> {
151 match symkind {
152 SymmetryElementKind::Proper(None) => Ok(Self::Proper),
153 SymmetryElementKind::Proper(Some(AntiunitaryKind::TimeReversal)) => Ok(Self::ProperTR),
154 SymmetryElementKind::ImproperMirrorPlane(None) => Ok(Self::ImproperMirrorPlane),
155 SymmetryElementKind::ImproperMirrorPlane(Some(AntiunitaryKind::TimeReversal)) => {
156 Ok(Self::ImproperMirrorPlaneTR)
157 }
158 _ => Err(format_err!(
159 "Symmetry element kind `{symkind}` is not yet supported in Python."
160 )),
161 }
162 }
163}
164
165#[pyclass]
171#[derive(Clone)]
172pub struct PySymmetry {
173 #[pyo3(get)]
175 group_name: String,
176
177 #[allow(clippy::type_complexity)]
181 elements: HashMap<PySymmetryElementKind, HashMap<i32, Vec<Py<PyArray1<f64>>>>>,
182
183 #[allow(clippy::type_complexity)]
187 generators: HashMap<PySymmetryElementKind, HashMap<i32, Vec<Py<PyArray1<f64>>>>>,
188}
189
190#[pymethods]
191impl PySymmetry {
192 pub fn is_infinite(&self) -> bool {
194 self.elements
195 .values()
196 .any(|kind_elements| kind_elements.contains_key(&-1))
197 || self
198 .generators
199 .values()
200 .any(|kind_generators| kind_generators.contains_key(&-1))
201 }
202
203 pub fn get_elements_of_kind(
215 &self,
216 py: Python<'_>,
217 kind: &PySymmetryElementKind,
218 ) -> PyResult<HashMap<i32, Vec<Py<PyArray1<f64>>>>> {
219 self.elements
220 .get(kind)
221 .map(|elements| {
222 elements
223 .iter()
224 .map(|(order, axes)| {
225 (*order, axes.iter().map(|axis| axis.clone_ref(py)).collect())
226 })
227 .collect()
228 })
229 .ok_or_else(|| PyRuntimeError::new_err(format!("Elements of kind `{kind}` not found.")))
230 }
231
232 pub fn get_generators_of_kind(
244 &self,
245 py: Python<'_>,
246 kind: &PySymmetryElementKind,
247 ) -> PyResult<HashMap<i32, Vec<Py<PyArray1<f64>>>>> {
248 self.generators
249 .get(kind)
250 .map(|generators| {
251 generators
252 .iter()
253 .map(|(order, axes)| {
254 (*order, axes.iter().map(|axis| axis.clone_ref(py)).collect())
255 })
256 .collect()
257 })
258 .ok_or_else(|| PyRuntimeError::new_err(format!("Elements of kind `{kind}` not found.")))
259 }
260}
261
262impl TryFrom<&Symmetry> for PySymmetry {
263 type Error = anyhow::Error;
264
265 fn try_from(sym: &Symmetry) -> Result<Self, Self::Error> {
266 let group_name = sym
267 .group_name
268 .clone()
269 .ok_or(format_err!("Symmetry group name not found."))?;
270 let elements = Python::attach(|py| {
271 sym.elements
272 .iter()
273 .map(|(symkind, kind_elements)| {
274 let pysymkind = PySymmetryElementKind::try_from(symkind)?;
275 let pykind_elements = kind_elements
276 .iter()
277 .map(|(order, order_elements)| {
278 let order_i32 = match order {
279 ElementOrder::Int(ord) => i32::try_from(*ord)?,
280 ElementOrder::Inf => -1,
281 };
282 let pyorder_elements = order_elements
283 .iter()
284 .map(|ele| {
285 ele.raw_axis()
286 .iter()
287 .cloned()
288 .collect::<Vec<_>>()
289 .to_pyarray(py)
290 .unbind()
291 })
292 .collect::<Vec<_>>();
293 Ok::<_, Self::Error>((order_i32, pyorder_elements))
294 })
295 .collect::<Result<HashMap<i32, Vec<_>>, _>>()?;
296 Ok::<_, Self::Error>((pysymkind, pykind_elements))
297 })
298 .collect::<Result<HashMap<_, _>, _>>()
299 })?;
300
301 let generators = Python::attach(|py| {
302 sym.generators
303 .iter()
304 .map(|(symkind, kind_generators)| {
305 let pysymkind = PySymmetryElementKind::try_from(symkind)?;
306 let pykind_generators = kind_generators
307 .iter()
308 .map(|(order, order_generators)| {
309 let order_i32 = match order {
310 ElementOrder::Int(ord) => i32::try_from(*ord)?,
311 ElementOrder::Inf => -1,
312 };
313 let pyorder_generators = order_generators
314 .iter()
315 .map(|ele| {
316 ele.raw_axis()
317 .iter()
318 .cloned()
319 .collect::<Vec<_>>()
320 .to_pyarray(py)
321 .unbind()
322 })
323 .collect::<Vec<_>>();
324 Ok::<_, Self::Error>((order_i32, pyorder_generators))
325 })
326 .collect::<Result<HashMap<i32, Vec<_>>, _>>()?;
327 Ok::<_, Self::Error>((pysymkind, pykind_generators))
328 })
329 .collect::<Result<HashMap<_, _>, _>>()
330 })?;
331
332 Ok(PySymmetry {
333 group_name,
334 elements,
335 generators,
336 })
337 }
338}
339
340#[allow(clippy::too_many_arguments)]
375#[pyfunction]
376#[pyo3(signature = (
377 inp_xyz,
378 inp_mol,
379 out_sym,
380 moi_thresholds,
381 distance_thresholds,
382 time_reversal,
383 write_symmetry_elements=true,
384 fictitious_magnetic_field=None,
385 fictitious_electric_field=None,
386))]
387pub fn detect_symmetry_group(
388 py: Python<'_>,
389 inp_xyz: Option<PathBuf>,
390 inp_mol: Option<PyMolecule>,
391 out_sym: Option<PathBuf>,
392 moi_thresholds: Vec<f64>,
393 distance_thresholds: Vec<f64>,
394 time_reversal: bool,
395 write_symmetry_elements: bool,
396 fictitious_magnetic_field: Option<[f64; 3]>,
397 fictitious_electric_field: Option<[f64; 3]>,
398) -> PyResult<(PySymmetry, Option<PySymmetry>)> {
399 let (unitary_symmetry, magnetic_symmetry_opt) = py.detach(|| {
400 let params = SymmetryGroupDetectionParams::builder()
401 .distance_thresholds(&distance_thresholds)
402 .moi_thresholds(&moi_thresholds)
403 .time_reversal(time_reversal)
404 .fictitious_magnetic_fields(
405 fictitious_magnetic_field
406 .map(|bs| vec![(Point3::<f64>::origin(), Vector3::new(bs[0], bs[1], bs[2]))]),
407 )
408 .fictitious_electric_fields(
409 fictitious_electric_field
410 .map(|es| vec![(Point3::<f64>::origin(), Vector3::new(es[0], es[1], es[2]))]),
411 )
412 .field_origin_com(true)
413 .write_symmetry_elements(write_symmetry_elements)
414 .result_save_name(out_sym)
415 .build()
416 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
417 let inp_mol = inp_mol.map(Molecule::from);
418 let mut pd_driver = SymmetryGroupDetectionDriver::builder()
419 .parameters(¶ms)
420 .xyz(inp_xyz)
421 .molecule(inp_mol.as_ref())
422 .build()
423 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
424 pd_driver
425 .run()
426 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
427 let result = pd_driver
428 .result()
429 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
430 Ok::<_, PyErr>((
431 result.unitary_symmetry.clone(),
432 result.magnetic_symmetry.clone(),
433 ))
434 })?;
435 let pyunitary_symmetry: PySymmetry = (&unitary_symmetry)
436 .try_into()
437 .map_err(|err: anyhow::Error| PyRuntimeError::new_err(err.to_string()))?;
438 let pymagnetic_symmetry: Option<PySymmetry> = magnetic_symmetry_opt
439 .map(|ref magsym| {
440 magsym
441 .try_into()
442 .map_err(|err: anyhow::Error| PyRuntimeError::new_err(err.to_string()))
443 })
444 .transpose()?;
445 Ok((pyunitary_symmetry, pymagnetic_symmetry))
446}