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::{PermutableCollection, Permutation, permute_inplace};
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![
594 Atom::new_special(AtomKind::Electric(true), com + e_vec_norm, self.threshold)
595 .expect("Unable to construct an electric special atom."),
596 ]);
597 } else {
598 self.electric_atoms = None;
599 }
600 } else {
601 self.electric_atoms = None;
602 }
603 }
604
605 pub fn adjust_threshold(&self, thresh: f64) -> Self {
615 Self::from_atoms(
616 &self
617 .get_all_atoms()
618 .into_iter()
619 .cloned()
620 .collect::<Vec<_>>(),
621 thresh,
622 )
623 }
624
625 pub fn reorientate_mut(&mut self, moi_thresh: f64) {
639 let (moi, principal_axes) = self.calc_moi();
640 let rotmat = if approx::relative_ne!(
641 moi[0],
642 moi[1],
643 max_relative = moi_thresh,
644 epsilon = moi_thresh
645 ) && approx::relative_eq!(
646 moi[1],
647 moi[2],
648 max_relative = moi_thresh,
649 epsilon = moi_thresh
650 ) {
651 Matrix3::from_columns(&[principal_axes[1], principal_axes[2], principal_axes[0]])
653 .transpose()
654 } else {
655 Matrix3::from_columns(&[principal_axes[0], principal_axes[1], principal_axes[2]])
657 .transpose()
658 };
659 let com = self.calc_com();
660 self.recentre_mut();
661 self.transform_mut(&rotmat);
662 self.translate_mut(&(com - Point3::origin()));
663 }
664
665 pub fn reorientate(&self, moi_thresh: f64) -> Self {
683 let mut reoriented_mol = self.clone();
684 reoriented_mol.reorientate_mut(moi_thresh);
685 reoriented_mol
686 }
687}
688
689impl fmt::Display for Molecule {
694 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695 writeln!(f, "Molecule consisting of")?;
696 for atom in self.get_all_atoms().iter() {
697 writeln!(f, " {atom}")?;
698 }
699 Ok(())
700 }
701}
702
703impl Transform for Molecule {
704 fn transform_mut(&mut self, mat: &Matrix3<f64>) {
705 for atom in &mut self.atoms {
706 atom.transform_mut(mat);
707 }
708 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
709 for atom in mag_atoms.iter_mut() {
710 atom.transform_mut(mat);
711 }
712 }
713 if let Some(ref mut ele_atoms) = self.electric_atoms {
714 for atom in ele_atoms.iter_mut() {
715 atom.transform_mut(mat);
716 }
717 }
718 }
719
720 fn rotate_mut(&mut self, angle: f64, axis: &Vector3<f64>) {
721 for atom in &mut self.atoms {
722 atom.rotate_mut(angle, axis);
723 }
724 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
725 for atom in mag_atoms.iter_mut() {
726 atom.rotate_mut(angle, axis);
727 }
728 }
729 if let Some(ref mut ele_atoms) = self.electric_atoms {
730 for atom in ele_atoms.iter_mut() {
731 atom.rotate_mut(angle, axis);
732 }
733 }
734 }
735
736 fn improper_rotate_mut(
737 &mut self,
738 angle: f64,
739 axis: &Vector3<f64>,
740 kind: &ImproperRotationKind,
741 ) {
742 for atom in &mut self.atoms {
743 atom.improper_rotate_mut(angle, axis, kind);
744 }
745 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
746 for atom in mag_atoms.iter_mut() {
747 atom.improper_rotate_mut(angle, axis, kind);
748 }
749 }
750 if let Some(ref mut ele_atoms) = self.electric_atoms {
751 for atom in ele_atoms.iter_mut() {
752 atom.improper_rotate_mut(angle, axis, kind);
753 }
754 }
755 }
756
757 fn translate_mut(&mut self, tvec: &Vector3<f64>) {
758 for atom in &mut self.atoms {
759 atom.translate_mut(tvec);
760 }
761 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
762 for atom in mag_atoms.iter_mut() {
763 atom.translate_mut(tvec);
764 }
765 }
766 if let Some(ref mut ele_atoms) = self.electric_atoms {
767 for atom in ele_atoms.iter_mut() {
768 atom.translate_mut(tvec);
769 }
770 }
771 }
772
773 fn recentre_mut(&mut self) {
774 let com = self.calc_com();
775 let tvec = -Vector3::new(com[0], com[1], com[2]);
776 self.translate_mut(&tvec);
777 }
778
779 fn reverse_time_mut(&mut self) {
780 if let Some(ref mut mag_atoms) = self.magnetic_atoms {
781 for atom in mag_atoms.iter_mut() {
782 atom.reverse_time_mut();
783 }
784 }
785 }
786
787 fn transform(&self, mat: &Matrix3<f64>) -> Self {
788 let mut transformed_mol = self.clone();
789 transformed_mol.transform_mut(mat);
790 transformed_mol
791 }
792
793 fn rotate(&self, angle: f64, axis: &Vector3<f64>) -> Self {
794 let mut rotated_mol = self.clone();
795 rotated_mol.rotate_mut(angle, axis);
796 rotated_mol
797 }
798
799 fn improper_rotate(
800 &self,
801 angle: f64,
802 axis: &Vector3<f64>,
803 kind: &ImproperRotationKind,
804 ) -> Self {
805 let mut improper_rotated_mol = self.clone();
806 improper_rotated_mol.improper_rotate_mut(angle, axis, kind);
807 improper_rotated_mol
808 }
809
810 fn translate(&self, tvec: &Vector3<f64>) -> Self {
811 let mut translated_mol = self.clone();
812 translated_mol.translate_mut(tvec);
813 translated_mol
814 }
815
816 fn recentre(&self) -> Self {
817 let mut recentred_mol = self.clone();
818 recentred_mol.recentre_mut();
819 recentred_mol
820 }
821
822 fn reverse_time(&self) -> Self {
823 let mut time_reversed_mol = self.clone();
824 time_reversed_mol.reverse_time_mut();
825 time_reversed_mol
826 }
827}
828
829impl PartialEq for Molecule {
830 fn eq(&self, other: &Self) -> bool {
831 if self.atoms.len() != other.atoms.len() {
832 return false;
833 };
834 let thresh = self
835 .atoms
836 .iter()
837 .chain(other.atoms.iter())
838 .fold(0.0_f64, |acc, atom| acc.max(atom.threshold));
839
840 let mut other_atoms_ref: HashSet<_> = other.atoms.iter().collect();
841 for s_atom in &self.atoms {
842 let o_atom = other
843 .atoms
844 .iter()
845 .find(|o_atm| (s_atom.coordinates - o_atm.coordinates).norm() < thresh);
846 match o_atom {
847 Some(atm) => {
848 other_atoms_ref.remove(atm);
849 }
850 None => {
851 break;
852 }
853 }
854 }
855 if !other_atoms_ref.is_empty() {
856 return false;
857 }
858
859 if let Some(self_mag_atoms) = &self.magnetic_atoms {
860 if let Some(other_mag_atoms) = &other.magnetic_atoms {
861 let mut other_mag_atoms_ref: HashSet<_> = other_mag_atoms.iter().collect();
862 for s_atom in self_mag_atoms.iter() {
863 let o_atom = other_mag_atoms.iter().find(|o_atm| {
864 (s_atom.coordinates - o_atm.coordinates).norm() < thresh
865 && s_atom.kind == o_atm.kind
866 });
867 match o_atom {
868 Some(atm) => {
869 other_mag_atoms_ref.remove(atm);
870 }
871 None => {
872 break;
873 }
874 }
875 }
876 if !other_mag_atoms_ref.is_empty() {
877 return false;
878 }
879 } else {
880 return false;
881 }
882 } else if other.magnetic_atoms.is_some() {
883 return false;
884 };
885
886 if let Some(self_ele_atoms) = &self.electric_atoms {
887 if let Some(other_ele_atoms) = &other.electric_atoms {
888 let mut other_ele_atoms_ref: HashSet<_> = other_ele_atoms.iter().collect();
889 for s_atom in self_ele_atoms.iter() {
890 let o_atom = other_ele_atoms.iter().find(|o_atm| {
891 (s_atom.coordinates - o_atm.coordinates).norm() < thresh
892 && s_atom.kind == o_atm.kind
893 });
894 match o_atom {
895 Some(atm) => {
896 other_ele_atoms_ref.remove(atm);
897 }
898 None => {
899 break;
900 }
901 }
902 }
903 if !other_ele_atoms_ref.is_empty() {
904 return false;
905 }
906 } else {
907 return false;
908 }
909 } else if other.electric_atoms.is_some() {
910 return false;
911 };
912 true
913 }
914}
915
916impl PermutableCollection for Molecule {
917 type Rank = usize;
918
919 fn get_perm_of(&self, other: &Self) -> Option<Permutation<Self::Rank>> {
931 let self_recentred = self.recentre();
932 let other_recentred = other.recentre();
933 let o_atoms: HashMap<Atom, usize> = other_recentred
934 .atoms
935 .into_iter()
936 .chain(other_recentred.magnetic_atoms.unwrap_or_default())
937 .chain(other_recentred.electric_atoms.unwrap_or_default())
938 .enumerate()
939 .map(|(i, atom)| (atom, i))
940 .collect();
941 let image_opt: Option<Vec<Self::Rank>> = self_recentred
942 .atoms
943 .iter()
944 .chain(self_recentred.magnetic_atoms.unwrap_or_default().iter())
945 .chain(self_recentred.electric_atoms.unwrap_or_default().iter())
946 .map(|s_atom| {
947 o_atoms
948 .get(s_atom)
949 .or_else(|| {
950 let thresh = s_atom.threshold;
951 log::debug!("Unable to retrieve matching original atom by hash. Falling back on distance comparisons with threshold {thresh:.3e}...");
952 o_atoms.iter().find_map(|(o_atom, o_atom_idx)| {
953 if s_atom.atomic_number == o_atom.atomic_number
954 && s_atom.kind == o_atom.kind
955 && (s_atom.coordinates - o_atom.coordinates).norm() < thresh
956 {
957 Some(o_atom_idx)
958 } else {
959 None
960 }
961 })
962 })
963 .copied()
964 })
965 .collect();
966 image_opt.and_then(|image| Permutation::from_image(image).ok())
967 }
968
969 fn permute(&self, perm: &Permutation<Self::Rank>) -> Result<Self, anyhow::Error> {
987 let mut p_mol = self.clone();
988 p_mol.permute_mut(perm)?;
989 Ok(p_mol)
990 }
991
992 fn permute_mut(&mut self, perm: &Permutation<Self::Rank>) -> Result<(), anyhow::Error> {
1008 let n_ordinary = self.atoms.len();
1009 let perm_ordinary = Permutation::from_image(perm.image()[0..n_ordinary].to_vec())?;
1010 permute_inplace(&mut self.atoms, &perm_ordinary);
1011
1012 let n_last = if let Some(mag_atoms) = self.magnetic_atoms.as_mut() {
1013 let n_magnetic = mag_atoms.len();
1014 let perm_magnetic = Permutation::from_image(
1015 perm.image()[n_ordinary..(n_ordinary + n_magnetic)]
1016 .iter()
1017 .map(|x| x - n_ordinary)
1018 .collect::<Vec<_>>(),
1019 )?;
1020 permute_inplace(mag_atoms, &perm_magnetic);
1021 n_ordinary + n_magnetic
1022 } else {
1023 n_ordinary
1024 };
1025
1026 if let Some(elec_atoms) = self.electric_atoms.as_mut() {
1027 let n_electric = elec_atoms.len();
1028 let perm_electric = Permutation::from_image(
1029 perm.image()[n_last..(n_last + n_electric)]
1030 .iter()
1031 .map(|x| x - n_last)
1032 .collect::<Vec<_>>(),
1033 )?;
1034 permute_inplace(elec_atoms, &perm_electric);
1035 }
1036 Ok(())
1037 }
1038}