gchemol_core/
atom.rs

1// [[file:../gchemol-core.note::*imports][imports:1]]
2use gut::prelude::*;
3
4use crate::element::*;
5use crate::property::PropertyStore;
6// imports:1 ends here
7
8// [[file:../gchemol-core.note::d14b8035][d14b8035]]
9/// nalgebra 3D Vector
10pub type Vector3f = vecfx::Vector3f;
11
12/// Plain 3D array
13pub type Point3 = [f64; 3];
14
15/// Atom is the smallest particle still characterizing a chemical element.
16///
17/// # Reference
18///
19/// https://goldbook.iupac.org/html/A/A00493.html
20///
21#[derive(Clone, Debug, Deserialize, Serialize)]
22pub struct Atom {
23    /// Arbitrary property stored in key-value pair. Key is a string type, but
24    /// it is the responsibility of the user to correctly interpret the value.
25    pub properties: PropertyStore,
26
27    /// Chemical element or dummy atom.
28    kind: AtomKind,
29
30    /// Atom position.
31    position: Vector3f,
32
33    /// Atom label.
34    label: Option<String>,
35
36    /// Vector quantity equal to the derivative of the position vector with respect to time
37    pub(crate) velocity: Vector3f,
38
39    /// Atomic mass
40    pub(crate) mass: Option<f64>,
41
42    /// Atomic partial charge
43    pub(crate) partial_charge: Option<f64>,
44
45    /// Indicates freezing atom
46    freezing: [bool; 3],
47}
48
49impl Default for Atom {
50    fn default() -> Self {
51        Self {
52            properties: PropertyStore::default(),
53            kind: "C".into(),
54            position: Vector3f::new(0.0, 0.0, 0.0),
55            velocity: Vector3f::new(0.0, 0.0, 0.0),
56            partial_charge: None,
57
58            // FIXME: not so sure these fields are necessary
59            mass: None,
60            label: None,
61            freezing: [false; 3],
62        }
63    }
64}
65
66impl Atom {
67    /// Construct `Atom` from element symbol and Cartesian coordinates.
68    pub fn new<S: Into<AtomKind>, P: Into<Vector3f>>(s: S, p: P) -> Self {
69        Self {
70            kind: s.into(),
71            position: p.into(),
72            ..Default::default()
73        }
74    }
75
76    /// Return element symbol
77    pub fn symbol(&self) -> &str {
78        self.kind.symbol()
79    }
80
81    /// Return atomic number
82    pub fn number(&self) -> usize {
83        self.kind.number()
84    }
85
86    /// Return atom position in 3D Cartesian coordinates
87    pub fn position(&self) -> Point3 {
88        self.position.into()
89    }
90
91    /// Set atom position in 3D Cartesian coordinates
92    pub fn set_position<P: Into<Vector3f>>(&mut self, p: P) {
93        self.position = p.into();
94    }
95
96    /// Return atom kind.
97    pub fn kind(&self) -> &AtomKind {
98        &self.kind
99    }
100
101    /// Set atom label
102    pub fn set_label<S: Into<String>>(&mut self, lbl: S) {
103        self.label = Some(lbl.into());
104    }
105
106    /// Return the user defined atom label, if not return the elment symbol.
107    #[deprecated(note = "use get_label instead")]
108    pub fn label(&self) -> &str {
109        if let Some(ref l) = self.label {
110            return l;
111        }
112
113        // default atom label: element symbol
114        self.symbol()
115    }
116
117    /// Return the user defined atom label if defined.
118    pub fn get_label(&self) -> Option<&str> {
119        self.label.as_deref()
120    }
121
122    /// Set atom symbol.
123    pub fn set_symbol<S: Into<AtomKind>>(&mut self, symbol: S) {
124        self.kind = symbol.into()
125    }
126
127    /// Assign atom partial charge, usually for molecular mechanical
128    /// calculations.
129    pub fn set_partial_charge(&mut self, c: f64) {
130        self.partial_charge = Some(c);
131    }
132
133    /// Return true if atom is dummy.
134    pub fn is_dummy(&self) -> bool {
135        match self.kind {
136            AtomKind::Element(_) => false,
137            AtomKind::Dummy(_) => true,
138        }
139    }
140
141    /// Return true if atom is an element.
142    pub fn is_element(&self) -> bool {
143        !self.is_dummy()
144    }
145
146    /// Return true if atom has freezing masks in all x, y, z coords
147    pub fn is_fixed(&self) -> bool {
148        self.freezing.iter().all(|f| *f)
149    }
150
151    /// Set freezing mask array for Cartesian coordinates
152    pub fn set_freezing(&mut self, freezing: [bool; 3]) {
153        self.freezing = freezing;
154    }
155
156    /// Return freezing mask array for Cartesian coordinates
157    pub fn freezing(&self) -> [bool; 3] {
158        self.freezing
159    }
160
161    /// Update Cartesian coordinates partially, without changing its freezing
162    /// coordinate components (xyz).
163    pub fn update_position<P: Into<Vector3f>>(&mut self, p: P) {
164        // refuse to update position of freezing atom
165        let new_position: Vector3f = p.into();
166        for (i, masked) in self.freezing.iter().enumerate() {
167            if !*masked {
168                self.position[i] = new_position[i];
169            }
170        }
171    }
172}
173// d14b8035 ends here
174
175// [[file:../gchemol-core.note::fa2e494e][fa2e494e]]
176use std::convert::From;
177use std::str::FromStr;
178
179impl FromStr for Atom {
180    type Err = Error;
181
182    fn from_str(line: &str) -> Result<Self> {
183        let parts: Vec<_> = line.split_whitespace().collect();
184        let nparts = parts.len();
185        ensure!(nparts >= 4, "Incorrect number of data fields: {line:?}");
186        let sym = parts[0];
187        let px: f64 = parts[1].parse()?;
188        let py: f64 = parts[2].parse()?;
189        let pz: f64 = parts[3].parse()?;
190
191        let mut atom = Atom::new(sym, [px, py, pz]);
192        // HACK: parse velocities
193        if nparts >= 6 {
194            let vxyz: Vec<_> = parts[4..7].iter().filter_map(|x| x.parse().ok()).collect();
195            if vxyz.len() == 3 {
196                atom.velocity = [vxyz[0], vxyz[1], vxyz[2]].into();
197            }
198        }
199
200        Ok(atom)
201    }
202}
203
204impl std::fmt::Display for Atom {
205    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
206        write!(
207            f,
208            "{:6} {:-12.6} {:-12.6} {:-12.6}",
209            self.symbol(),
210            self.position[0],
211            self.position[1],
212            self.position[2]
213        )
214    }
215}
216
217impl<S, P> From<(S, P)> for Atom
218where
219    S: Into<AtomKind>,
220    P: Into<Vector3f>,
221{
222    fn from(item: (S, P)) -> Self {
223        Self::new(item.0, item.1)
224    }
225}
226// fa2e494e ends here
227
228// [[file:../gchemol-core.note::a09f666f][a09f666f]]
229#[test]
230fn test_atom_basic() {
231    let _ = Atom::default();
232    let atom = Atom::new("Fe", [9.3; 3]);
233    assert_eq!(9.3, atom.position()[0]);
234    assert_eq!("Fe", atom.symbol());
235    assert_eq!(26, atom.number());
236
237    let mut atom = Atom::new(6, [1.0, 0.0, 0.0]);
238    assert_eq!(atom.symbol(), "C");
239    atom.set_symbol("H");
240    assert_eq!(atom.symbol(), "H");
241
242    let atom = Atom::new("X", [9.3; 3]);
243    assert_eq!("X", atom.symbol());
244    assert_eq!(0, atom.number());
245}
246
247#[test]
248fn test_atom_convert() {
249    let line = "H 1.0 1.0 1.0";
250    let a: Atom = line.parse().unwrap();
251    let line = a.to_string();
252    let b: Atom = line.parse().unwrap();
253    assert_eq!(a.symbol(), b.symbol());
254    assert_eq!(a.position(), b.position());
255
256    // from and into
257    let a: Atom = (1, [0.0; 3]).into();
258    assert_eq!(a.number(), 1);
259
260    // velocity
261    let line = "H 1.0 1.0 1.0 2.0 0.0 0";
262    let a: Atom = line.parse().unwrap();
263    assert_eq!(a.velocity.x, 2.0);
264    assert_eq!(a.velocity.y, 0.0);
265    assert_eq!(a.velocity.z, 0.0);
266}
267// a09f666f ends here