1use std::collections::HashSet;
15
16use chematic_core::{AtomIdx, BondIdx, BondOrder, Molecule};
17
18use crate::sssr::find_sssr;
19
20#[derive(Debug, Clone)]
29pub struct AromaticityModel {
30 aromatic_atoms: HashSet<AtomIdx>,
31 aromatic_bonds: HashSet<BondIdx>,
32}
33
34impl AromaticityModel {
35 pub fn is_atom_aromatic(&self, idx: AtomIdx) -> bool {
37 self.aromatic_atoms.contains(&idx)
38 }
39
40 pub fn is_bond_aromatic(&self, idx: BondIdx) -> bool {
42 self.aromatic_bonds.contains(&idx)
43 }
44
45 pub fn aromatic_atom_count(&self) -> usize {
47 self.aromatic_atoms.len()
48 }
49}
50
51pub fn assign_aromaticity(mol: &Molecule) -> AromaticityModel {
61 let ring_set = find_sssr(mol);
62
63 let mut aromatic_atoms: HashSet<AtomIdx> = HashSet::new();
64 let mut aromatic_bonds: HashSet<BondIdx> = HashSet::new();
65
66 for ring in ring_set.rings() {
67 if let Some(pi_electrons) = ring_pi_electrons(mol, ring) {
68 if pi_electrons >= 2 && (pi_electrons - 2) % 4 == 0 {
70 for &atom in ring {
72 aromatic_atoms.insert(atom);
73 }
74 for i in 0..ring.len() {
76 let a = ring[i];
77 let b = ring[(i + 1) % ring.len()];
78 if let Some((bidx, _)) = mol.bond_between(a, b) {
79 aromatic_bonds.insert(bidx);
80 }
81 }
82 }
83 }
84 }
85
86 AromaticityModel { aromatic_atoms, aromatic_bonds }
87}
88
89fn ring_pi_electrons(mol: &Molecule, ring: &[AtomIdx]) -> Option<u32> {
99 let ring_atom_set: HashSet<AtomIdx> = ring.iter().copied().collect();
100 let mut total_pi: u32 = 0;
101
102 for &atom_idx in ring {
103 let atom = mol.atom(atom_idx);
104 let an = atom.element.atomic_number();
105
106 let ring_degree = mol
108 .neighbors(atom_idx)
109 .filter(|(nb, _)| ring_atom_set.contains(nb))
110 .count();
111
112 let has_double_in_ring = mol
114 .neighbors(atom_idx)
115 .filter(|(nb, _)| ring_atom_set.contains(nb))
116 .any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Double);
117
118 let has_double_any = mol
120 .neighbors(atom_idx)
121 .any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Double);
122
123 let pi = match an {
124 6 => {
126 if !has_double_any {
127 return None;
129 }
130 1 }
132 7 => {
134 if hydrogen_count(mol, atom_idx) > 0 {
137 2
139 } else if has_double_in_ring || has_double_any {
140 1
142 } else {
143 return None;
145 }
146 }
147 8 | 16 => {
149 if ring_degree != 2 {
150 return None;
151 }
152 2
153 }
154 _ => return None,
156 };
157
158 total_pi += pi;
159 }
160
161 Some(total_pi)
162}
163
164fn hydrogen_count(mol: &Molecule, atom_idx: AtomIdx) -> u8 {
173 let atom = mol.atom(atom_idx);
174
175 if let Some(h) = atom.hydrogen_count {
177 return h;
178 }
179
180 let normal_valences = atom.element.normal_valences();
182 if normal_valences.is_empty() {
183 return 0;
184 }
185
186 let bond_sum: i32 = mol
187 .neighbors(atom_idx)
188 .map(|(_, bidx)| mol.bond(bidx).order.order_int() as i32)
189 .sum();
190
191 let charge = atom.charge as i32;
192
193 for &v in normal_valences {
194 let target = v as i32 + charge;
195 if target >= bond_sum {
196 return (target - bond_sum) as u8;
197 }
198 }
199
200 0
201}
202
203#[cfg(test)]
208mod tests {
209 use super::*;
210 use chematic_core::{Atom, BondOrder, Element, MoleculeBuilder};
211
212 fn benzene_kekule() -> chematic_core::Molecule {
214 let mut b = MoleculeBuilder::new();
215 let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
216 for i in 0..6 {
217 let order = if i % 2 == 0 { BondOrder::Double } else { BondOrder::Single };
218 b.add_bond(atoms[i], atoms[(i + 1) % 6], order).unwrap();
219 }
220 b.build()
221 }
222
223 fn cyclohexane() -> chematic_core::Molecule {
225 let mut b = MoleculeBuilder::new();
226 let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
227 for i in 0..6 {
228 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Single).unwrap();
229 }
230 b.build()
231 }
232
233 fn pyridine_kekule() -> chematic_core::Molecule {
236 let mut b = MoleculeBuilder::new();
237 let n = b.add_atom(Atom::new(Element::N));
238 let atoms_c: Vec<_> = (0..5).map(|_| b.add_atom(Atom::new(Element::C))).collect();
239 let ring = [n, atoms_c[0], atoms_c[1], atoms_c[2], atoms_c[3], atoms_c[4]];
240 for i in 0..6 {
242 let order = if i % 2 == 0 { BondOrder::Double } else { BondOrder::Single };
243 b.add_bond(ring[i], ring[(i + 1) % 6], order).unwrap();
244 }
245 b.build()
246 }
247
248 fn furan_kekule() -> chematic_core::Molecule {
251 let mut b = MoleculeBuilder::new();
252 let o = b.add_atom(Atom::new(Element::O));
253 let c1 = b.add_atom(Atom::new(Element::C));
254 let c2 = b.add_atom(Atom::new(Element::C));
255 let c3 = b.add_atom(Atom::new(Element::C));
256 let c4 = b.add_atom(Atom::new(Element::C));
257 let ring = [o, c1, c2, c3, c4];
258 b.add_bond(ring[0], ring[1], BondOrder::Single).unwrap();
260 b.add_bond(ring[1], ring[2], BondOrder::Double).unwrap();
261 b.add_bond(ring[2], ring[3], BondOrder::Single).unwrap();
262 b.add_bond(ring[3], ring[4], BondOrder::Double).unwrap();
263 b.add_bond(ring[4], ring[0], BondOrder::Single).unwrap();
264 b.build()
265 }
266
267 fn pyrrole_kekule() -> chematic_core::Molecule {
270 let mut b = MoleculeBuilder::new();
271 let mut n_atom = Atom::new(Element::N);
272 n_atom.hydrogen_count = Some(1);
273 let n = b.add_atom(n_atom);
274 let c1 = b.add_atom(Atom::new(Element::C));
275 let c2 = b.add_atom(Atom::new(Element::C));
276 let c3 = b.add_atom(Atom::new(Element::C));
277 let c4 = b.add_atom(Atom::new(Element::C));
278 let ring = [n, c1, c2, c3, c4];
279 b.add_bond(ring[0], ring[1], BondOrder::Single).unwrap();
280 b.add_bond(ring[1], ring[2], BondOrder::Double).unwrap();
281 b.add_bond(ring[2], ring[3], BondOrder::Single).unwrap();
282 b.add_bond(ring[3], ring[4], BondOrder::Double).unwrap();
283 b.add_bond(ring[4], ring[0], BondOrder::Single).unwrap();
284 b.build()
285 }
286
287 fn naphthalene_kekule() -> chematic_core::Molecule {
289 let mut b = MoleculeBuilder::new();
290 let atoms: Vec<_> = (0..10).map(|_| b.add_atom(Atom::new(Element::C))).collect();
291 let ring1 = [0usize, 1, 2, 3, 4, 9];
298 let orders1 = [
299 BondOrder::Double,
300 BondOrder::Single,
301 BondOrder::Double,
302 BondOrder::Single,
303 BondOrder::Double,
304 BondOrder::Single,
305 ];
306 for i in 0..6 {
307 b.add_bond(atoms[ring1[i]], atoms[ring1[(i + 1) % 6]], orders1[i]).unwrap();
308 }
309 let ring2_extra = [(4, 5), (5, 6), (6, 7), (7, 8), (8, 9)];
311 let orders2 = [
312 BondOrder::Single,
313 BondOrder::Double,
314 BondOrder::Single,
315 BondOrder::Double,
316 BondOrder::Single,
317 ];
318 for (i, &(a, bb)) in ring2_extra.iter().enumerate() {
319 b.add_bond(atoms[a], atoms[bb], orders2[i]).unwrap();
320 }
321 b.build()
322 }
323
324 #[test]
325 fn test_benzene_is_aromatic() {
326 let mol = benzene_kekule();
327 let model = assign_aromaticity(&mol);
328 assert_eq!(model.aromatic_atom_count(), 6, "all 6 benzene atoms are aromatic");
329 for i in 0..6u32 {
330 assert!(model.is_atom_aromatic(AtomIdx(i)), "atom {} should be aromatic", i);
331 }
332 }
333
334 #[test]
335 fn test_cyclohexane_not_aromatic() {
336 let mol = cyclohexane();
337 let model = assign_aromaticity(&mol);
338 assert_eq!(model.aromatic_atom_count(), 0, "cyclohexane has no aromatic atoms");
339 }
340
341 #[test]
342 fn test_pyridine_is_aromatic() {
343 let mol = pyridine_kekule();
344 let model = assign_aromaticity(&mol);
345 assert_eq!(model.aromatic_atom_count(), 6, "all 6 pyridine atoms are aromatic");
346 }
347
348 #[test]
349 fn test_furan_is_aromatic() {
350 let mol = furan_kekule();
351 let model = assign_aromaticity(&mol);
352 assert_eq!(model.aromatic_atom_count(), 5, "all 5 furan atoms are aromatic");
353 }
354
355 #[test]
356 fn test_pyrrole_is_aromatic() {
357 let mol = pyrrole_kekule();
358 let model = assign_aromaticity(&mol);
359 assert_eq!(model.aromatic_atom_count(), 5, "all 5 pyrrole atoms are aromatic");
360 }
361
362 #[test]
363 fn test_naphthalene_both_rings_aromatic() {
364 let mol = naphthalene_kekule();
365 let model = assign_aromaticity(&mol);
366 assert_eq!(
368 model.aromatic_atom_count(),
369 10,
370 "all 10 naphthalene atoms are aromatic"
371 );
372 }
373
374 #[test]
375 fn test_bond_aromaticity_benzene() {
376 let mol = benzene_kekule();
377 let model = assign_aromaticity(&mol);
378 let mut count = 0;
380 for (bidx, _) in mol.bonds() {
381 if model.is_bond_aromatic(bidx) {
382 count += 1;
383 }
384 }
385 assert_eq!(count, 6, "benzene has 6 aromatic bonds");
386 }
387}