qsym2/auxiliary/
atom.rs

1//! Atoms and chemical elements.
2
3use std::collections::HashMap;
4use std::fmt;
5use std::hash::{Hash, Hasher};
6
7use approx;
8use nalgebra::{Matrix3, Point3, Rotation3, Translation3, UnitVector3, Vector3};
9use num_traits::ToPrimitive;
10use periodic_table;
11use serde::{Deserialize, Serialize};
12
13use crate::auxiliary::geometry::{self, ImproperRotationKind, Transform};
14use crate::auxiliary::misc::{self, HashableFloat};
15
16// https://physics.nist.gov/cgi-bin/cuu/Value?bohrrada0
17/// Constant for converting $`a_0`$ to $`Å`$.
18#[allow(dead_code)]
19pub(crate) const BOHR_TO_ANGSTROM: f64 = 0.529177210903;
20
21/// Constant for converting $`Å`$ to $`a_0`$.
22pub(crate) const ANGSTROM_TO_BOHR: f64 = 1.889726124626;
23
24/// Structure storing a look-up of element symbols to give atomic numbers
25/// and atomic masses.
26pub struct ElementMap<'a> {
27    /// A [`HashMap`] from a symbol string to a tuple of atomic number and atomic mass.
28    map: HashMap<&'a str, (u32, f64)>,
29}
30
31impl Default for ElementMap<'static> {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl ElementMap<'static> {
38    /// Creates a new [`ElementMap`] for all elements in the periodic table.
39    #[must_use]
40    pub fn new() -> ElementMap<'static> {
41        let elements = periodic_table::periodic_table();
42        let map = elements
43            .iter()
44            .map(|element| {
45                let mass = parse_atomic_mass(element.atomic_mass);
46                (element.symbol, (element.atomic_number, mass))
47            })
48            .collect::<HashMap<_, _>>();
49        ElementMap { map }
50    }
51
52    /// Returns a tuple of atomic number and atomic mass for the requested element.
53    pub fn get(&self, element: &str) -> Option<&(u32, f64)> {
54        self.map.get(element)
55    }
56}
57
58/// Parses the atomic mass string in the format of [`periodic_table`] to a single float value.
59///
60/// # Arguments
61///
62/// * `mass_str` - A string of mass value that is either `x.y(z)` where the
63///   uncertain digit `z` is enclosed in parentheses, or `[x]` where `x`
64///   is the mass number in place of precise experimental values.
65///
66/// # Returns
67///
68/// The numeric mass value.
69fn parse_atomic_mass(mass_str: &str) -> f64 {
70    let mass = mass_str.replace(&['(', ')', '[', ']'][..], "");
71    mass.parse::<f64>()
72        .unwrap_or_else(|_| panic!("Unable to parse atomic mass string {mass}."))
73}
74
75/// Structure representing an atom.
76#[derive(Clone, Serialize, Deserialize)]
77pub struct Atom {
78    /// The atom kind.
79    pub kind: AtomKind,
80
81    /// The atomic number of the atom.
82    pub atomic_number: u32,
83
84    /// The atomic symbol of the atom.
85    pub atomic_symbol: String,
86
87    /// The weighted-average atomic mass for all naturally occuring isotopes.
88    pub atomic_mass: f64,
89
90    /// The position of the atom.
91    pub coordinates: Point3<f64>,
92
93    /// A threshold for approximate equality comparisons.
94    pub threshold: f64,
95}
96
97impl Atom {
98    /// Parses an atom line in an `xyz` file to construct an [`Atom`].
99    ///
100    /// # Arguments
101    ///
102    /// * `line` - A line in an `xyz` file containing an atomic symbol and
103    ///   three Cartesian coordinates.
104    /// * `emap` - A hash map between atomic symbols and atomic numbers and
105    ///   masses.
106    /// * `thresh` - A threshold for approximate equality comparisons.
107    ///
108    /// # Returns
109    ///
110    /// The parsed [`Atom`] struct if the line has the correct format,
111    /// otherwise [`None`].
112    #[must_use]
113    pub(crate) fn from_xyz(line: &str, emap: &ElementMap, thresh: f64) -> Option<Atom> {
114        let split: Vec<&str> = line.split_whitespace().collect();
115        if split.len() != 4 {
116            return None;
117        };
118        let atomic_symbol = split.first().expect("Unable to get the element symbol.");
119        let (atomic_number, atomic_mass) = emap
120            .map
121            .get(atomic_symbol)
122            .expect("Invalid atomic symbol encountered.");
123        let coordinates = Point3::new(
124            split
125                .get(1)
126                .expect("Unable to get the x coordinate.")
127                .parse::<f64>()
128                .expect("Unable to parse the x coordinate."),
129            split
130                .get(2)
131                .expect("Unable to get the y coordinate.")
132                .parse::<f64>()
133                .expect("Unable to parse the y coordinate."),
134            split
135                .get(3)
136                .expect("Unable to get the z coordinate.")
137                .parse::<f64>()
138                .expect("Unable to parse the z coordinate."),
139        );
140        let atom = Atom {
141            kind: AtomKind::Ordinary,
142            atomic_number: *atomic_number,
143            atomic_symbol: (*atomic_symbol).to_string(),
144            atomic_mass: *atomic_mass,
145            coordinates,
146            threshold: thresh,
147        };
148        Some(atom)
149    }
150
151    /// Writes the atom to an XYZ line.
152    #[must_use]
153    pub(crate) fn to_xyz(&self) -> String {
154        let precision = self.precision();
155        let length = (precision + precision.div_euclid(2)).max(6);
156        if let AtomKind::Ordinary = self.kind {
157            format!(
158                "{:<3} {:+length$.precision$} {:+length$.precision$} {:+length$.precision$}",
159                self.atomic_symbol, self.coordinates[0], self.coordinates[1], self.coordinates[2],
160            )
161        } else {
162            String::new()
163        }
164    }
165
166    /// Creates an ordinary atom.
167    ///
168    /// # Arguments
169    ///
170    /// * `atomic_symbol` - The element symbol of the atom.
171    /// * `coordinates` - The coordinates of the atom.
172    /// * `emap` - A hash map between atomic symbols and atomic numbers and
173    ///   masses.
174    /// * `thresh` - The threshold for comparing atoms.
175    ///
176    /// # Returns
177    ///
178    /// The required ordinary atom.
179    #[must_use]
180    pub fn new_ordinary(
181        atomic_symbol: &str,
182        coordinates: Point3<f64>,
183        emap: &ElementMap,
184        thresh: f64,
185    ) -> Atom {
186        let (atomic_number, atomic_mass) = emap
187            .map
188            .get(atomic_symbol)
189            .expect("Invalid atomic symbol encountered.");
190        Atom {
191            kind: AtomKind::Ordinary,
192            atomic_number: *atomic_number,
193            atomic_symbol: atomic_symbol.to_string(),
194            atomic_mass: *atomic_mass,
195            coordinates,
196            threshold: thresh,
197        }
198    }
199
200    /// Creates a special atom.
201    ///
202    /// # Arguments
203    ///
204    /// * `kind` - The required special kind.
205    /// * `coordinates` - The coordinates of the special atom.
206    /// * `thresh` - The threshold for comparing atoms.
207    ///
208    /// # Returns
209    ///
210    /// `None` if `kind` is not one of the special atom kinds, `Some(Atom)`
211    /// otherwise.
212    #[must_use]
213    pub fn new_special(kind: AtomKind, coordinates: Point3<f64>, thresh: f64) -> Option<Atom> {
214        match kind {
215            AtomKind::Magnetic(_) | AtomKind::Electric(_) => Some(Atom {
216                kind,
217                atomic_number: 0,
218                atomic_symbol: String::new(),
219                atomic_mass: 100.0,
220                coordinates,
221                threshold: thresh,
222            }),
223            AtomKind::Ordinary => None,
224        }
225    }
226
227    /// Computes the precision for printing out atom coordinates in a manner compatible with the
228    /// comparison threshold of the atom.
229    ///
230    /// # Returns
231    ///
232    /// The print-out precision.
233    fn precision(&self) -> usize {
234        self.threshold.log10().abs().round().to_usize().unwrap_or(7) + 1
235    }
236}
237
238impl fmt::Display for Atom {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        let precision = self.precision();
241        let length = (precision + precision.div_euclid(2)).max(6);
242        if let AtomKind::Ordinary = self.kind {
243            write!(
244                f,
245                "{:>9} {:>3} {:+length$.precision$} {:+length$.precision$} {:+length$.precision$}",
246                "Atom",
247                self.atomic_symbol,
248                self.coordinates[0],
249                self.coordinates[1],
250                self.coordinates[2],
251            )
252        } else {
253            write!(
254                f,
255                "{:>13} {:+length$.precision$} {:+length$.precision$} {:+length$.precision$}",
256                self.kind, self.coordinates[0], self.coordinates[1], self.coordinates[2],
257            )
258        }
259    }
260}
261
262impl fmt::Debug for Atom {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        write!(f, "{self}")
265    }
266}
267
268/// Enumerated type describing the atom kind.
269#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
270pub enum AtomKind {
271    /// An ordinary atom.
272    Ordinary,
273
274    /// A fictitious atom representing a magnetic field. This variant contains
275    /// a flag to indicate if the fictitious atom is of positive type or not.
276    Magnetic(bool),
277
278    /// A fictitious atom representing an electric field. This variant contains
279    /// a flag to indicate if the fictitious atom is of positive type or not.
280    Electric(bool),
281}
282
283impl fmt::Display for AtomKind {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        match self {
286            Self::Ordinary => write!(f, "{}", "Atom".to_owned()),
287            Self::Magnetic(pos) => {
288                if *pos {
289                    write!(f, "{}", "MagneticAtom+".to_owned())
290                } else {
291                    write!(f, "{}", "MagneticAtom-".to_owned())
292                }
293            }
294            Self::Electric(pos) => {
295                if *pos {
296                    write!(f, "{}", "ElectricAtom+".to_owned())
297                } else {
298                    write!(f, "{}", "ElectricAtom-".to_owned())
299                }
300            }
301        }
302    }
303}
304
305impl Transform for Atom {
306    fn transform_mut(&mut self, mat: &Matrix3<f64>) {
307        let det = mat.determinant();
308        assert!(
309            approx::relative_eq!(
310                det,
311                1.0,
312                epsilon = self.threshold,
313                max_relative = self.threshold
314            ) || approx::relative_eq!(
315                det,
316                -1.0,
317                epsilon = self.threshold,
318                max_relative = self.threshold
319            )
320        );
321        self.coordinates = mat * self.coordinates;
322        if approx::relative_eq!(
323            det,
324            -1.0,
325            epsilon = self.threshold,
326            max_relative = self.threshold
327        ) && let AtomKind::Magnetic(pos) = self.kind
328        {
329            self.kind = AtomKind::Magnetic(!pos);
330        };
331    }
332
333    fn rotate_mut(&mut self, angle: f64, axis: &Vector3<f64>) {
334        let normalised_axis = UnitVector3::new_normalize(*axis);
335        let rotation = Rotation3::from_axis_angle(&normalised_axis, angle);
336        self.coordinates = rotation.transform_point(&self.coordinates);
337    }
338
339    fn improper_rotate_mut(
340        &mut self,
341        angle: f64,
342        axis: &Vector3<f64>,
343        kind: &ImproperRotationKind,
344    ) {
345        let mat = geometry::improper_rotation_matrix(angle, axis, 1, kind);
346        self.transform_mut(&mat);
347    }
348
349    fn translate_mut(&mut self, tvec: &Vector3<f64>) {
350        let translation = Translation3::from(*tvec);
351        self.coordinates = translation.transform_point(&self.coordinates);
352    }
353
354    fn recentre_mut(&mut self) {
355        self.coordinates = Point3::origin();
356    }
357
358    fn reverse_time_mut(&mut self) {
359        if let AtomKind::Magnetic(polarity) = self.kind {
360            self.kind = AtomKind::Magnetic(!polarity);
361        }
362    }
363
364    fn transform(&self, mat: &Matrix3<f64>) -> Self {
365        let mut transformed_atom = self.clone();
366        transformed_atom.transform_mut(mat);
367        transformed_atom
368    }
369
370    fn rotate(&self, angle: f64, axis: &Vector3<f64>) -> Self {
371        let mut rotated_atom = self.clone();
372        rotated_atom.rotate_mut(angle, axis);
373        rotated_atom
374    }
375
376    fn improper_rotate(
377        &self,
378        angle: f64,
379        axis: &Vector3<f64>,
380        kind: &ImproperRotationKind,
381    ) -> Self {
382        let mut improper_rotated_atom = self.clone();
383        improper_rotated_atom.improper_rotate_mut(angle, axis, kind);
384        improper_rotated_atom
385    }
386
387    fn translate(&self, tvec: &Vector3<f64>) -> Self {
388        let mut translated_atom = self.clone();
389        translated_atom.translate_mut(tvec);
390        translated_atom
391    }
392
393    fn recentre(&self) -> Self {
394        let mut recentred_atom = self.clone();
395        recentred_atom.recentre_mut();
396        recentred_atom
397    }
398
399    fn reverse_time(&self) -> Self {
400        let mut time_reversed_atom = self.clone();
401        time_reversed_atom.reverse_time_mut();
402        time_reversed_atom
403    }
404}
405
406impl PartialEq for Atom {
407    /// The `[Self::threshold]` value for each atom defines a discrete grid over
408    /// the real number field. All real numbers (*e.g.* atomic mass, coordinates)
409    /// are rounded to take on the values on this discrete grid which are then
410    /// used for hashing and comparisons.
411    fn eq(&self, other: &Self) -> bool {
412        let result = self.atomic_number == other.atomic_number
413            && self.kind == other.kind
414            && approx::relative_eq!(
415                self.atomic_mass.round_factor(self.threshold),
416                other.atomic_mass.round_factor(other.threshold),
417            )
418            && approx::relative_eq!(
419                self.coordinates[0].round_factor(self.threshold),
420                other.coordinates[0].round_factor(other.threshold),
421            )
422            && approx::relative_eq!(
423                self.coordinates[1].round_factor(self.threshold),
424                other.coordinates[1].round_factor(other.threshold),
425            )
426            && approx::relative_eq!(
427                self.coordinates[2].round_factor(self.threshold),
428                other.coordinates[2].round_factor(other.threshold),
429            );
430        // if result {
431        //     assert_eq!(misc::calculate_hash(self), misc::calculate_hash(other));
432        // }
433        result && (misc::calculate_hash(self) == misc::calculate_hash(other))
434    }
435}
436
437impl Eq for Atom {}
438
439impl Hash for Atom {
440    fn hash<H: Hasher>(&self, state: &mut H) {
441        self.atomic_number.hash(state);
442        self.kind.hash(state);
443        self.atomic_mass
444            .round_factor(self.threshold)
445            .integer_decode()
446            .hash(state);
447        self.coordinates[0]
448            .round_factor(self.threshold)
449            .integer_decode()
450            .hash(state);
451        self.coordinates[1]
452            .round_factor(self.threshold)
453            .integer_decode()
454            .hash(state);
455        self.coordinates[2]
456            .round_factor(self.threshold)
457            .integer_decode()
458            .hash(state);
459    }
460}