qsym2/auxiliary/
molecule.rs

1//! Molecular structures.
2
3use 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// ==================
32// Struct definitions
33// ==================
34
35/// Structure containing the atoms constituting a molecule.
36#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct Molecule {
38    /// The atoms constituting this molecule.
39    pub atoms: Vec<Atom>,
40
41    /// Optional special atom to represent the electric fields applied to this molecule.
42    pub electric_atoms: Option<Vec<Atom>>,
43
44    /// Optional special atoms to represent the magnetic fields applied to this molecule.
45    pub magnetic_atoms: Option<Vec<Atom>>,
46
47    /// A threshold for approximate equality comparisons.
48    pub threshold: f64,
49}
50
51impl Molecule {
52    /// Parses an XYZ file to construct a molecule.
53    ///
54    /// # Arguments
55    ///
56    /// * `filename` - The XYZ file to be parsed.
57    ///
58    /// # Returns
59    ///
60    /// The parsed [`Molecule`] structure.
61    ///
62    /// # Panics
63    ///
64    /// Panics when unable to parse the provided XYZ file.
65    #[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    /// Writes the ordinary atoms in the molecule to an XYZ file.
111    ///
112    /// # Arguments
113    ///
114    /// * `filename` - The XYZ file to be written.
115    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    /// Construct a molecule from an array of atoms.
126    ///
127    /// # Arguments
128    ///
129    /// * `all_atoms` - The atoms (of all types) constituting this molecule.
130    /// * `threshold` - A threshold for approximate equality comparisons.
131    ///
132    /// # Returns
133    ///
134    /// The constructed [`Molecule`] struct.
135    ///
136    /// # Panics
137    ///
138    /// Panics when the numbers of fictitious special atoms, if any, are invalid. It is expected
139    /// that, when present, there are two magnetic special atoms and/or one electric special atom.
140    #[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    /// Constructs a new molecule containing only the ordinary atoms in this molecule.
186    #[must_use]
187    pub fn molecule_ordinary_atoms(&self) -> Self {
188        Self::from_atoms(&self.atoms, self.threshold)
189    }
190
191    /// Constructs a new molecule containing only the fictitious magnetic atoms in this molecule,
192    /// if any.
193    ///
194    /// # Returns
195    ///
196    /// Returns `None` if this molecule has no fictitious magnetic atoms.
197    #[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    /// Constructs a new molecule containing only the fictitious electric atoms in this molecule,
206    /// if any.
207    ///
208    /// # Returns
209    ///
210    /// Returns `None` if this molecule has no fictitious electric atoms.
211    #[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    /// Retrieves a vector of references to all atoms in this molecule, including special ones, if
220    /// any.
221    ///
222    /// # Returns
223    ///
224    /// All atoms in this molecule.
225    #[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    /// Retrieves a vector of mutable references to all atoms in this molecule, including special
245    /// ones, if any.
246    ///
247    /// # Returns
248    ///
249    /// All atoms in this molecule.
250    #[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    /// Calculates the centre of mass of the molecule.
270    ///
271    /// This does not take into account fictitious special atoms.
272    ///
273    /// # Returns
274    ///
275    /// The centre of mass.
276    #[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    /// Calculates the inertia tensor of the molecule.
294    ///
295    /// This *does* take into account fictitious special atoms.
296    ///
297    /// # Arguments
298    ///
299    /// * `origin` - An origin about which the inertia tensor is evaluated.
300    ///
301    /// # Returns
302    ///
303    /// The inertia tensor as a $`3 \times 3`$ matrix.
304    #[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    /// Calculates the moments of inertia and the corresponding principal axes.
334    ///
335    /// This *does* take into account fictitious special atoms.
336    ///
337    /// # Returns
338    ///
339    /// * The moments of inertia in ascending order.
340    /// * The corresponding principal axes.
341    ///
342    /// # Panics
343    ///
344    /// Panics when any of the moments of inertia cannot be compared.
345    #[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    /// Determines the interatomic distance matrix and the indices of symmetry-equivalent atoms.
421    ///
422    /// This *does* take into account fictitious special atoms.
423    ///
424    /// # Returns
425    ///
426    /// * The interatomic distance matrix where the distances in each column are sorted in ascending
427    /// order. Column $`j`$ contains the interatomic distances from atom $`j`$ to all other atoms
428    /// (both ordinary and fictitious) in the molecule. Also note that all atoms (both ordinary and
429    /// fictitious) are included here, so the matrix is square.
430    /// * A vector of vectors of symmetry-equivalent atom indices. Each inner vector contains
431    /// indices of atoms in one SEA group.
432    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        // Determine indices of symmetry-equivalent atoms
439        let mut equiv_indicess: Vec<Vec<usize>> = vec![vec![0]];
440        for (j, coord_j) in all_coords.iter().enumerate() {
441            // column_j is the j-th column in the interatomic distance matrix. This column contains
442            // distances from ordinary atom j to all other atoms (both ordinary and fictitious) in
443            // the molecule.
444            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    /// Determines the sets of symmetry-equivalent atoms.
492    ///
493    /// This *does* take into account fictitious special atoms.
494    ///
495    /// # Returns
496    ///
497    /// * Copies of the atoms in the molecule, grouped into symmetry-equivalent
498    /// groups.
499    ///
500    /// # Panics
501    ///
502    /// Panics when the any of the mass-weighted interatomic distances cannot be compared.
503    #[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    /// Adds two fictitious magnetic atoms to represent the magnetic field.
522    ///
523    /// # Arguments
524    ///
525    /// * `magnetic_field` - The magnetic field vector. If zero or `None`, any magnetic
526    /// field present will be removed.
527    ///
528    /// # Panics
529    ///
530    /// Panics when the number of atoms cannot be represented as an `f64` value.
531    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    /// Adds one fictitious electric atom to represent the electric field.
565    ///
566    /// # Arguments
567    ///
568    /// * `electric_field` - The electric field vector. If zero or `None`, any electric
569    /// field present will be removed.
570    ///
571    /// # Panics
572    ///
573    /// Panics when the number of atoms cannot be represented as an `f64` value.
574    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    /// Clones this molecule and adjusts all comparison thresholds to that specified by `thresh`.
608    ///
609    /// # Arguments
610    ///
611    /// * `thresh` - The new threshold to be assigned to the cloned molecule.
612    ///
613    /// # Returns
614    ///
615    /// A cloned copy of the molecule wit the adjusted threshold.
616    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    /// Reorientates the molecule in-place into a canonical alignment with the space-fixed axes of
628    /// the coordinate system.
629    ///
630    /// Fictitious special atoms are also moved during the reorientation.
631    ///
632    /// If the molecule has a unique principal axis, then this axis becomes aligned with the
633    /// $`z`$-axis and the other two degenerate axes become aligned with the $`x`$- and $`y`$-axes
634    /// of the coordinate system. If the molecule has no unique principal axes, then the axes are
635    /// aligned with $`x`$-, $`y`$-,  and $`z`$-axes in ascending order of moments of inertia.
636    ///
637    /// # Arguments
638    ///
639    /// * `moi_thresh` - Threshold for comparing moments of inertia.
640    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            // principal_axes[0] is unique.
654            Matrix3::from_columns(&[principal_axes[1], principal_axes[2], principal_axes[0]])
655                .transpose()
656        } else {
657            // principal_axes[2] is unique, or no unique axis, or isotropic.
658            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    /// Clones and reorientates the molecule into a canonical alignment with the space-fixed axes
668    /// of the coordinate system.
669    ///
670    /// Fictitious special atoms are also moved during the reorientation.
671    ///
672    /// If the molecule has a unique principal axis, then this axis becomes aligned with the
673    /// $`z`$-axis and the other two degenerate axes become aligned with the $`x`$- and $`y`$-axes
674    /// of the coordinate system. If the molecule has no unique principal axes, then the axes are
675    /// aligned with $`x`$-, $`y`$-,  and $`z`$-axes in ascending order of moments of inertia.
676    ///
677    /// # Arguments
678    ///
679    /// * `moi_thresh` - Threshold for comparing moments of inertia.
680    ///
681    /// # Returns
682    ///
683    /// A reoriented copy of the molecule.
684    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
691// =====================
692// Trait implementations
693// =====================
694
695impl 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    /// Determines the permutation of *all* atoms to map `self` to `other`. Special fictitious
922    /// atoms are included after ordinary atoms, with magnetic atoms before electric atoms.
923    ///
924    /// # Arguments
925    ///
926    /// * `other` - Another molecule to be compared with `self`.
927    ///
928    /// # Returns
929    ///
930    /// Returns a permutation that permutes *all* atoms of `self` to give `other`, or `None` if no
931    /// such permutation exists.
932    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    /// Permutes *all* atoms in this molecule (including special fictitious atoms) and places them
982    /// in a new molecule to be returned.
983    ///
984    /// # Arguments
985    ///
986    /// * `perm` - A permutation for the atoms. Special fictitious atoms are included after
987    /// ordinary atoms, with magnetic atoms before electric atoms.
988    ///
989    /// # Returns
990    ///
991    /// A new molecule with the permuted atoms.
992    ///
993    /// # Panics
994    ///
995    /// Panics if the rank of `perm` does not match the number of atoms in this molecule, or if the
996    /// permutation results in atoms of different kind (*e.g.* ordinary and magnetic) are permuted
997    /// into each other.
998    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    /// Permutes in-place *all* atoms in this molecule (including special fictitious atoms).
1005    ///
1006    /// The in-place rearrangement implementation is taken from
1007    /// [here](https://stackoverflow.com/a/69774341/5112668).
1008    ///
1009    /// # Arguments
1010    ///
1011    /// * `perm` - A permutation for the atoms. Special fictitious atoms are included after
1012    /// ordinary atoms, with magnetic atoms before electric atoms.
1013    ///
1014    /// # Panics
1015    ///
1016    /// Panics if the rank of `perm` does not match the number of atoms in this molecule, or if the
1017    /// permutation results in atoms of different kind (*e.g.* ordinary and magnetic) are permuted
1018    /// into each other.
1019    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}