1use std::collections::HashMap;
6use std::fmt;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use anyhow::{self, format_err};
11use derive_builder::Builder;
12use nalgebra::{Point3, Vector3};
13use numpy::{PyArray1, ToPyArray};
14use pyo3::exceptions::PyRuntimeError;
15use pyo3::prelude::*;
16
17use crate::auxiliary::atom::{Atom, ElementMap};
18use crate::auxiliary::molecule::Molecule;
19use crate::drivers::QSym2Driver;
20use crate::drivers::symmetry_group_detection::{
21 SymmetryGroupDetectionDriver, SymmetryGroupDetectionParams,
22};
23#[allow(unused_imports)]
24use crate::io::QSym2FileType;
25use crate::symmetry::symmetry_core::Symmetry;
26use crate::symmetry::symmetry_element::{AntiunitaryKind, SymmetryElementKind};
27use crate::symmetry::symmetry_element_order::ElementOrder;
28
29#[pyclass]
39#[derive(Clone)]
40pub struct PyMolecule {
41 #[pyo3(get)]
45 pub atoms: Vec<(String, [f64; 3])>,
46
47 #[pyo3(get)]
51 pub magnetic_field: Option<[f64; 3]>,
52
53 #[pyo3(get)]
57 pub electric_field: Option<[f64; 3]>,
58
59 #[pyo3(get)]
63 pub threshold: f64,
64}
65
66#[pymethods]
67impl PyMolecule {
68 #[new]
77 #[pyo3(signature = (atoms, threshold, magnetic_field=None, electric_field=None))]
78 pub fn new(
79 atoms: Vec<(String, [f64; 3])>,
80 threshold: f64,
81 magnetic_field: Option<[f64; 3]>,
82 electric_field: Option<[f64; 3]>,
83 ) -> Self {
84 Self {
85 atoms,
86 threshold,
87 magnetic_field,
88 electric_field,
89 }
90 }
91}
92
93impl From<PyMolecule> for Molecule {
94 fn from(pymol: PyMolecule) -> Self {
95 let emap = ElementMap::new();
96 let mut mol = Self::from_atoms(
97 &pymol
98 .atoms
99 .iter()
100 .map(|(ele, r)| {
101 Atom::new_ordinary(ele, Point3::new(r[0], r[1], r[2]), &emap, pymol.threshold)
102 })
103 .collect::<Vec<_>>(),
104 pymol.threshold,
105 );
106 mol.set_magnetic_field(pymol.magnetic_field.map(Vector3::from_iterator));
107 mol.set_electric_field(pymol.electric_field.map(Vector3::from_iterator));
108 mol
109 }
110}
111
112#[pyclass(eq, eq_int)]
119#[derive(Clone, Hash, PartialEq, Eq)]
120pub enum PySymmetryElementKind {
121 Proper,
123
124 ProperTR,
126
127 ImproperMirrorPlane,
129
130 ImproperMirrorPlaneTR,
132}
133
134impl fmt::Display for PySymmetryElementKind {
135 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136 match self {
137 PySymmetryElementKind::Proper => write!(f, "Proper"),
138 PySymmetryElementKind::ProperTR => write!(f, "Time-reversed proper"),
139 PySymmetryElementKind::ImproperMirrorPlane => {
140 write!(f, "Improper (mirror-plane convention)")
141 }
142 PySymmetryElementKind::ImproperMirrorPlaneTR => {
143 write!(f, "Time-reversed improper (mirror-plane convention)")
144 }
145 }
146 }
147}
148
149impl TryFrom<&SymmetryElementKind> for PySymmetryElementKind {
150 type Error = anyhow::Error;
151
152 fn try_from(symkind: &SymmetryElementKind) -> Result<Self, Self::Error> {
153 match symkind {
154 SymmetryElementKind::Proper(None) => Ok(Self::Proper),
155 SymmetryElementKind::Proper(Some(AntiunitaryKind::TimeReversal)) => Ok(Self::ProperTR),
156 SymmetryElementKind::ImproperMirrorPlane(None) => Ok(Self::ImproperMirrorPlane),
157 SymmetryElementKind::ImproperMirrorPlane(Some(AntiunitaryKind::TimeReversal)) => {
158 Ok(Self::ImproperMirrorPlaneTR)
159 }
160 _ => Err(format_err!(
161 "Symmetry element kind `{symkind}` is not yet supported in Python."
162 )),
163 }
164 }
165}
166
167#[pyclass]
173#[derive(Clone, Builder)]
174pub struct PySymmetry {
175 #[pyo3(get)]
177 group_name: String,
178
179 #[allow(clippy::type_complexity)]
183 elements: HashMap<PySymmetryElementKind, HashMap<i32, Vec<Arc<Py<PyArray1<f64>>>>>>,
184
185 #[allow(clippy::type_complexity)]
189 generators: HashMap<PySymmetryElementKind, HashMap<i32, Vec<Arc<Py<PyArray1<f64>>>>>>,
190}
191
192impl PySymmetry {
193 fn builder() -> PySymmetryBuilder {
194 PySymmetryBuilder::default()
195 }
196}
197
198#[pymethods]
199impl PySymmetry {
200 pub fn is_infinite(&self) -> bool {
202 self.elements
203 .values()
204 .any(|kind_elements| kind_elements.contains_key(&-1))
205 || self
206 .generators
207 .values()
208 .any(|kind_generators| kind_generators.contains_key(&-1))
209 }
210
211 pub fn get_elements_of_kind(
223 &self,
224 kind: &PySymmetryElementKind,
225 ) -> PyResult<HashMap<i32, Vec<Py<PyArray1<f64>>>>> {
226 self.elements
227 .get(kind)
228 .cloned()
229 .and_then(|elements| {
230 elements
231 .iter()
232 .map(|(order, axes_arc)| {
233 let axes_opt = axes_arc
234 .iter()
235 .map(|axis_arc| Arc::into_inner(axis_arc.clone()))
236 .collect::<Option<Vec<_>>>();
237 axes_opt.map(|axes| (*order, axes))
238 })
239 .collect::<Option<HashMap<_, _>>>()
240 })
241 .ok_or(PyRuntimeError::new_err(format!(
242 "Elements of kind `{kind}` not found."
243 )))
244 }
245
246 pub fn get_generators_of_kind(
258 &self,
259 kind: &PySymmetryElementKind,
260 ) -> PyResult<HashMap<i32, Vec<Py<PyArray1<f64>>>>> {
261 self.generators
262 .get(kind)
263 .cloned()
264 .and_then(|generators| {
265 generators
266 .iter()
267 .map(|(order, axes_arc)| {
268 let axes_opt = axes_arc
269 .iter()
270 .map(|axis_arc| Arc::into_inner(axis_arc.clone()))
271 .collect::<Option<Vec<_>>>();
272 axes_opt.map(|axes| (*order, axes))
273 })
274 .collect::<Option<HashMap<_, _>>>()
275 })
276 .ok_or(PyRuntimeError::new_err(format!(
277 "Elements of kind `{kind}` not found."
278 )))
279 }
280}
281
282impl TryFrom<&Symmetry> for PySymmetry {
283 type Error = anyhow::Error;
284
285 fn try_from(sym: &Symmetry) -> Result<Self, Self::Error> {
286 let group_name = sym
287 .group_name
288 .clone()
289 .ok_or(format_err!("Symmetry group name not found."))?;
290 let elements = sym
291 .elements
292 .iter()
293 .map(|(symkind, kind_elements)| {
294 let pysymkind = PySymmetryElementKind::try_from(symkind)?;
295 let pykind_elements = kind_elements
296 .iter()
297 .map(|(order, order_elements)| {
298 let order_i32 = match order {
299 ElementOrder::Int(ord) => i32::try_from(*ord)?,
300 ElementOrder::Inf => -1,
301 };
302 let pyorder_elements = order_elements
303 .iter()
304 .map(|ele| {
305 Arc::new(Python::attach(|py| {
306 ele.raw_axis()
307 .iter()
308 .cloned()
309 .collect::<Vec<_>>()
310 .to_pyarray(py)
311 .unbind()
312 }))
313 })
314 .collect::<Vec<_>>();
315 Ok::<_, Self::Error>((order_i32, pyorder_elements))
316 })
317 .collect::<Result<HashMap<i32, Vec<_>>, _>>()?;
318 Ok::<_, Self::Error>((pysymkind, pykind_elements))
319 })
320 .collect::<Result<HashMap<_, _>, _>>()?;
321
322 let generators = sym
323 .generators
324 .iter()
325 .map(|(symkind, kind_generators)| {
326 let pysymkind = PySymmetryElementKind::try_from(symkind)?;
327 let pykind_generators = kind_generators
328 .iter()
329 .map(|(order, order_generators)| {
330 let order_i32 = match order {
331 ElementOrder::Int(ord) => i32::try_from(*ord)?,
332 ElementOrder::Inf => -1,
333 };
334 let pyorder_generators = order_generators
335 .iter()
336 .map(|ele| {
337 Arc::new(Python::attach(|py| {
338 ele.raw_axis()
339 .iter()
340 .cloned()
341 .collect::<Vec<_>>()
342 .to_pyarray(py)
343 .unbind()
344 }))
345 })
346 .collect::<Vec<_>>();
347 Ok::<_, Self::Error>((order_i32, pyorder_generators))
348 })
349 .collect::<Result<HashMap<i32, Vec<_>>, _>>()?;
350 Ok::<_, Self::Error>((pysymkind, pykind_generators))
351 })
352 .collect::<Result<HashMap<_, _>, _>>()?;
353
354 PySymmetry::builder()
355 .group_name(group_name)
356 .elements(elements)
357 .generators(generators)
358 .build()
359 .map_err(|err| format_err!(err))
360 }
361}
362
363#[allow(clippy::too_many_arguments)]
398#[pyfunction]
399#[pyo3(signature = (
400 inp_xyz,
401 inp_mol,
402 out_sym,
403 moi_thresholds,
404 distance_thresholds,
405 time_reversal,
406 write_symmetry_elements=true,
407 fictitious_magnetic_field=None,
408 fictitious_electric_field=None,
409))]
410pub fn detect_symmetry_group(
411 py: Python<'_>,
412 inp_xyz: Option<PathBuf>,
413 inp_mol: Option<PyMolecule>,
414 out_sym: Option<PathBuf>,
415 moi_thresholds: Vec<f64>,
416 distance_thresholds: Vec<f64>,
417 time_reversal: bool,
418 write_symmetry_elements: bool,
419 fictitious_magnetic_field: Option<[f64; 3]>,
420 fictitious_electric_field: Option<[f64; 3]>,
421) -> PyResult<(PySymmetry, Option<PySymmetry>)> {
422 py.detach(|| {
423 let params = SymmetryGroupDetectionParams::builder()
424 .distance_thresholds(&distance_thresholds)
425 .moi_thresholds(&moi_thresholds)
426 .time_reversal(time_reversal)
427 .fictitious_magnetic_fields(
428 fictitious_magnetic_field
429 .map(|bs| vec![(Point3::<f64>::origin(), Vector3::new(bs[0], bs[1], bs[2]))]),
430 )
431 .fictitious_electric_fields(
432 fictitious_electric_field
433 .map(|es| vec![(Point3::<f64>::origin(), Vector3::new(es[0], es[1], es[2]))]),
434 )
435 .field_origin_com(true)
436 .write_symmetry_elements(write_symmetry_elements)
437 .result_save_name(out_sym)
438 .build()
439 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
440 let inp_mol = inp_mol.map(Molecule::from);
441 let mut pd_driver = SymmetryGroupDetectionDriver::builder()
442 .parameters(¶ms)
443 .xyz(inp_xyz)
444 .molecule(inp_mol.as_ref())
445 .build()
446 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
447 pd_driver
448 .run()
449 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
450 let pyunitary_symmetry: PySymmetry = (&pd_driver
451 .result()
452 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
453 .unitary_symmetry)
454 .try_into()
455 .map_err(|err: anyhow::Error| PyRuntimeError::new_err(err.to_string()))?;
456 let pymagnetic_symmetry: Option<PySymmetry> = pd_driver
457 .result()
458 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
459 .magnetic_symmetry
460 .as_ref()
461 .map(|magsym| {
462 magsym
463 .try_into()
464 .map_err(|err: anyhow::Error| PyRuntimeError::new_err(err.to_string()))
465 })
466 .transpose()?;
467 Ok((pyunitary_symmetry, pymagnetic_symmetry))
468 })
469}