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 let symbol = if self.aromatic {
136 self.element.symbol().to_lowercase()
137 } else {
138 self.element.symbol().to_string()
139 };
140 match self.isotope {
141 Some(iso) => write!(f, "[{iso}{symbol}]"),
142 None => write!(f, "{symbol}"),
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_atom_new() {
153 let a = Atom::new(Element::C);
154 assert_eq!(a.element, Element::C);
155 assert_eq!(a.charge, 0);
156 assert!(!a.aromatic);
157 assert!(!a.wildcard);
158 assert_eq!(a.hydrogen_count, None);
159 }
160
161 #[test]
162 fn test_aromatic_atom() {
163 let a = Atom::aromatic(Element::C);
164 assert!(a.aromatic);
165 }
166
167 #[test]
168 fn test_wildcard_atom() {
169 let a = Atom::wildcard();
170 assert!(a.wildcard);
171 assert_eq!(format!("{a}"), "*");
172 }
173}