1use std::collections::{HashMap, HashSet};
17
18use crate::bond::BondOrder;
19use crate::molecule::{AtomIdx, BondIdx, Molecule};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct KekuleError {
24 pub detail: String,
25}
26
27impl core::fmt::Display for KekuleError {
28 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
29 write!(f, "kekulization failed: {}", self.detail)
30 }
31}
32
33impl std::error::Error for KekuleError {}
34
35pub type KekuleResult = HashMap<BondIdx, BondOrder>;
39
40pub fn kekulize(mol: &Molecule) -> Result<KekuleResult, KekuleError> {
47 let mut aromatic_bonds: Vec<BondIdx> = Vec::new();
49 let mut aromatic_atoms: HashSet<AtomIdx> = HashSet::new();
50
51 for (bidx, bond) in mol.bonds() {
52 if bond.order == BondOrder::Aromatic {
53 aromatic_bonds.push(bidx);
54 aromatic_atoms.insert(bond.atom1);
55 aromatic_atoms.insert(bond.atom2);
56 }
57 }
58
59 if aromatic_bonds.is_empty() {
60 return Ok(HashMap::new());
61 }
62
63 let must_match: HashSet<AtomIdx> = aromatic_atoms
75 .iter()
76 .copied()
77 .filter(|&idx| atom_must_be_matched(mol, idx))
78 .collect();
79
80 let mut adj: HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>> = HashMap::new();
91 for &bidx in &aromatic_bonds {
92 let bond = mol.bond(bidx);
93 if must_match.contains(&bond.atom1) && must_match.contains(&bond.atom2) {
94 adj.entry(bond.atom1).or_default().push((bond.atom2, bidx));
95 adj.entry(bond.atom2).or_default().push((bond.atom1, bidx));
96 }
97 }
98
99 let mut matching: HashMap<AtomIdx, AtomIdx> = HashMap::new(); let mut sorted_atoms: Vec<AtomIdx> = must_match.iter().copied().collect();
106 sorted_atoms.sort();
107
108 for &start in &sorted_atoms {
109 if matching.contains_key(&start) {
110 continue; }
112 let mut visited: HashSet<AtomIdx> = HashSet::new();
114 augment(start, &adj, &mut matching, &mut visited);
115 }
116
117 for &idx in &must_match {
119 if !matching.contains_key(&idx) {
120 return Err(KekuleError {
121 detail: format!(
122 "atom {} ({}) cannot be assigned a double bond",
123 idx.0,
124 mol.atom(idx).element.symbol()
125 ),
126 });
127 }
128 }
129
130 let mut double_bonds: HashSet<BondIdx> = HashSet::new();
132 for (&atom, &partner) in &matching {
133 if atom >= partner {
134 continue; }
136 if let Some((bidx, _)) = mol.bond_between(atom, partner) {
137 if mol.bond(bidx).order == BondOrder::Aromatic {
138 double_bonds.insert(bidx);
139 }
140 }
141 }
142
143 let result: KekuleResult = aromatic_bonds
144 .iter()
145 .map(|&bidx| {
146 let order = if double_bonds.contains(&bidx) {
147 BondOrder::Double
148 } else {
149 BondOrder::Single
150 };
151 (bidx, order)
152 })
153 .collect();
154 Ok(result)
155}
156
157pub fn apply_kekule(mol: &Molecule, kekule: &KekuleResult) -> Molecule {
162 use crate::molecule::MoleculeBuilder;
163
164 let mut builder = MoleculeBuilder::new();
165
166 for (_, atom) in mol.atoms() {
168 builder.add_atom(atom.clone());
169 }
170
171 for (bidx, bond) in mol.bonds() {
173 let order = kekule.get(&bidx).copied().unwrap_or(bond.order);
174 builder.add_bond(bond.atom1, bond.atom2, order)
175 .expect("duplicate bond during apply_kekule");
176 }
177
178 builder.build()
179}
180
181fn augment(
184 v: AtomIdx,
185 adj: &HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>>,
186 matching: &mut HashMap<AtomIdx, AtomIdx>,
187 visited: &mut HashSet<AtomIdx>,
188) -> bool {
189 let Some(neighbors) = adj.get(&v) else { return false };
190 for &(u, _) in neighbors {
191 if !visited.insert(u) {
192 continue;
193 }
194 let can_augment = match matching.get(&u).copied() {
196 None => true,
197 Some(partner) => augment(partner, adj, matching, visited),
198 };
199 if can_augment {
200 matching.insert(v, u);
201 matching.insert(u, v);
202 return true;
203 }
204 }
205 false
206}
207
208fn atom_must_be_matched(mol: &Molecule, idx: AtomIdx) -> bool {
223 let atom = mol.atom(idx);
224 match atom.element.atomic_number() {
225 8 | 16 | 34 => false,
227 5 => false,
229 7 => !matches!(atom.hydrogen_count, Some(h) if h > 0),
231 _ => true,
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::atom::Atom;
240 use crate::element::Element;
241 use crate::molecule::MoleculeBuilder;
242
243 fn benzene() -> Molecule {
245 let mut b = MoleculeBuilder::new();
246 let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::aromatic(Element::C))).collect();
247 for i in 0..6 {
248 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic).unwrap();
249 }
250 b.build()
251 }
252
253 fn pyridine() -> Molecule {
255 let mut b = MoleculeBuilder::new();
256 let c1 = b.add_atom(Atom::aromatic(Element::C));
257 let c2 = b.add_atom(Atom::aromatic(Element::C));
258 let c3 = b.add_atom(Atom::aromatic(Element::C));
259 let n = b.add_atom(Atom::aromatic(Element::N));
260 let c4 = b.add_atom(Atom::aromatic(Element::C));
261 let c5 = b.add_atom(Atom::aromatic(Element::C));
262 let atoms = [c1, c2, c3, n, c4, c5];
263 for i in 0..6 {
264 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic).unwrap();
265 }
266 b.build()
267 }
268
269 fn furan() -> Molecule {
271 let mut b = MoleculeBuilder::new();
272 let o = b.add_atom(Atom::aromatic(Element::O));
273 let c1 = b.add_atom(Atom::aromatic(Element::C));
274 let c2 = b.add_atom(Atom::aromatic(Element::C));
275 let c3 = b.add_atom(Atom::aromatic(Element::C));
276 let c4 = b.add_atom(Atom::aromatic(Element::C));
277 let atoms = [o, c1, c2, c3, c4];
278 for i in 0..5 {
279 b.add_bond(atoms[i], atoms[(i + 1) % 5], BondOrder::Aromatic).unwrap();
280 }
281 b.build()
282 }
283
284 fn pyrrole() -> Molecule {
286 let mut b = MoleculeBuilder::new();
287 let mut n_atom = Atom::aromatic(Element::N);
289 n_atom.hydrogen_count = Some(1);
290 let n = b.add_atom(n_atom);
291 let c1 = b.add_atom(Atom::aromatic(Element::C));
292 let c2 = b.add_atom(Atom::aromatic(Element::C));
293 let c3 = b.add_atom(Atom::aromatic(Element::C));
294 let c4 = b.add_atom(Atom::aromatic(Element::C));
295 let atoms = [n, c1, c2, c3, c4];
296 for i in 0..5 {
297 b.add_bond(atoms[i], atoms[(i + 1) % 5], BondOrder::Aromatic).unwrap();
298 }
299 b.build()
300 }
301
302 #[test]
303 fn test_kekulize_benzene() {
304 let mol = benzene();
305 let result = kekulize(&mol).expect("benzene kekulization failed");
306 assert_eq!(result.len(), 6); let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
309 let singles = result.values().filter(|&&o| o == BondOrder::Single).count();
310 assert_eq!(doubles, 3, "benzene must have 3 double bonds");
311 assert_eq!(singles, 3, "benzene must have 3 single bonds");
312 }
313
314 #[test]
315 fn test_kekulize_pyridine() {
316 let mol = pyridine();
317 let result = kekulize(&mol).expect("pyridine kekulization failed");
318 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
319 assert_eq!(doubles, 3, "pyridine must have 3 double bonds");
320 }
321
322 #[test]
323 fn test_kekulize_furan() {
324 let mol = furan();
325 let result = kekulize(&mol).expect("furan kekulization failed");
326 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
327 assert_eq!(doubles, 2, "furan must have 2 double bonds");
328 }
329
330 #[test]
331 fn test_kekulize_pyrrole() {
332 let mol = pyrrole();
333 let result = kekulize(&mol).expect("pyrrole kekulization failed");
334 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
335 assert_eq!(doubles, 2, "pyrrole must have 2 double bonds");
336 }
337
338 #[test]
339 fn test_kekulize_naphthalene() {
340 let mut b = MoleculeBuilder::new();
342 let atoms: Vec<_> = (0..10).map(|_| b.add_atom(Atom::aromatic(Element::C))).collect();
343 let ring1 = [0,1,2,3,4,9];
345 for i in 0..ring1.len() {
346 b.add_bond(atoms[ring1[i]], atoms[ring1[(i+1)%ring1.len()]], BondOrder::Aromatic).unwrap();
347 }
348 let ring2 = [4,5,6,7,8,9];
350 for i in 0..ring2.len() {
351 let a = atoms[ring2[i]];
352 let bb = atoms[ring2[(i+1)%ring2.len()]];
353 if mol_has_no_bond_yet(&b, a, bb) {
355 b.add_bond(a, bb, BondOrder::Aromatic).unwrap();
356 }
357 }
358 let mol = b.build();
359 let result = kekulize(&mol).expect("naphthalene kekulization failed");
360 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
361 assert_eq!(doubles, 5, "naphthalene must have 5 double bonds");
362 }
363
364 #[test]
365 fn test_apply_kekule() {
366 let mol = benzene();
367 let kekule = kekulize(&mol).unwrap();
368 let kekule_mol = apply_kekule(&mol, &kekule);
369
370 for (_, bond) in kekule_mol.bonds() {
372 assert_ne!(bond.order, BondOrder::Aromatic,
373 "apply_kekule should remove all aromatic bonds");
374 }
375 }
376
377 #[test]
378 fn test_no_aromatic_bonds_noop() {
379 let mut b = MoleculeBuilder::new();
381 let c1 = b.add_atom(Atom::new(Element::C));
382 let c2 = b.add_atom(Atom::new(Element::C));
383 b.add_bond(c1, c2, BondOrder::Single).unwrap();
384 let mol = b.build();
385 let result = kekulize(&mol).unwrap();
386 assert!(result.is_empty());
387 }
388
389 fn mol_has_no_bond_yet(b: &MoleculeBuilder, a: AtomIdx, bb: AtomIdx) -> bool {
391 for (_, partner) in b.atom_neighbors(a) {
392 if partner == bb { return false; }
393 }
394 true
395 }
396}