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}