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 /// Pseudoasymmetric center, *rectus*-like (Rule 5, lowercase `r`). Emitted only by
32 /// `chematic_cip::assign_cip_accurate_experimental`'s Rule 5 pass; the default
33 /// `chematic_chem::assign_cip` never produces this variant.
34 LowerR,
35 /// Pseudoasymmetric center, *sinister*-like (Rule 5, lowercase `s`). See [`Self::LowerR`].
36 LowerS,
37}
38
39/// A single atom in a molecular graph.
40///
41/// - `isotope`: mass number (e.g. 13 for ¹³C). `None` = natural isotope abundance.
42/// - `charge`: formal charge.
43/// - `hydrogen_count`: explicit H count from a bracket atom `[...]`.
44/// `None` for organic-subset atoms whose H count is inferred from valence.
45/// - `aromatic`: set when the atom is written as a lowercase letter (c, n, …)
46/// or connected via `:` bonds.
47/// - `wildcard`: `true` for the SMILES `*` atom (any element, query context).
48/// - `atom_map`: atom-mapping number used in reaction SMILES.
49/// - `cip_code`: CIP stereodescriptor (R/S/E/Z). Populated by
50/// `chematic_chem::assign_cip`; `None` until then.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Atom {
53 pub element: Element,
54 pub isotope: Option<u16>,
55 pub charge: i8,
56 /// Explicit H count (bracket atoms only). `None` for organic-subset atoms.
57 pub hydrogen_count: Option<u8>,
58 pub aromatic: bool,
59 pub chirality: Chirality,
60 /// True for the wildcard atom `*` or `[*]`.
61 pub wildcard: bool,
62 pub atom_map: Option<u16>,
63 /// CIP stereodescriptor assigned by `chematic_chem::assign_cip`.
64 /// `None` until explicitly computed.
65 pub cip_code: Option<CipCode>,
66}
67
68impl Atom {
69 /// Create a plain, neutral, non-aromatic atom.
70 pub fn new(element: Element) -> Self {
71 Self {
72 element,
73 isotope: None,
74 charge: 0,
75 hydrogen_count: None,
76 aromatic: false,
77 chirality: Chirality::None,
78 wildcard: false,
79 atom_map: None,
80 cip_code: None,
81 }
82 }
83
84 /// Organic-subset atom (charge=0, non-aromatic, implicit H from valence).
85 pub fn organic(element: Element) -> Self {
86 Self::new(element)
87 }
88
89 /// Aromatic organic atom (lowercase SMILES notation).
90 pub fn aromatic(element: Element) -> Self {
91 Self {
92 aromatic: true,
93 ..Self::new(element)
94 }
95 }
96
97 /// Bracket atom with explicit properties.
98 pub fn bracket(
99 element: Element,
100 isotope: Option<u16>,
101 chirality: Chirality,
102 hydrogen_count: u8,
103 charge: i8,
104 atom_map: Option<u16>,
105 ) -> Self {
106 Self {
107 element,
108 isotope,
109 charge,
110 hydrogen_count: Some(hydrogen_count),
111 aromatic: false,
112 chirality,
113 wildcard: false,
114 atom_map,
115 cip_code: None,
116 }
117 }
118
119 /// Wildcard atom `*` / `[*]` (matches any element in query contexts).
120 pub fn wildcard() -> Self {
121 Self {
122 // Element is a placeholder; callers should check `wildcard` first.
123 element: Element::C,
124 wildcard: true,
125 hydrogen_count: Some(0),
126 ..Self::new(Element::C)
127 }
128 }
129
130 /// Return the explicit H count for bracket atoms; `None` for organic-subset atoms.
131 pub fn explicit_hcount(&self) -> Option<u8> {
132 self.hydrogen_count
133 }
134}
135
136impl core::fmt::Display for Atom {
137 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
138 if self.wildcard {
139 return write!(f, "*");
140 }
141 let symbol = if self.aromatic {
142 self.element.symbol().to_lowercase()
143 } else {
144 self.element.symbol().to_string()
145 };
146 match self.isotope {
147 Some(iso) => write!(f, "[{iso}{symbol}]"),
148 None => write!(f, "{symbol}"),
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_atom_new() {
159 let a = Atom::new(Element::C);
160 assert_eq!(a.element, Element::C);
161 assert_eq!(a.charge, 0);
162 assert!(!a.aromatic);
163 assert!(!a.wildcard);
164 assert_eq!(a.hydrogen_count, None);
165 }
166
167 #[test]
168 fn test_aromatic_atom() {
169 let a = Atom::aromatic(Element::C);
170 assert!(a.aromatic);
171 }
172
173 #[test]
174 fn test_wildcard_atom() {
175 let a = Atom::wildcard();
176 assert!(a.wildcard);
177 assert_eq!(format!("{a}"), "*");
178 }
179}