Skip to main content

chematic_core/
atom.rs

1//! Atom type: a single atom in a molecule.
2
3use crate::element::Element;
4
5/// Tetrahedral chirality as specified in OpenSMILES.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
7pub enum Chirality {
8    /// No chirality specified.
9    #[default]
10    None,
11    /// `@` — counterclockwise (looking from the first neighbor).
12    CounterClockwise,
13    /// `@@` — clockwise.
14    Clockwise,
15}
16
17/// Assigned CIP (Cahn–Ingold–Prelog) stereodescriptor.
18///
19/// Stored on [`Atom`] after running [`chematic_chem::assign_cip`] or
20/// [`chematic_chem::cip::assign_cip`].
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum CipCode {
23    /// Tetrahedral center with *rectus* (right-handed) configuration.
24    R,
25    /// Tetrahedral center with *sinister* (left-handed) configuration.
26    S,
27    /// Double-bond *entgegen* (opposite, trans) geometry.
28    E,
29    /// Double-bond *zusammen* (together, cis) geometry.
30    Z,
31}
32
33/// A single atom in a molecular graph.
34///
35/// - `isotope`: mass number (e.g. 13 for ¹³C). `None` = natural isotope abundance.
36/// - `charge`: formal charge.
37/// - `hydrogen_count`: explicit H count from a bracket atom `[...]`.
38///   `None` for organic-subset atoms whose H count is inferred from valence.
39/// - `aromatic`: set when the atom is written as a lowercase letter (c, n, …)
40///   or connected via `:` bonds.
41/// - `wildcard`: `true` for the SMILES `*` atom (any element, query context).
42/// - `atom_map`: atom-mapping number used in reaction SMILES.
43/// - `cip_code`: CIP stereodescriptor (R/S/E/Z). Populated by
44///   `chematic_chem::assign_cip`; `None` until then.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Atom {
47    pub element: Element,
48    pub isotope: Option<u16>,
49    pub charge: i8,
50    /// Explicit H count (bracket atoms only). `None` for organic-subset atoms.
51    pub hydrogen_count: Option<u8>,
52    pub aromatic: bool,
53    pub chirality: Chirality,
54    /// True for the wildcard atom `*` or `[*]`.
55    pub wildcard: bool,
56    pub atom_map: Option<u16>,
57    /// CIP stereodescriptor assigned by `chematic_chem::assign_cip`.
58    /// `None` until explicitly computed.
59    pub cip_code: Option<CipCode>,
60}
61
62impl Atom {
63    /// Create a plain, neutral, non-aromatic atom.
64    pub fn new(element: Element) -> Self {
65        Self {
66            element,
67            isotope: None,
68            charge: 0,
69            hydrogen_count: None,
70            aromatic: false,
71            chirality: Chirality::None,
72            wildcard: false,
73            atom_map: None,
74            cip_code: None,
75        }
76    }
77
78    /// Organic-subset atom (charge=0, non-aromatic, implicit H from valence).
79    pub fn organic(element: Element) -> Self {
80        Self::new(element)
81    }
82
83    /// Aromatic organic atom (lowercase SMILES notation).
84    pub fn aromatic(element: Element) -> Self {
85        Self {
86            aromatic: true,
87            ..Self::new(element)
88        }
89    }
90
91    /// Bracket atom with explicit properties.
92    pub fn bracket(
93        element: Element,
94        isotope: Option<u16>,
95        chirality: Chirality,
96        hydrogen_count: u8,
97        charge: i8,
98        atom_map: Option<u16>,
99    ) -> Self {
100        Self {
101            element,
102            isotope,
103            charge,
104            hydrogen_count: Some(hydrogen_count),
105            aromatic: false,
106            chirality,
107            wildcard: false,
108            atom_map,
109            cip_code: None,
110        }
111    }
112
113    /// Wildcard atom `*` / `[*]` (matches any element in query contexts).
114    pub fn wildcard() -> Self {
115        Self {
116            // Element is a placeholder; callers should check `wildcard` first.
117            element: Element::C,
118            wildcard: true,
119            hydrogen_count: Some(0),
120            ..Self::new(Element::C)
121        }
122    }
123
124    /// Return the explicit H count for bracket atoms; `None` for organic-subset atoms.
125    pub fn explicit_hcount(&self) -> Option<u8> {
126        self.hydrogen_count
127    }
128}
129
130impl core::fmt::Display for Atom {
131    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
132        if self.wildcard {
133            return write!(f, "*");
134        }
135        if self.isotope.is_some() {
136            write!(f, "[{}{}]",
137                self.isotope.unwrap(),
138                if self.aromatic { self.element.symbol().to_lowercase() }
139                else { self.element.symbol().to_string() })
140        } else if self.aromatic {
141            write!(f, "{}", self.element.symbol().to_lowercase())
142        } else {
143            write!(f, "{}", self.element.symbol())
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_atom_new() {
154        let a = Atom::new(Element::C);
155        assert_eq!(a.element, Element::C);
156        assert_eq!(a.charge, 0);
157        assert!(!a.aromatic);
158        assert!(!a.wildcard);
159        assert_eq!(a.hydrogen_count, None);
160    }
161
162    #[test]
163    fn test_aromatic_atom() {
164        let a = Atom::aromatic(Element::C);
165        assert!(a.aromatic);
166    }
167
168    #[test]
169    fn test_wildcard_atom() {
170        let a = Atom::wildcard();
171        assert!(a.wildcard);
172        assert_eq!(format!("{a}"), "*");
173    }
174}