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 elements: HashMap<PySymmetryElementKind, HashMap<i32, Vec<Arc<Py<PyArray1<f64>>>>>>,
183
184 generators: HashMap<PySymmetryElementKind, HashMap<i32, Vec<Arc<Py<PyArray1<f64>>>>>>,
188}
189
190impl PySymmetry {
191 fn builder() -> PySymmetryBuilder {
192 PySymmetryBuilder::default()
193 }
194}
195
196#[pymethods]
197impl PySymmetry {
198 pub fn is_infinite(&self) -> bool {
200 self.elements
201 .values()
202 .any(|kind_elements| kind_elements.contains_key(&-1))
203 || self
204 .generators
205 .values()
206 .any(|kind_generators| kind_generators.contains_key(&-1))
207 }
208
209 pub fn get_elements_of_kind(
221 &self,
222 kind: &PySymmetryElementKind,
223 ) -> PyResult<HashMap<i32, Vec<Py<PyArray1<f64>>>>> {
224 self.elements
225 .get(kind)
226 .cloned()
227 .and_then(|elements| {
228 elements
229 .iter()
230 .map(|(order, axes_arc)| {
231 let axes_opt = axes_arc
232 .iter()
233 .map(|axis_arc| Arc::into_inner(axis_arc.clone()))
234 .collect::<Option<Vec<_>>>();
235 axes_opt.map(|axes| (order.clone(), axes))
236 })
237 .collect::<Option<HashMap<_, _>>>()
238 })
239 .ok_or(PyRuntimeError::new_err(format!(
240 "Elements of kind `{kind}` not found."
241 )))
242 }
243
244 pub fn get_generators_of_kind(
256 &self,
257 kind: &PySymmetryElementKind,
258 ) -> PyResult<HashMap<i32, Vec<Py<PyArray1<f64>>>>> {
259 self.generators
260 .get(kind)
261 .cloned()
262 .and_then(|generators| {
263 generators
264 .iter()
265 .map(|(order, axes_arc)| {
266 let axes_opt = axes_arc
267 .iter()
268 .map(|axis_arc| Arc::into_inner(axis_arc.clone()))
269 .collect::<Option<Vec<_>>>();
270 axes_opt.map(|axes| (order.clone(), axes))
271 })
272 .collect::<Option<HashMap<_, _>>>()
273 })
274 .ok_or(PyRuntimeError::new_err(format!(
275 "Elements of kind `{kind}` not found."
276 )))
277 }
278}
279
280impl TryFrom<&Symmetry> for PySymmetry {
281 type Error = anyhow::Error;
282
283 fn try_from(sym: &Symmetry) -> Result<Self, Self::Error> {
284 let group_name = sym
285 .group_name
286 .clone()
287 .ok_or(format_err!("Symmetry group name not found."))?;
288 let elements = sym
289 .elements
290 .iter()
291 .map(|(symkind, kind_elements)| {
292 let pysymkind = PySymmetryElementKind::try_from(symkind)?;
293 let pykind_elements = kind_elements
294 .iter()
295 .map(|(order, order_elements)| {
296 let order_i32 = match order {
297 ElementOrder::Int(ord) => i32::try_from(*ord)?,
298 ElementOrder::Inf => -1,
299 };
300 let pyorder_elements = order_elements
301 .iter()
302 .map(|ele| {
303 Arc::new(Python::attach(|py| {
304 ele.raw_axis()
305 .iter()
306 .cloned()
307 .collect::<Vec<_>>()
308 .to_pyarray(py)
309 .unbind()
310 }))
311 })
312 .collect::<Vec<_>>();
313 Ok::<_, Self::Error>((order_i32, pyorder_elements))
314 })
315 .collect::<Result<HashMap<i32, Vec<_>>, _>>()?;
316 Ok::<_, Self::Error>((pysymkind, pykind_elements))
317 })
318 .collect::<Result<HashMap<_, _>, _>>()?;
319
320 let generators = sym
321 .generators
322 .iter()
323 .map(|(symkind, kind_generators)| {
324 let pysymkind = PySymmetryElementKind::try_from(symkind)?;
325 let pykind_generators = kind_generators
326 .iter()
327 .map(|(order, order_generators)| {
328 let order_i32 = match order {
329 ElementOrder::Int(ord) => i32::try_from(*ord)?,
330 ElementOrder::Inf => -1,
331 };
332 let pyorder_generators = order_generators
333 .iter()
334 .map(|ele| {
335 Arc::new(Python::attach(|py| {
336 ele.raw_axis()
337 .iter()
338 .cloned()
339 .collect::<Vec<_>>()
340 .to_pyarray(py)
341 .unbind()
342 }))
343 })
344 .collect::<Vec<_>>();
345 Ok::<_, Self::Error>((order_i32, pyorder_generators))
346 })
347 .collect::<Result<HashMap<i32, Vec<_>>, _>>()?;
348 Ok::<_, Self::Error>((pysymkind, pykind_generators))
349 })
350 .collect::<Result<HashMap<_, _>, _>>()?;
351
352 PySymmetry::builder()
353 .group_name(group_name)
354 .elements(elements)
355 .generators(generators)
356 .build()
357 .map_err(|err| format_err!(err))
358 }
359}
360
361#[pyfunction]
396#[pyo3(signature = (
397 inp_xyz,
398 inp_mol,
399 out_sym,
400 moi_thresholds,
401 distance_thresholds,
402 time_reversal,
403 write_symmetry_elements=true,
404 fictitious_magnetic_field=None,
405 fictitious_electric_field=None,
406))]
407pub fn detect_symmetry_group(
408 py: Python<'_>,
409 inp_xyz: Option<PathBuf>,
410 inp_mol: Option<PyMolecule>,
411 out_sym: Option<PathBuf>,
412 moi_thresholds: Vec<f64>,
413 distance_thresholds: Vec<f64>,
414 time_reversal: bool,
415 write_symmetry_elements: bool,
416 fictitious_magnetic_field: Option<[f64; 3]>,
417 fictitious_electric_field: Option<[f64; 3]>,
418) -> PyResult<(PySymmetry, Option<PySymmetry>)> {
419 py.detach(|| {
420 let params = SymmetryGroupDetectionParams::builder()
421 .distance_thresholds(&distance_thresholds)
422 .moi_thresholds(&moi_thresholds)
423 .time_reversal(time_reversal)
424 .fictitious_magnetic_fields(
425 fictitious_magnetic_field
426 .map(|bs| vec![(Point3::<f64>::origin(), Vector3::new(bs[0], bs[1], bs[2]))]),
427 )
428 .fictitious_electric_fields(
429 fictitious_electric_field
430 .map(|es| vec![(Point3::<f64>::origin(), Vector3::new(es[0], es[1], es[2]))]),
431 )
432 .field_origin_com(true)
433 .write_symmetry_elements(write_symmetry_elements)
434 .result_save_name(out_sym)
435 .build()
436 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
437 let inp_mol = inp_mol.map(Molecule::from);
438 let mut pd_driver = SymmetryGroupDetectionDriver::builder()
439 .parameters(¶ms)
440 .xyz(inp_xyz)
441 .molecule(inp_mol.as_ref())
442 .build()
443 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
444 pd_driver
445 .run()
446 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?;
447 let pyunitary_symmetry: PySymmetry = (&pd_driver
448 .result()
449 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
450 .unitary_symmetry)
451 .try_into()
452 .map_err(|err: anyhow::Error| PyRuntimeError::new_err(err.to_string()))?;
453 let pymagnetic_symmetry: Option<PySymmetry> = pd_driver
454 .result()
455 .map_err(|err| PyRuntimeError::new_err(err.to_string()))?
456 .magnetic_symmetry
457 .as_ref()
458 .map(|magsym| {
459 magsym
460 .try_into()
461 .map_err(|err: anyhow::Error| PyRuntimeError::new_err(err.to_string()))
462 })
463 .transpose()?;
464 Ok((pyunitary_symmetry, pymagnetic_symmetry))
465 })
466}