1use 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#[allow(dead_code)]
19pub(crate) const BOHR_TO_ANGSTROM: f64 = 0.529177210903;
20
21pub(crate) const ANGSTROM_TO_BOHR: f64 = 1.889726124626;
23
24pub struct ElementMap<'a> {
27 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 #[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 pub fn get(&self, element: &str) -> Option<&(u32, f64)> {
54 self.map.get(element)
55 }
56}
57
58fn 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#[derive(Clone, Serialize, Deserialize)]
77pub struct Atom {
78 pub kind: AtomKind,
80
81 pub atomic_number: u32,
83
84 pub atomic_symbol: String,
86
87 pub atomic_mass: f64,
89
90 pub coordinates: Point3<f64>,
92
93 pub threshold: f64,
95}
96
97impl Atom {
98 #[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 #[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 #[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 #[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 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#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
270pub enum AtomKind {
271 Ordinary,
273
274 Magnetic(bool),
277
278 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 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 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}