Skip to main content

molprint_core/mol/
atom.rs

1use std::fmt;
2
3/// Chemical elements relevant for drug-like molecules.
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5pub enum Element {
6    H,
7    He,
8    Li,
9    Be,
10    B,
11    C,
12    N,
13    O,
14    F,
15    Ne,
16    Na,
17    Mg,
18    Al,
19    Si,
20    P,
21    S,
22    Cl,
23    Ar,
24    K,
25    Ca,
26    Ti,
27    V,
28    Cr,
29    Mn,
30    Fe,
31    Co,
32    Ni,
33    Cu,
34    Zn,
35    Ga,
36    Ge,
37    As,
38    Se,
39    Br,
40    Kr,
41    Rb,
42    Sr,
43    Zr,
44    Mo,
45    Ru,
46    Rh,
47    Pd,
48    Ag,
49    Cd,
50    In,
51    Sn,
52    Sb,
53    Te,
54    I,
55    Xe,
56    Cs,
57    Ba,
58    Pt,
59    Au,
60    Hg,
61    Tl,
62    Pb,
63    Bi,
64    Unknown,
65}
66
67impl Element {
68    /// Parse from element symbol string (case-sensitive: "C", "Cl", "Br").
69    pub fn from_symbol(s: &str) -> Option<Self> {
70        match s {
71            "H" => Some(Element::H),
72            "He" => Some(Element::He),
73            "Li" => Some(Element::Li),
74            "Be" => Some(Element::Be),
75            "B" => Some(Element::B),
76            "C" => Some(Element::C),
77            "N" => Some(Element::N),
78            "O" => Some(Element::O),
79            "F" => Some(Element::F),
80            "Ne" => Some(Element::Ne),
81            "Na" => Some(Element::Na),
82            "Mg" => Some(Element::Mg),
83            "Al" => Some(Element::Al),
84            "Si" => Some(Element::Si),
85            "P" => Some(Element::P),
86            "S" => Some(Element::S),
87            "Cl" => Some(Element::Cl),
88            "Ar" => Some(Element::Ar),
89            "K" => Some(Element::K),
90            "Ca" => Some(Element::Ca),
91            "Ti" => Some(Element::Ti),
92            "V" => Some(Element::V),
93            "Cr" => Some(Element::Cr),
94            "Mn" => Some(Element::Mn),
95            "Fe" => Some(Element::Fe),
96            "Co" => Some(Element::Co),
97            "Ni" => Some(Element::Ni),
98            "Cu" => Some(Element::Cu),
99            "Zn" => Some(Element::Zn),
100            "Ga" => Some(Element::Ga),
101            "Ge" => Some(Element::Ge),
102            "As" => Some(Element::As),
103            "Se" => Some(Element::Se),
104            "Br" => Some(Element::Br),
105            "Kr" => Some(Element::Kr),
106            "Rb" => Some(Element::Rb),
107            "Sr" => Some(Element::Sr),
108            "Zr" => Some(Element::Zr),
109            "Mo" => Some(Element::Mo),
110            "Ru" => Some(Element::Ru),
111            "Rh" => Some(Element::Rh),
112            "Pd" => Some(Element::Pd),
113            "Ag" => Some(Element::Ag),
114            "Cd" => Some(Element::Cd),
115            "In" => Some(Element::In),
116            "Sn" => Some(Element::Sn),
117            "Sb" => Some(Element::Sb),
118            "Te" => Some(Element::Te),
119            "I" => Some(Element::I),
120            "Xe" => Some(Element::Xe),
121            "Cs" => Some(Element::Cs),
122            "Ba" => Some(Element::Ba),
123            "Pt" => Some(Element::Pt),
124            "Au" => Some(Element::Au),
125            "Hg" => Some(Element::Hg),
126            "Tl" => Some(Element::Tl),
127            "Pb" => Some(Element::Pb),
128            "Bi" => Some(Element::Bi),
129            _ => None,
130        }
131    }
132
133    /// Atomic number.
134    pub fn atomic_number(&self) -> u8 {
135        match self {
136            Element::H => 1,
137            Element::He => 2,
138            Element::Li => 3,
139            Element::Be => 4,
140            Element::B => 5,
141            Element::C => 6,
142            Element::N => 7,
143            Element::O => 8,
144            Element::F => 9,
145            Element::Ne => 10,
146            Element::Na => 11,
147            Element::Mg => 12,
148            Element::Al => 13,
149            Element::Si => 14,
150            Element::P => 15,
151            Element::S => 16,
152            Element::Cl => 17,
153            Element::Ar => 18,
154            Element::K => 19,
155            Element::Ca => 20,
156            Element::Ti => 22,
157            Element::V => 23,
158            Element::Cr => 24,
159            Element::Mn => 25,
160            Element::Fe => 26,
161            Element::Co => 27,
162            Element::Ni => 28,
163            Element::Cu => 29,
164            Element::Zn => 30,
165            Element::Ga => 31,
166            Element::Ge => 32,
167            Element::As => 33,
168            Element::Se => 34,
169            Element::Br => 35,
170            Element::Kr => 36,
171            Element::Rb => 37,
172            Element::Sr => 38,
173            Element::Zr => 40,
174            Element::Mo => 42,
175            Element::Ru => 44,
176            Element::Rh => 45,
177            Element::Pd => 46,
178            Element::Ag => 47,
179            Element::Cd => 48,
180            Element::In => 49,
181            Element::Sn => 50,
182            Element::Sb => 51,
183            Element::Te => 52,
184            Element::I => 53,
185            Element::Xe => 54,
186            Element::Cs => 55,
187            Element::Ba => 56,
188            Element::Pt => 78,
189            Element::Au => 79,
190            Element::Hg => 80,
191            Element::Tl => 81,
192            Element::Pb => 82,
193            Element::Bi => 83,
194            Element::Unknown => 0,
195        }
196    }
197
198    /// Symbol string.
199    pub fn symbol(&self) -> &'static str {
200        match self {
201            Element::H => "H",
202            Element::He => "He",
203            Element::Li => "Li",
204            Element::Be => "Be",
205            Element::B => "B",
206            Element::C => "C",
207            Element::N => "N",
208            Element::O => "O",
209            Element::F => "F",
210            Element::Ne => "Ne",
211            Element::Na => "Na",
212            Element::Mg => "Mg",
213            Element::Al => "Al",
214            Element::Si => "Si",
215            Element::P => "P",
216            Element::S => "S",
217            Element::Cl => "Cl",
218            Element::Ar => "Ar",
219            Element::K => "K",
220            Element::Ca => "Ca",
221            Element::Ti => "Ti",
222            Element::V => "V",
223            Element::Cr => "Cr",
224            Element::Mn => "Mn",
225            Element::Fe => "Fe",
226            Element::Co => "Co",
227            Element::Ni => "Ni",
228            Element::Cu => "Cu",
229            Element::Zn => "Zn",
230            Element::Ga => "Ga",
231            Element::Ge => "Ge",
232            Element::As => "As",
233            Element::Se => "Se",
234            Element::Br => "Br",
235            Element::Kr => "Kr",
236            Element::Rb => "Rb",
237            Element::Sr => "Sr",
238            Element::Zr => "Zr",
239            Element::Mo => "Mo",
240            Element::Ru => "Ru",
241            Element::Rh => "Rh",
242            Element::Pd => "Pd",
243            Element::Ag => "Ag",
244            Element::Cd => "Cd",
245            Element::In => "In",
246            Element::Sn => "Sn",
247            Element::Sb => "Sb",
248            Element::Te => "Te",
249            Element::I => "I",
250            Element::Xe => "Xe",
251            Element::Cs => "Cs",
252            Element::Ba => "Ba",
253            Element::Pt => "Pt",
254            Element::Au => "Au",
255            Element::Hg => "Hg",
256            Element::Tl => "Tl",
257            Element::Pb => "Pb",
258            Element::Bi => "Bi",
259            Element::Unknown => "*",
260        }
261    }
262
263    /// Default valence(s) for implicit hydrogen calculation.
264    pub fn default_valences(&self) -> &'static [u8] {
265        match self {
266            Element::B => &[3],
267            Element::C => &[4],
268            Element::N => &[3, 5],
269            Element::O => &[2],
270            Element::P => &[3, 5],
271            Element::S => &[2, 4, 6],
272            Element::F => &[1],
273            Element::Cl => &[1, 3, 5, 7],
274            Element::Br => &[1, 3, 5, 7],
275            Element::I => &[1, 3, 5, 7],
276            Element::Si => &[4],
277            Element::Al => &[3],
278            Element::Sn => &[2, 4],
279            Element::Pb => &[2, 4],
280            _ => &[0],
281        }
282    }
283
284    /// Is this an organic subset atom?
285    pub fn is_organic_subset(&self) -> bool {
286        matches!(
287            self,
288            Element::B
289                | Element::C
290                | Element::N
291                | Element::O
292                | Element::P
293                | Element::S
294                | Element::F
295                | Element::Cl
296                | Element::Br
297                | Element::I
298        )
299    }
300}
301
302impl fmt::Display for Element {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        write!(f, "{}", self.symbol())
305    }
306}
307
308/// A single atom in a molecular graph.
309#[derive(Clone, Debug)]
310pub struct Atom {
311    pub element: Element,
312    pub charge: i8,
313    pub isotope: Option<u16>,
314    /// Total H count (explicit + implicit); assigned after graph construction.
315    pub h_count: u8,
316    /// Explicit H count from bracket atom syntax, e.g. `[CH3]` → Some(3).
317    /// None means "no bracket H specification" (compute implicitly).
318    pub explicit_h: Option<u8>,
319    pub aromatic: bool,
320    pub in_ring: bool,
321    pub ring_sizes: smallvec::SmallVec<[u8; 4]>,
322    pub map_num: Option<u16>,
323}
324
325impl Atom {
326    /// Create a new atom with default properties (neutral, non-aromatic, no ring membership).
327    pub fn new(element: Element) -> Self {
328        Self {
329            element,
330            charge: 0,
331            isotope: None,
332            h_count: 0,
333            explicit_h: None,
334            aromatic: false,
335            in_ring: false,
336            ring_sizes: smallvec::SmallVec::new(),
337            map_num: None,
338        }
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_from_symbol() {
348        assert_eq!(Element::from_symbol("C"), Some(Element::C));
349        assert_eq!(Element::from_symbol("Cl"), Some(Element::Cl));
350        assert_eq!(Element::from_symbol("Br"), Some(Element::Br));
351        assert_eq!(Element::from_symbol("Xx"), None);
352    }
353
354    #[test]
355    fn test_default_valences() {
356        assert_eq!(Element::C.default_valences(), &[4]);
357        assert_eq!(Element::N.default_valences(), &[3, 5]);
358        assert_eq!(Element::S.default_valences(), &[2, 4, 6]);
359    }
360
361    #[test]
362    fn test_is_organic_subset() {
363        assert!(Element::C.is_organic_subset());
364        assert!(!Element::Fe.is_organic_subset());
365    }
366
367    #[test]
368    fn test_atomic_number() {
369        assert_eq!(Element::C.atomic_number(), 6);
370        assert_eq!(Element::N.atomic_number(), 7);
371        assert_eq!(Element::Fe.atomic_number(), 26);
372    }
373}