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 run_matching_pass(&sorted_atoms, &adj, &mut matching);
110
111 if must_match.iter().any(|&idx| !matching.contains_key(&idx)) {
118 matching.clear();
119 let mut rev = sorted_atoms.clone();
120 rev.reverse();
121 run_matching_pass(&rev, &adj, &mut matching);
122 }
123
124 for &idx in &must_match {
126 if !matching.contains_key(&idx) {
127 return Err(KekuleError {
128 detail: format!(
129 "atom {} ({}) cannot be assigned a double bond",
130 idx.0,
131 mol.atom(idx).element.symbol()
132 ),
133 });
134 }
135 }
136
137 let mut double_bonds: HashSet<BondIdx> = HashSet::new();
139 for (&atom, &partner) in &matching {
140 if atom >= partner {
141 continue; }
143 if let Some((bidx, _)) = mol.bond_between(atom, partner)
144 && mol.bond(bidx).order == BondOrder::Aromatic
145 {
146 double_bonds.insert(bidx);
147 }
148 }
149
150 let result: KekuleResult = aromatic_bonds
151 .iter()
152 .map(|&bidx| {
153 let order = if double_bonds.contains(&bidx) {
154 BondOrder::Double
155 } else {
156 BondOrder::Single
157 };
158 (bidx, order)
159 })
160 .collect();
161 Ok(result)
162}
163
164pub fn apply_kekule(mol: &Molecule, kekule: &KekuleResult) -> Molecule {
169 use crate::molecule::MoleculeBuilder;
170
171 let mut builder = MoleculeBuilder::new();
172
173 for (_, atom) in mol.atoms() {
175 builder.add_atom(atom.clone());
176 }
177
178 for (bidx, bond) in mol.bonds() {
180 let order = kekule.get(&bidx).copied().unwrap_or(bond.order);
181 builder
182 .add_bond(bond.atom1, bond.atom2, order)
183 .expect("duplicate bond during apply_kekule");
184 }
185
186 builder.build()
187}
188
189fn augment(
197 start: AtomIdx,
198 adj: &HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>>,
199 matching: &mut HashMap<AtomIdx, AtomIdx>,
200 visited: &mut HashSet<AtomIdx>,
201) -> bool {
202 let mut parent: HashMap<AtomIdx, AtomIdx> = HashMap::new();
204 let mut queue: std::collections::VecDeque<AtomIdx> = std::collections::VecDeque::new();
205 queue.push_back(start);
206
207 'bfs: while let Some(v) = queue.pop_front() {
208 let Some(neighbors) = adj.get(&v) else {
209 continue;
210 };
211 for &(u, _) in neighbors {
212 if !visited.insert(u) {
213 continue;
214 }
215 parent.insert(u, v);
216
217 match matching.get(&u).copied() {
218 None => {
219 let mut cur = u;
222 loop {
223 let prev = parent[&cur];
224 let prev_old_match = matching.get(&prev).copied();
225 matching.insert(prev, cur);
226 matching.insert(cur, prev);
227 match prev_old_match {
228 None | Some(_) if prev == start => break,
229 Some(m) => cur = m,
230 None => break,
231 }
232 }
233 break 'bfs;
234 }
235 Some(partner) => {
236 if visited.insert(partner) {
238 parent.insert(partner, u);
239 queue.push_back(partner);
240 }
241 }
242 }
243 }
244 }
245
246 matching.contains_key(&start)
248}
249
250fn run_matching_pass(
256 atoms: &[AtomIdx],
257 adj: &HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>>,
258 matching: &mut HashMap<AtomIdx, AtomIdx>,
259) {
260 for &start in atoms {
261 if matching.contains_key(&start) {
262 continue;
263 }
264 let mut visited: HashSet<AtomIdx> = HashSet::new();
265 visited.insert(start);
266 augment(start, adj, matching, &mut visited);
267 }
268}
269
270fn atom_must_be_matched(mol: &Molecule, idx: AtomIdx) -> bool {
285 let atom = mol.atom(idx);
286 match atom.element.atomic_number() {
287 8 | 16 | 34 => false,
289 5 => false,
291 7 => !matches!(atom.hydrogen_count, Some(h) if h > 0),
293 _ => true,
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::atom::Atom;
302 use crate::element::Element;
303 use crate::molecule::MoleculeBuilder;
304
305 fn benzene() -> Molecule {
307 let mut b = MoleculeBuilder::new();
308 let atoms: Vec<_> = (0..6)
309 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
310 .collect();
311 for i in 0..6 {
312 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
313 .unwrap();
314 }
315 b.build()
316 }
317
318 fn pyridine() -> Molecule {
320 let mut b = MoleculeBuilder::new();
321 let c1 = b.add_atom(Atom::aromatic(Element::C));
322 let c2 = b.add_atom(Atom::aromatic(Element::C));
323 let c3 = b.add_atom(Atom::aromatic(Element::C));
324 let n = b.add_atom(Atom::aromatic(Element::N));
325 let c4 = b.add_atom(Atom::aromatic(Element::C));
326 let c5 = b.add_atom(Atom::aromatic(Element::C));
327 let atoms = [c1, c2, c3, n, c4, c5];
328 for i in 0..6 {
329 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
330 .unwrap();
331 }
332 b.build()
333 }
334
335 fn furan() -> Molecule {
337 let mut b = MoleculeBuilder::new();
338 let o = b.add_atom(Atom::aromatic(Element::O));
339 let c1 = b.add_atom(Atom::aromatic(Element::C));
340 let c2 = b.add_atom(Atom::aromatic(Element::C));
341 let c3 = b.add_atom(Atom::aromatic(Element::C));
342 let c4 = b.add_atom(Atom::aromatic(Element::C));
343 let atoms = [o, c1, c2, c3, c4];
344 for i in 0..5 {
345 b.add_bond(atoms[i], atoms[(i + 1) % 5], BondOrder::Aromatic)
346 .unwrap();
347 }
348 b.build()
349 }
350
351 fn pyrrole() -> Molecule {
353 let mut b = MoleculeBuilder::new();
354 let mut n_atom = Atom::aromatic(Element::N);
356 n_atom.hydrogen_count = Some(1);
357 let n = b.add_atom(n_atom);
358 let c1 = b.add_atom(Atom::aromatic(Element::C));
359 let c2 = b.add_atom(Atom::aromatic(Element::C));
360 let c3 = b.add_atom(Atom::aromatic(Element::C));
361 let c4 = b.add_atom(Atom::aromatic(Element::C));
362 let atoms = [n, c1, c2, c3, c4];
363 for i in 0..5 {
364 b.add_bond(atoms[i], atoms[(i + 1) % 5], BondOrder::Aromatic)
365 .unwrap();
366 }
367 b.build()
368 }
369
370 #[test]
371 fn test_kekulize_benzene() {
372 let mol = benzene();
373 let result = kekulize(&mol).expect("benzene kekulization failed");
374 assert_eq!(result.len(), 6); let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
377 let singles = result.values().filter(|&&o| o == BondOrder::Single).count();
378 assert_eq!(doubles, 3, "benzene must have 3 double bonds");
379 assert_eq!(singles, 3, "benzene must have 3 single bonds");
380 }
381
382 #[test]
383 fn test_kekulize_pyridine() {
384 let mol = pyridine();
385 let result = kekulize(&mol).expect("pyridine kekulization failed");
386 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
387 assert_eq!(doubles, 3, "pyridine must have 3 double bonds");
388 }
389
390 #[test]
391 fn test_kekulize_furan() {
392 let mol = furan();
393 let result = kekulize(&mol).expect("furan kekulization failed");
394 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
395 assert_eq!(doubles, 2, "furan must have 2 double bonds");
396 }
397
398 #[test]
399 fn test_kekulize_pyrrole() {
400 let mol = pyrrole();
401 let result = kekulize(&mol).expect("pyrrole kekulization failed");
402 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
403 assert_eq!(doubles, 2, "pyrrole must have 2 double bonds");
404 }
405
406 #[test]
407 fn test_kekulize_naphthalene() {
408 let mut b = MoleculeBuilder::new();
410 let atoms: Vec<_> = (0..10)
411 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
412 .collect();
413 let ring1 = [0, 1, 2, 3, 4, 9];
415 for i in 0..ring1.len() {
416 b.add_bond(
417 atoms[ring1[i]],
418 atoms[ring1[(i + 1) % ring1.len()]],
419 BondOrder::Aromatic,
420 )
421 .unwrap();
422 }
423 let ring2 = [4, 5, 6, 7, 8, 9];
425 for i in 0..ring2.len() {
426 let a = atoms[ring2[i]];
427 let bb = atoms[ring2[(i + 1) % ring2.len()]];
428 if mol_has_no_bond_yet(&b, a, bb) {
430 b.add_bond(a, bb, BondOrder::Aromatic).unwrap();
431 }
432 }
433 let mol = b.build();
434 let result = kekulize(&mol).expect("naphthalene kekulization failed");
435 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
436 assert_eq!(doubles, 5, "naphthalene must have 5 double bonds");
437 }
438
439 #[test]
440 fn test_apply_kekule() {
441 let mol = benzene();
442 let kekule = kekulize(&mol).unwrap();
443 let kekule_mol = apply_kekule(&mol, &kekule);
444
445 for (_, bond) in kekule_mol.bonds() {
447 assert_ne!(
448 bond.order,
449 BondOrder::Aromatic,
450 "apply_kekule should remove all aromatic bonds"
451 );
452 }
453 }
454
455 #[test]
456 fn test_no_aromatic_bonds_noop() {
457 let mut b = MoleculeBuilder::new();
459 let c1 = b.add_atom(Atom::new(Element::C));
460 let c2 = b.add_atom(Atom::new(Element::C));
461 b.add_bond(c1, c2, BondOrder::Single).unwrap();
462 let mol = b.build();
463 let result = kekulize(&mol).unwrap();
464 assert!(result.is_empty());
465 }
466
467 fn mol_has_no_bond_yet(b: &MoleculeBuilder, a: AtomIdx, bb: AtomIdx) -> bool {
469 for (_, partner) in b.atom_neighbors(a) {
470 if partner == bb {
471 return false;
472 }
473 }
474 true
475 }
476
477 #[test]
482 fn test_kekulize_azulene() {
483 let mut b = MoleculeBuilder::new();
484 let a: Vec<_> = (0..10)
485 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
486 .collect();
487 for i in 0..5 {
489 b.add_bond(a[i], a[(i + 1) % 5], BondOrder::Aromatic).unwrap();
490 }
491 for (x, y) in [(4usize, 5usize), (5, 6), (6, 7), (7, 8), (8, 9), (9, 0)] {
493 if mol_has_no_bond_yet(&b, a[x], a[y]) {
494 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
495 }
496 }
497 let mol = b.build();
498 let result = kekulize(&mol).expect("azulene kekulization failed");
499 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
500 assert_eq!(doubles, 5, "azulene needs 5 double bonds");
501 }
502
503 #[test]
505 fn test_kekulize_acenaphthylene() {
506 let mut b = MoleculeBuilder::new();
510 let a: Vec<_> = (0..12)
511 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
512 .collect();
513 for (x, y) in [(0,1),(1,2),(2,3),(3,4),(4,9),(9,0)] {
515 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
516 }
517 for (x, y) in [(4,5),(5,6),(6,7),(7,8),(8,9)] {
519 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
520 }
521 for (x, y) in [(0,11),(11,10),(10,1)] {
523 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
524 }
525 let mol = b.build();
526 let result = kekulize(&mol).expect("acenaphthylene kekulization failed");
527 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
528 assert_eq!(doubles, 6, "acenaphthylene needs 6 double bonds");
529 }
530
531 fn biphenylene() -> Molecule {
540 let mut b = MoleculeBuilder::new();
541 let a: Vec<_> = (0..12)
542 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
543 .collect();
544 for i in 0..6 {
546 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic).unwrap();
547 }
548 for i in 0..6 {
550 b.add_bond(a[6 + i], a[6 + (i + 1) % 6], BondOrder::Aromatic).unwrap();
551 }
552 b.add_bond(a[5], a[6], BondOrder::Aromatic).unwrap();
554 b.add_bond(a[0], a[11], BondOrder::Aromatic).unwrap();
555 b.build()
556 }
557
558 fn anthracene() -> Molecule {
561 let mut b = MoleculeBuilder::new();
562 let a: Vec<_> = (0..14)
563 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
564 .collect();
565 for i in 0..6 {
567 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic).unwrap();
568 }
569 for (x, y) in [(4,9),(9,8),(8,7),(7,6),(6,5)] {
571 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
572 }
573 for (x, y) in [(7,13),(13,12),(12,11),(11,10),(10,6)] {
575 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
576 }
577 b.build()
578 }
579
580 #[test]
581 fn test_kekulize_biphenylene() {
582 let mol = biphenylene();
585 let result = kekulize(&mol).expect("biphenylene kekulization should succeed");
586 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
587 assert_eq!(doubles, 6, "biphenylene needs 6 double bonds");
588 }
589
590 #[test]
591 fn test_kekulize_anthracene() {
592 let mol = anthracene();
594 let result = kekulize(&mol).expect("anthracene kekulization should succeed");
595 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
596 assert_eq!(doubles, 7, "anthracene needs 7 double bonds");
597 }
598
599 #[test]
600 fn test_kekulize_biphenylene_double_bond_count() {
601 let mol = biphenylene();
603 let result = kekulize(&mol).expect("biphenylene kekulization");
604 let singles = result.values().filter(|&&o| o == BondOrder::Single).count();
605 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
606 assert_eq!(singles + doubles, 14, "biphenylene has 14 aromatic bonds");
607 }
608
609 #[test]
610 fn test_kekulize_large_fused_6rings() {
611 let mol = {
614 let mut b = MoleculeBuilder::new();
615 let a: Vec<_> = (0..16)
616 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
617 .collect();
618 for i in 0..6 { b.add_bond(a[i], a[(i+1)%6], BondOrder::Aromatic).unwrap(); }
620 for (x,y) in [(4,9),(9,8),(8,7),(7,6),(6,5)] {
622 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
623 }
624 for (x,y) in [(2,11),(11,10),(10,13),(13,12),(12,1)] {
626 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
627 }
628 for (x,y) in [(7,15),(15,14),(14,11)] {
630 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
631 }
632 b.build()
633 };
634 let result = kekulize(&mol).expect("4-ring PAH kekulization should succeed");
635 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
636 assert!(doubles >= 6, "4-ring PAH needs at least 6 double bonds, got {doubles}");
637 }
638
639 #[test]
640 fn test_kekulize_deterministic() {
641 let mol1 = biphenylene();
643 let mol2 = biphenylene();
644 let r1 = kekulize(&mol1).expect("pass1");
645 let r2 = kekulize(&mol2).expect("pass2");
646 assert_eq!(
647 r1.values().filter(|&&o| o == BondOrder::Double).count(),
648 r2.values().filter(|&&o| o == BondOrder::Double).count(),
649 "kekulization must be deterministic"
650 );
651 }
652
653 #[test]
655 fn test_kekulize_fluoranthene() {
656 let mut b = MoleculeBuilder::new();
665 let a: Vec<_> = (0..16)
666 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
667 .collect();
668 for i in 0..6 { b.add_bond(a[i], a[(i+1)%6], BondOrder::Aromatic).unwrap(); }
670 for (x,y) in [(5,6),(6,7),(7,8),(8,9),(9,0)] {
672 if mol_has_no_bond_yet(&b, a[x], a[y]) {
673 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
674 }
675 }
676 for (x,y) in [(2,10),(10,11),(11,12),(12,13),(13,1)] {
678 if mol_has_no_bond_yet(&b, a[x], a[y]) {
679 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
680 }
681 }
682 for (x,y) in [(8,14),(14,15),(15,13),(13,9)] {
684 if mol_has_no_bond_yet(&b, a[x], a[y]) {
685 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
686 }
687 }
688 let mol = b.build();
689 let result = kekulize(&mol).expect("fluoranthene-like kekulization failed");
690 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
691 assert_eq!(doubles, 8, "fluoranthene-like structure needs 8 double bonds");
692 }
693}