1use std::collections::{HashMap, HashSet};
4use std::fmt;
5use std::fs;
6use std::io::{BufWriter, Write};
7use std::path::Path;
8use std::process;
9
10use anyhow;
11use itertools::Itertools;
12use log;
13use nalgebra::{DVector, Matrix3, Point3, Vector3};
14use ndarray::{Array2, ShapeBuilder};
15use ndarray_linalg::{Eigh, UPLO};
16use num_traits::ToPrimitive;
17use serde::{Deserialize, Serialize};
18
19use crate::auxiliary::atom::{Atom, AtomKind, ElementMap};
20use crate::auxiliary::geometry::{self, ImproperRotationKind, Transform};
21use crate::permutation::{permute_inplace, PermutableCollection, Permutation};
22
23#[cfg(test)]
24#[path = "sea_tests.rs"]
25mod sea_tests;
26
27#[cfg(test)]
28#[path = "molecule_tests.rs"]
29mod molecule_tests;
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct Molecule {
38 pub atoms: Vec<Atom>,
40
41 pub electric_atoms: Option<Vec<Atom>>,
43
44 pub magnetic_atoms: Option<Vec<Atom>>,
46
47 pub threshold: f64,
49}
50
51impl Molecule {
52 #[must_use]
66 pub fn from_xyz<P: AsRef<Path>>(filename: P, thresh: f64) -> Self {
67 let contents = fs::read_to_string(&filename).unwrap_or_else(|err| {
68 log::error!("Unable to read file {}.", filename.as_ref().display());
69 log::error!("{}", err);
70 process::exit(1);
71 });
72
73 let mut atoms: Vec<Atom> = vec![];
74 let emap = ElementMap::new();
75 let mut n_atoms = 0usize;
76 for (i, line) in contents.lines().enumerate() {
77 if i == 0 {
78 n_atoms = line.parse::<usize>().unwrap_or_else(|err| {
79 log::error!(
80 "Unable to read number of atoms in {}.",
81 filename.as_ref().display()
82 );
83 log::error!("{}", err);
84 process::exit(1);
85 });
86 } else if i == 1 {
87 continue;
88 } else {
89 atoms.push(
90 Atom::from_xyz(line, &emap, thresh)
91 .unwrap_or_else(|| panic!("Unable to parse {line} to give an atom.")),
92 );
93 }
94 }
95 assert_eq!(
96 atoms.len(),
97 n_atoms,
98 "Expected {} atoms, got {} instead.",
99 n_atoms,
100 atoms.len()
101 );
102 Molecule {
103 atoms,
104 electric_atoms: None,
105 magnetic_atoms: None,
106 threshold: thresh,
107 }
108 }
109
110 pub fn to_xyz<P: AsRef<Path>>(&self, filename: P) -> Result<(), anyhow::Error> {
116 let mut f = BufWriter::new(fs::File::create(filename)?);
117 writeln!(f, "{}", self.atoms.len())?;
118 writeln!(f, "")?;
119 for atom in self.atoms.iter() {
120 writeln!(f, "{}", atom.to_xyz())?;
121 }
122 Ok(())
123 }
124
125 #[must_use]
141 pub fn from_atoms(all_atoms: &[Atom], thresh: f64) -> Self {
142 let mut atoms: Vec<Atom> = all_atoms
143 .iter()
144 .filter(|atom| matches!(atom.kind, AtomKind::Ordinary))
145 .cloned()
146 .collect();
147 atoms.iter_mut().for_each(|atom| atom.threshold = thresh);
148
149 let mut magnetic_atoms_vec: Vec<Atom> = all_atoms
150 .iter()
151 .filter(|atom| matches!(atom.kind, AtomKind::Magnetic(_)))
152 .cloned()
153 .collect();
154 magnetic_atoms_vec
155 .iter_mut()
156 .for_each(|atom| atom.threshold = thresh);
157 let magnetic_atoms = if magnetic_atoms_vec.is_empty() {
158 None
159 } else {
160 Some(magnetic_atoms_vec)
161 };
162
163 let mut electric_atoms_vec: Vec<Atom> = all_atoms
164 .iter()
165 .filter(|atom| matches!(atom.kind, AtomKind::Electric(_)))
166 .cloned()
167 .collect();
168 electric_atoms_vec
169 .iter_mut()
170 .for_each(|atom| atom.threshold = thresh);
171 let electric_atoms = if electric_atoms_vec.is_empty() {
172 None
173 } else {
174 Some(electric_atoms_vec)
175 };
176
177 Molecule {
178 atoms,
179 electric_atoms,
180 magnetic_atoms,
181 threshold: thresh,
182 }
183 }
184
185 #[must_use]
187 pub fn molecule_ordinary_atoms(&self) -> Self {
188 Self::from_atoms(&self.atoms, self.threshold)
189 }
190
191 #[must_use]
198 pub fn molecule_magnetic_atoms(&self) -> Option<Self> {
199 Some(Self::from_atoms(
200 self.magnetic_atoms.as_ref()?,
201 self.threshold,
202 ))
203 }
204
205 #[must_use]
212 pub fn molecule_electric_atoms(&self) -> Option<Self> {
213 Some(Self::from_atoms(
214 self.electric_atoms.as_ref()?,
215 self.threshold,
216 ))
217 }
218
219 #[must_use]
226 pub fn get_all_atoms(&self) -> Vec<&Atom> {
227 let mut atoms: Vec<&Atom> = vec![];
228 for atom in &self.atoms {
229 atoms.push(atom);
230 }
231 if let Some(magnetic_atoms) = &self.magnetic_atoms {
232 for magnetic_atom in magnetic_atoms.iter() {
233 atoms.push(magnetic_atom);
234 }
235 }
236 if let Some(electric_atoms) = &self.electric_atoms {
237 for electric_atom in electric_atoms.iter() {
238 atoms.push(electric_atom);
239 }
240 }
241 atoms
242 }
243
244 #[must_use]
251 pub fn get_all_atoms_mut(&mut self) -> Vec<&mut Atom> {
252 let mut atoms: Vec<&mut Atom> = vec![];
253 for atom in &mut self.atoms {
254 atoms.push(atom);
255 }
256 if let Some(magnetic_atoms) = &mut self.magnetic_atoms {
257 for magnetic_atom in magnetic_atoms.iter_mut() {
258 atoms.push(magnetic_atom);
259 }
260 }
261 if let Some(electric_atoms) = &mut self.electric_atoms {
262 for electric_atom in electric_atoms.iter_mut() {
263 atoms.push(electric_atom);
264 }
265 }
266 atoms
267 }
268
269 #[must_use]
277 pub fn calc_com(&self) -> Point3<f64> {
278 let atoms = &self.atoms;
279 let mut com: Point3<f64> = Point3::origin();
280 if atoms.is_empty() {
281 return com;
282 }
283 let mut tot_m: f64 = 0.0;
284 for atom in atoms.iter() {
285 let m: f64 = atom.atomic_mass;
286 com += atom.coordinates * m - Point3::origin();
287 tot_m += m;
288 }
289 com *= 1.0 / tot_m;
290 com
291 }
292
293 #[must_use]
305 pub fn calc_inertia_tensor(&self, origin: &Point3<f64>) -> Matrix3<f64> {
306 let atoms = self.get_all_atoms();
307 let mut inertia_tensor = Matrix3::zeros();
308 for atom in &atoms {
309 let rel_coordinates: Vector3<f64> = atom.coordinates - origin;
310 for i in 0..3 {
311 for j in 0..=i {
312 if i == j {
313 inertia_tensor[(i, j)] += atom.atomic_mass
314 * (rel_coordinates.norm_squared()
315 - rel_coordinates[i] * rel_coordinates[j]);
316 } else {
317 inertia_tensor[(i, j)] -=
318 atom.atomic_mass * rel_coordinates[i] * rel_coordinates[j];
319 inertia_tensor[(j, i)] -=
320 atom.atomic_mass * rel_coordinates[j] * rel_coordinates[i];
321 }
322 }
323 }
324 }
325 log::debug!("Origin for inertia tensor:");
326 for component in origin.iter() {
327 log::debug!(" {component:+.14}");
328 }
329 log::debug!("Inertia tensor:\n{}", inertia_tensor);
330 inertia_tensor
331 }
332
333 #[must_use]
346 pub fn calc_moi(&self) -> ([f64; 3], [Vector3<f64>; 3]) {
347 let inertia_tensor = Array2::from_shape_vec(
348 (3, 3).f(),
349 self.calc_inertia_tensor(&self.calc_com())
350 .into_iter()
351 .copied()
352 .collect::<Vec<_>>(),
353 )
354 .expect("Unable to construct the inertia tensor.");
355 let (eigvals, eigvecs) = inertia_tensor
356 .eigh(UPLO::Lower)
357 .expect("Unable to diagonalise the inertia tensor.");
358 let eigenvalues: Vec<f64> = eigvals.into_iter().collect::<Vec<_>>();
359 let eigenvectors = eigvecs
360 .columns()
361 .into_iter()
362 .map(|col| Vector3::from_iterator(col.into_owned()))
363 .collect::<Vec<_>>();
364 let mut eigen_tuple: Vec<(f64, _)> = eigenvalues
365 .iter()
366 .copied()
367 .zip(eigenvectors.iter().copied())
368 .collect();
369 eigen_tuple.sort_by(|(eigval0, _), (eigval1, _)| {
370 eigval0
371 .partial_cmp(eigval1)
372 .unwrap_or_else(|| panic!("{eigval0} and {eigval1} cannot be compared."))
373 });
374 let (sorted_eigenvalues, sorted_eigenvectors): (Vec<f64>, Vec<_>) =
375 eigen_tuple.into_iter().unzip();
376 let result = (
377 [
378 sorted_eigenvalues[0],
379 sorted_eigenvalues[1],
380 sorted_eigenvalues[2],
381 ],
382 [
383 geometry::get_standard_positive_pole(
384 &Vector3::new(
385 sorted_eigenvectors[0][(0, 0)],
386 sorted_eigenvectors[0][(1, 0)],
387 sorted_eigenvectors[0][(2, 0)],
388 ),
389 self.threshold,
390 ),
391 geometry::get_standard_positive_pole(
392 &Vector3::new(
393 sorted_eigenvectors[1][(0, 0)],
394 sorted_eigenvectors[1][(1, 0)],
395 sorted_eigenvectors[1][(2, 0)],
396 ),
397 self.threshold,
398 ),
399 geometry::get_standard_positive_pole(
400 &Vector3::new(
401 sorted_eigenvectors[2][(0, 0)],
402 sorted_eigenvectors[2][(1, 0)],
403 sorted_eigenvectors[2][(2, 0)],
404 ),
405 self.threshold,
406 ),
407 ],
408 );
409 result
410 .0
411 .iter()
412 .zip(result.1.iter())
413 .for_each(|(moi, axis)| {
414 log::debug!("Principal moment of inertia: {moi:.14}");
415 log::debug!(" -- Principal axis:\n{axis}");
416 });
417 result
418 }
419
420 pub fn calc_interatomic_distance_matrix(&self) -> (Array2<f64>, Vec<Vec<usize>>) {
433 let all_atoms = &self.get_all_atoms();
434 let all_coords: Vec<_> = all_atoms.iter().map(|atm| atm.coordinates).collect();
435 let mut dist_columns: Vec<DVector<f64>> = vec![];
436 let mut sorted_dist_columns: Vec<DVector<f64>> = vec![];
437
438 let mut equiv_indicess: Vec<Vec<usize>> = vec![vec![0]];
440 for (j, coord_j) in all_coords.iter().enumerate() {
441 let column_j = all_coords
445 .iter()
446 .map(|coord_i| (coord_j - coord_i).norm())
447 .collect_vec();
448 let mut sorted_column_j = column_j.clone();
449 dist_columns.push(DVector::from_vec(column_j));
450
451 sorted_column_j.sort_by(|a, b| {
452 a.partial_cmp(b).unwrap_or_else(|| {
453 panic!("Mass-weighted interatomic distances {a} and {b} cannot be compared.")
454 })
455 });
456 let sorted_column_j_vec = DVector::from_vec(sorted_column_j);
457 if j == 0 {
458 sorted_dist_columns.push(sorted_column_j_vec);
459 } else {
460 let equiv_set_search = equiv_indicess.iter().position(|equiv_indices| {
461 sorted_dist_columns[equiv_indices[0]].relative_eq(
462 &sorted_column_j_vec,
463 self.threshold,
464 self.threshold,
465 ) && match (&all_atoms[j].kind, &all_atoms[equiv_indices[0]].kind) {
466 (AtomKind::Ordinary, AtomKind::Ordinary) => {
467 all_atoms[j].atomic_number == all_atoms[equiv_indices[0]].atomic_number
468 }
469 (AtomKind::Magnetic(_), AtomKind::Magnetic(_))
470 | (AtomKind::Electric(_), AtomKind::Electric(_)) => true,
471 _ => false,
472 }
473 });
474 sorted_dist_columns.push(sorted_column_j_vec);
475 if let Some(index) = equiv_set_search {
476 equiv_indicess[index].push(j);
477 } else {
478 equiv_indicess.push(vec![j]);
479 };
480 }
481 }
482
483 let dist_elements_f = dist_columns.iter().flatten().cloned().collect::<Vec<_>>();
484 let n_atoms = all_atoms.len();
485 let dist_matrix = Array2::<f64>::from_shape_vec((n_atoms, n_atoms).f(), dist_elements_f)
486 .expect("Unable to collect the interatomic distances into a square matrix.");
487
488 (dist_matrix, equiv_indicess)
489 }
490
491 #[must_use]
504 pub fn calc_sea_groups(&self) -> Vec<Vec<Atom>> {
505 let all_atoms = &self.get_all_atoms();
506 let (_, equiv_indicess) = self.calc_interatomic_distance_matrix();
507 let sea_groups: Vec<Vec<Atom>> = equiv_indicess
508 .iter()
509 .map(|equiv_indices| {
510 equiv_indices
511 .iter()
512 .map(|index| all_atoms[*index].clone())
513 .collect()
514 })
515 .collect();
516
517 log::debug!("Number of SEA groups: {}", sea_groups.len());
518 sea_groups
519 }
520
521 pub fn set_magnetic_field(&mut self, magnetic_field: Option<Vector3<f64>>) {
532 if let Some(b_vec) = magnetic_field {
533 if approx::relative_ne!(b_vec.norm(), 0.0) {
534 let com = self.calc_com();
535 let ave_mag = {
536 let average_distance = self
537 .atoms
538 .iter()
539 .fold(0.0, |acc, atom| acc + (atom.coordinates - com).magnitude())
540 / self.atoms.len().to_f64().unwrap_or_else(|| {
541 panic!("Unable to convert `{}` to `f64`.", self.atoms.len())
542 });
543 if average_distance > 0.0 {
544 average_distance
545 } else {
546 0.5
547 }
548 };
549 let b_vec_norm = b_vec.normalize() * ave_mag * 0.5;
550 self.magnetic_atoms = Some(vec![
551 Atom::new_special(AtomKind::Magnetic(true), com + b_vec_norm, self.threshold)
552 .expect("Unable to construct a special magnetic atom."),
553 Atom::new_special(AtomKind::Magnetic(false), com - b_vec_norm, self.threshold)
554 .expect("Unable to construct a special magnetic atom."),
555 ]);
556 } else {
557 self.magnetic_atoms = None;
558 }
559 } else {
560 self.magnetic_atoms = None;
561 }
562 }
563
564 pub fn set_electric_field(&mut self, electric_field: Option<Vector3<f64>>) {
575 if let Some(e_vec) = electric_field {
576 if approx::relative_ne!(e_vec.norm(), 0.0) {
577 let com = self.calc_com();
578 let ave_mag = {
579 let average_distance = self
580 .atoms
581 .iter()
582 .fold(0.0, |acc, atom| acc + (atom.coordinates - com).magnitude())
583 / self.atoms.len().to_f64().unwrap_or_else(|| {
584 panic!("Unable to convert `{}` to `f64`.", self.atoms.len())
585 });
586 if average_distance > 0.0 {
587 average_distance
588 } else {
589 0.5
590 }
591 };
592 let e_vec_norm = e_vec.normalize() * ave_mag * 0.5;
593 self.electric_atoms = Some(vec![Atom::new_special(
594 AtomKind::Electric(true),
595 com + e_vec_norm,
596 self.threshold,
597 )
598 .expect("Unable to construct an electric special atom.")]);
599 } else {
600 self.electric_atoms = None;
601 }
602 } else {
603 self.electric_atoms = None;
604 }
605 }
606
607 pub fn adjust_threshold(&self, thresh: f64) -> Self {
617 Self::from_atoms(
618 &self
619 .get_all_atoms()
620 .into_iter()
621 .cloned()
622 .collect::<Vec<_>>(),
623 thresh,
624 )
625 }
626
627 pub fn reorientate_mut(&mut self, moi_thresh: f64) {
641 let (moi, principal_axes) = self.calc_moi();
642 let rotmat = if approx::relative_ne!(
643 moi[0],
644 moi[1],
645 max_relative = moi_thresh,
646 epsilon = moi_thresh
647 ) && approx::relative_eq!(
648 moi[1],
649 moi[2],
650 max_relative = moi_thresh,
651 epsilon = moi_thresh
652 ) {
653 Matrix3::from_columns(&[principal_axes[1], principal_axes[2], principal_axes[0]])
655 .transpose()
656 } else {
657 Matrix3::from_columns(&[principal_axes[0], principal_axes[1], principal_axes[2]])
659 .transpose()
660 };
661 let com = self.calc_com();
662 self.recentre_mut();
663 self.transform_mut(&rotmat);
664 self.translate_mut(&(com - Point3::origin()));
665 }
666
667 pub fn reorientate(&self, moi_thresh: f64) -> Self {
685 let mut reoriented_mol = self.clone();
686 reoriented_mol.reorientate_mut(moi_thresh);
687 reoriented_mol
688 }
689}
690
691impl fmt::Display for Molecule {
696 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
697 writeln!(f, "Molecule consisting of")?;
698 for atom in self.get_all_atoms().iter() {
699 writeln!(f, " {atom}")?;
700 }
701 Ok(())
702 }
703}
704
705impl Transform for Molecule {
706 fn transform_mut(&mut self, mat: &Matrix3<f64>) {
707 for atom in &mut self.atoms {
708 atom.transform_mut(mat);
709 }
710 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
711 for atom in mag_atoms.iter_mut() {
712 atom.transform_mut(mat);
713 }
714 }
715 if let Some(ref mut ele_atoms) = self.electric_atoms {
716 for atom in ele_atoms.iter_mut() {
717 atom.transform_mut(mat);
718 }
719 }
720 }
721
722 fn rotate_mut(&mut self, angle: f64, axis: &Vector3<f64>) {
723 for atom in &mut self.atoms {
724 atom.rotate_mut(angle, axis);
725 }
726 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
727 for atom in mag_atoms.iter_mut() {
728 atom.rotate_mut(angle, axis);
729 }
730 }
731 if let Some(ref mut ele_atoms) = self.electric_atoms {
732 for atom in ele_atoms.iter_mut() {
733 atom.rotate_mut(angle, axis);
734 }
735 }
736 }
737
738 fn improper_rotate_mut(
739 &mut self,
740 angle: f64,
741 axis: &Vector3<f64>,
742 kind: &ImproperRotationKind,
743 ) {
744 for atom in &mut self.atoms {
745 atom.improper_rotate_mut(angle, axis, kind);
746 }
747 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
748 for atom in mag_atoms.iter_mut() {
749 atom.improper_rotate_mut(angle, axis, kind);
750 }
751 }
752 if let Some(ref mut ele_atoms) = self.electric_atoms {
753 for atom in ele_atoms.iter_mut() {
754 atom.improper_rotate_mut(angle, axis, kind);
755 }
756 }
757 }
758
759 fn translate_mut(&mut self, tvec: &Vector3<f64>) {
760 for atom in &mut self.atoms {
761 atom.translate_mut(tvec);
762 }
763 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
764 for atom in mag_atoms.iter_mut() {
765 atom.translate_mut(tvec);
766 }
767 }
768 if let Some(ref mut ele_atoms) = self.electric_atoms {
769 for atom in ele_atoms.iter_mut() {
770 atom.translate_mut(tvec);
771 }
772 }
773 }
774
775 fn recentre_mut(&mut self) {
776 let com = self.calc_com();
777 let tvec = -Vector3::new(com[0], com[1], com[2]);
778 self.translate_mut(&tvec);
779 }
780
781 fn reverse_time_mut(&mut self) {
782 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
783 for atom in mag_atoms.iter_mut() {
784 atom.reverse_time_mut();
785 }
786 }
787 }
788
789 fn transform(&self, mat: &Matrix3<f64>) -> Self {
790 let mut transformed_mol = self.clone();
791 transformed_mol.transform_mut(mat);
792 transformed_mol
793 }
794
795 fn rotate(&self, angle: f64, axis: &Vector3<f64>) -> Self {
796 let mut rotated_mol = self.clone();
797 rotated_mol.rotate_mut(angle, axis);
798 rotated_mol
799 }
800
801 fn improper_rotate(
802 &self,
803 angle: f64,
804 axis: &Vector3<f64>,
805 kind: &ImproperRotationKind,
806 ) -> Self {
807 let mut improper_rotated_mol = self.clone();
808 improper_rotated_mol.improper_rotate_mut(angle, axis, kind);
809 improper_rotated_mol
810 }
811
812 fn translate(&self, tvec: &Vector3<f64>) -> Self {
813 let mut translated_mol = self.clone();
814 translated_mol.translate_mut(tvec);
815 translated_mol
816 }
817
818 fn recentre(&self) -> Self {
819 let mut recentred_mol = self.clone();
820 recentred_mol.recentre_mut();
821 recentred_mol
822 }
823
824 fn reverse_time(&self) -> Self {
825 let mut time_reversed_mol = self.clone();
826 time_reversed_mol.reverse_time_mut();
827 time_reversed_mol
828 }
829}
830
831impl PartialEq for Molecule {
832 fn eq(&self, other: &Self) -> bool {
833 if self.atoms.len() != other.atoms.len() {
834 return false;
835 };
836 let thresh = self
837 .atoms
838 .iter()
839 .chain(other.atoms.iter())
840 .fold(0.0_f64, |acc, atom| acc.max(atom.threshold));
841
842 let mut other_atoms_ref: HashSet<_> = other.atoms.iter().collect();
843 for s_atom in &self.atoms {
844 let o_atom = other
845 .atoms
846 .iter()
847 .find(|o_atm| (s_atom.coordinates - o_atm.coordinates).norm() < thresh);
848 match o_atom {
849 Some(atm) => {
850 other_atoms_ref.remove(atm);
851 }
852 None => {
853 break;
854 }
855 }
856 }
857 if !other_atoms_ref.is_empty() {
858 return false;
859 }
860
861 if let Some(self_mag_atoms) = &self.magnetic_atoms {
862 if let Some(other_mag_atoms) = &other.magnetic_atoms {
863 let mut other_mag_atoms_ref: HashSet<_> = other_mag_atoms.iter().collect();
864 for s_atom in self_mag_atoms.iter() {
865 let o_atom = other_mag_atoms.iter().find(|o_atm| {
866 (s_atom.coordinates - o_atm.coordinates).norm() < thresh
867 && s_atom.kind == o_atm.kind
868 });
869 match o_atom {
870 Some(atm) => {
871 other_mag_atoms_ref.remove(atm);
872 }
873 None => {
874 break;
875 }
876 }
877 }
878 if !other_mag_atoms_ref.is_empty() {
879 return false;
880 }
881 } else {
882 return false;
883 }
884 } else if other.magnetic_atoms.is_some() {
885 return false;
886 };
887
888 if let Some(self_ele_atoms) = &self.electric_atoms {
889 if let Some(other_ele_atoms) = &other.electric_atoms {
890 let mut other_ele_atoms_ref: HashSet<_> = other_ele_atoms.iter().collect();
891 for s_atom in self_ele_atoms.iter() {
892 let o_atom = other_ele_atoms.iter().find(|o_atm| {
893 (s_atom.coordinates - o_atm.coordinates).norm() < thresh
894 && s_atom.kind == o_atm.kind
895 });
896 match o_atom {
897 Some(atm) => {
898 other_ele_atoms_ref.remove(atm);
899 }
900 None => {
901 break;
902 }
903 }
904 }
905 if !other_ele_atoms_ref.is_empty() {
906 return false;
907 }
908 } else {
909 return false;
910 }
911 } else if other.electric_atoms.is_some() {
912 return false;
913 };
914 true
915 }
916}
917
918impl PermutableCollection for Molecule {
919 type Rank = usize;
920
921 fn get_perm_of(&self, other: &Self) -> Option<Permutation<Self::Rank>> {
933 let self_recentred = self.recentre();
934 let other_recentred = other.recentre();
935 let o_atoms: HashMap<Atom, usize> = other_recentred
936 .atoms
937 .into_iter()
938 .chain(
939 other_recentred
940 .magnetic_atoms
941 .unwrap_or_default()
942 .into_iter(),
943 )
944 .chain(
945 other_recentred
946 .electric_atoms
947 .unwrap_or_default()
948 .into_iter(),
949 )
950 .enumerate()
951 .map(|(i, atom)| (atom, i))
952 .collect();
953 let image_opt: Option<Vec<Self::Rank>> = self_recentred
954 .atoms
955 .iter()
956 .chain(self_recentred.magnetic_atoms.unwrap_or_default().iter())
957 .chain(self_recentred.electric_atoms.unwrap_or_default().iter())
958 .map(|s_atom| {
959 o_atoms
960 .get(s_atom)
961 .or_else(|| {
962 let thresh = s_atom.threshold;
963 log::debug!("Unable to retrieve matching original atom by hash. Falling back on distance comparisons with threshold {thresh:.3e}...");
964 o_atoms.iter().find_map(|(o_atom, o_atom_idx)| {
965 if s_atom.atomic_number == o_atom.atomic_number
966 && s_atom.kind == o_atom.kind
967 && (s_atom.coordinates - o_atom.coordinates).norm() < thresh
968 {
969 Some(o_atom_idx)
970 } else {
971 None
972 }
973 })
974 })
975 .copied()
976 })
977 .collect();
978 image_opt.and_then(|image| Permutation::from_image(image).ok())
979 }
980
981 fn permute(&self, perm: &Permutation<Self::Rank>) -> Result<Self, anyhow::Error> {
999 let mut p_mol = self.clone();
1000 p_mol.permute_mut(perm)?;
1001 Ok(p_mol)
1002 }
1003
1004 fn permute_mut(&mut self, perm: &Permutation<Self::Rank>) -> Result<(), anyhow::Error> {
1020 let n_ordinary = self.atoms.len();
1021 let perm_ordinary = Permutation::from_image(perm.image()[0..n_ordinary].to_vec())?;
1022 permute_inplace(&mut self.atoms, &perm_ordinary);
1023
1024 let n_last = if let Some(mag_atoms) = self.magnetic_atoms.as_mut() {
1025 let n_magnetic = mag_atoms.len();
1026 let perm_magnetic = Permutation::from_image(
1027 perm.image()[n_ordinary..(n_ordinary + n_magnetic)]
1028 .iter()
1029 .map(|x| x - n_ordinary)
1030 .collect::<Vec<_>>(),
1031 )?;
1032 permute_inplace(mag_atoms, &perm_magnetic);
1033 n_ordinary + n_magnetic
1034 } else {
1035 n_ordinary
1036 };
1037
1038 if let Some(elec_atoms) = self.electric_atoms.as_mut() {
1039 let n_electric = elec_atoms.len();
1040 let perm_electric = Permutation::from_image(
1041 perm.image()[n_last..(n_last + n_electric)]
1042 .iter()
1043 .map(|x| x - n_last)
1044 .collect::<Vec<_>>(),
1045 )?;
1046 permute_inplace(elec_atoms, &perm_electric);
1047 }
1048 Ok(())
1049 }
1050}