1use std::collections::{HashMap, HashSet, VecDeque};
15
16use crate::bond::BondOrder;
17use crate::molecule::{AtomIdx, BondIdx, Molecule};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct KekuleError {
22 pub detail: String,
23}
24
25impl core::fmt::Display for KekuleError {
26 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27 write!(f, "kekulization failed: {}", self.detail)
28 }
29}
30
31impl std::error::Error for KekuleError {}
32
33pub type KekuleResult = HashMap<BondIdx, BondOrder>;
37
38pub fn kekulize(mol: &Molecule) -> Result<KekuleResult, KekuleError> {
45 let mut aromatic_bonds: Vec<BondIdx> = Vec::new();
47 let mut aromatic_atoms: HashSet<AtomIdx> = HashSet::new();
48
49 for (bidx, bond) in mol.bonds() {
50 if bond.order == BondOrder::Aromatic {
51 aromatic_bonds.push(bidx);
52 aromatic_atoms.insert(bond.atom1);
53 aromatic_atoms.insert(bond.atom2);
54 }
55 }
56
57 if aromatic_bonds.is_empty() {
58 return Ok(HashMap::new());
59 }
60
61 let must_match: HashSet<AtomIdx> = aromatic_atoms
73 .iter()
74 .copied()
75 .filter(|&idx| atom_must_be_matched(mol, idx))
76 .collect();
77
78 let mut adj: HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>> = HashMap::new();
89 for &bidx in &aromatic_bonds {
90 let bond = mol.bond(bidx);
91 if must_match.contains(&bond.atom1) && must_match.contains(&bond.atom2) {
92 adj.entry(bond.atom1).or_default().push((bond.atom2, bidx));
93 adj.entry(bond.atom2).or_default().push((bond.atom1, bidx));
94 }
95 }
96
97 let mut matching: HashMap<AtomIdx, AtomIdx> = HashMap::new(); let mut sorted_atoms: Vec<AtomIdx> = must_match.iter().copied().collect();
104 sorted_atoms.sort();
105
106 run_matching_pass(&sorted_atoms, &adj, &mut matching);
108
109 if must_match.iter().any(|&idx| !matching.contains_key(&idx)) {
116 matching.clear();
117 let mut rev = sorted_atoms.clone();
118 rev.reverse();
119 run_matching_pass(&rev, &adj, &mut matching);
120 }
121
122 if must_match.iter().any(|&idx| !matching.contains_key(&idx)) {
136 let bridgehead_n: HashSet<AtomIdx> = must_match
137 .iter()
138 .copied()
139 .filter(|&idx| {
140 mol.atom(idx).element.atomic_number() == 7
141 && adj.get(&idx).map_or(0, |v| v.len()) >= 3
142 })
143 .collect();
144
145 if !bridgehead_n.is_empty() {
146 let must_match_nb: HashSet<AtomIdx> =
147 must_match.difference(&bridgehead_n).copied().collect();
148
149 let mut adj_nb: HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>> = HashMap::new();
150 for &bidx in &aromatic_bonds {
151 let bond = mol.bond(bidx);
152 if must_match_nb.contains(&bond.atom1) && must_match_nb.contains(&bond.atom2) {
153 adj_nb
154 .entry(bond.atom1)
155 .or_default()
156 .push((bond.atom2, bidx));
157 adj_nb
158 .entry(bond.atom2)
159 .or_default()
160 .push((bond.atom1, bidx));
161 }
162 }
163
164 let mut sorted_nb: Vec<AtomIdx> = must_match_nb.iter().copied().collect();
165 sorted_nb.sort();
166
167 matching.clear();
168 run_matching_pass(&sorted_nb, &adj_nb, &mut matching);
169 if must_match_nb
170 .iter()
171 .any(|&idx| !matching.contains_key(&idx))
172 {
173 matching.clear();
174 let rev_nb: Vec<AtomIdx> = sorted_nb.iter().copied().rev().collect();
175 run_matching_pass(&rev_nb, &adj_nb, &mut matching);
176 }
177
178 if must_match_nb.iter().all(|&idx| matching.contains_key(&idx)) {
179 return Ok(build_kekule_result(&aromatic_bonds, mol, &matching));
180 }
181 }
182 }
183
184 if must_match.iter().any(|&idx| !matching.contains_key(&idx)) {
193 let n = sorted_atoms.len();
194 let idx_to_int: HashMap<AtomIdx, usize> = sorted_atoms
195 .iter()
196 .enumerate()
197 .map(|(i, &a)| (a, i))
198 .collect();
199 let int_adj: Vec<Vec<usize>> = sorted_atoms
200 .iter()
201 .map(|&a| {
202 adj.get(&a)
203 .map(|nbrs| {
204 nbrs.iter()
205 .filter_map(|(nb, _)| idx_to_int.get(nb).copied())
206 .collect()
207 })
208 .unwrap_or_default()
209 })
210 .collect();
211
212 matching.clear();
213 let int_mate = blossom_max_matching(n, &int_adj);
214 for (i, &j) in int_mate.iter().enumerate() {
215 if j != usize::MAX {
216 matching.insert(sorted_atoms[i], sorted_atoms[j]);
217 }
218 }
219 }
220
221 for &idx in &must_match {
223 if !matching.contains_key(&idx) {
224 return Err(KekuleError {
225 detail: format!(
226 "atom {} ({}) cannot be assigned a double bond",
227 idx.0,
228 mol.atom(idx).element.symbol()
229 ),
230 });
231 }
232 }
233
234 Ok(build_kekule_result(&aromatic_bonds, mol, &matching))
235}
236
237fn build_kekule_result(
239 aromatic_bonds: &[BondIdx],
240 mol: &Molecule,
241 matching: &HashMap<AtomIdx, AtomIdx>,
242) -> KekuleResult {
243 let mut double_bonds: HashSet<BondIdx> = HashSet::new();
244 for (&atom, &partner) in matching {
245 if atom >= partner {
246 continue;
247 }
248 if let Some((bidx, _)) = mol.bond_between(atom, partner)
249 && mol.bond(bidx).order == BondOrder::Aromatic
250 {
251 double_bonds.insert(bidx);
252 }
253 }
254 aromatic_bonds
255 .iter()
256 .map(|&bidx| {
257 let order = if double_bonds.contains(&bidx) {
258 BondOrder::Double
259 } else {
260 BondOrder::Single
261 };
262 (bidx, order)
263 })
264 .collect()
265}
266
267pub fn apply_kekule(mol: &Molecule, kekule: &KekuleResult) -> Molecule {
272 use crate::molecule::MoleculeBuilder;
273
274 let mut builder = MoleculeBuilder::new();
275
276 for (_, atom) in mol.atoms() {
278 builder.add_atom(atom.clone());
279 }
280
281 for (bidx, bond) in mol.bonds() {
283 let order = kekule.get(&bidx).copied().unwrap_or(bond.order);
284 builder
285 .add_bond(bond.atom1, bond.atom2, order)
286 .expect("duplicate bond during apply_kekule");
287 }
288
289 builder.build()
290}
291
292fn augment(
300 start: AtomIdx,
301 adj: &HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>>,
302 matching: &mut HashMap<AtomIdx, AtomIdx>,
303 visited: &mut HashSet<AtomIdx>,
304) -> bool {
305 let mut parent: HashMap<AtomIdx, AtomIdx> = HashMap::new();
307 let mut queue: std::collections::VecDeque<AtomIdx> = std::collections::VecDeque::new();
308 queue.push_back(start);
309
310 'bfs: while let Some(v) = queue.pop_front() {
311 let Some(neighbors) = adj.get(&v) else {
312 continue;
313 };
314 for &(u, _) in neighbors {
315 if !visited.insert(u) {
316 continue;
317 }
318 parent.insert(u, v);
319
320 match matching.get(&u).copied() {
321 None => {
322 let mut cur = u;
325 loop {
326 let prev = parent[&cur];
327 let prev_old_match = matching.get(&prev).copied();
328 matching.insert(prev, cur);
329 matching.insert(cur, prev);
330 match prev_old_match {
331 None | Some(_) if prev == start => break,
332 Some(m) => cur = m,
333 None => break,
334 }
335 }
336 break 'bfs;
337 }
338 Some(partner) => {
339 if visited.insert(partner) {
341 parent.insert(partner, u);
342 queue.push_back(partner);
343 }
344 }
345 }
346 }
347 }
348
349 matching.contains_key(&start)
351}
352
353fn run_matching_pass(
359 atoms: &[AtomIdx],
360 adj: &HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>>,
361 matching: &mut HashMap<AtomIdx, AtomIdx>,
362) {
363 for &start in atoms {
364 if matching.contains_key(&start) {
365 continue;
366 }
367 let mut visited: HashSet<AtomIdx> = HashSet::new();
368 visited.insert(start);
369 augment(start, adj, matching, &mut visited);
370 }
371}
372
373const NONE: usize = usize::MAX;
382
383fn blossom_max_matching(n: usize, adj: &[Vec<usize>]) -> Vec<usize> {
385 let mut mate = vec![NONE; n];
386 for v in 0..n {
387 if mate[v] == NONE {
388 blossom_augment(v, n, adj, &mut mate);
389 }
390 }
391 mate
392}
393
394fn blossom_augment(root: usize, n: usize, adj: &[Vec<usize>], mate: &mut [usize]) {
396 let mut base: Vec<usize> = (0..n).collect();
398 let mut parent: Vec<usize> = vec![NONE; n];
400 let mut is_outer: Vec<bool> = vec![false; n];
402
403 is_outer[root] = true;
404 let mut queue: VecDeque<usize> = VecDeque::new();
405 queue.push_back(root);
406
407 'bfs: while let Some(v) = queue.pop_front() {
408 for &w in &adj[v] {
409 if base[v] == base[w] {
410 continue;
411 } if mate[v] == w {
413 continue;
414 } if is_outer[w] {
417 let b = blossom_lca(v, w, &base, &parent, mate, n);
419 blossom_mark_path(
420 v,
421 b,
422 w,
423 &mut base,
424 &mut parent,
425 &mut is_outer,
426 &mut queue,
427 mate,
428 n,
429 );
430 blossom_mark_path(
431 w,
432 b,
433 v,
434 &mut base,
435 &mut parent,
436 &mut is_outer,
437 &mut queue,
438 mate,
439 n,
440 );
441 } else if parent[w] == NONE {
442 parent[w] = v;
444 if mate[w] == NONE {
445 let mut cur = w;
447 while cur != NONE {
448 let prev = parent[cur];
449 let prev_old = mate[prev];
450 mate[cur] = prev;
451 mate[prev] = cur;
452 cur = prev_old;
453 }
454 break 'bfs;
455 }
456 let u = mate[w];
458 if !is_outer[u] {
459 is_outer[u] = true;
460 parent[u] = w;
461 queue.push_back(u);
462 }
463 }
464 }
466 }
467}
468
469fn blossom_lca(
474 mut a: usize,
475 mut b: usize,
476 base: &[usize],
477 parent: &[usize],
478 mate: &[usize],
479 n: usize,
480) -> usize {
481 let mut visited = vec![false; n];
482 loop {
483 a = base[a];
484 visited[a] = true;
485 if mate[a] == NONE {
486 break;
487 } a = parent[mate[a]]; }
490 loop {
491 b = base[b];
492 if visited[b] {
493 return b;
494 } b = parent[mate[b]];
496 }
497}
498
499#[allow(clippy::too_many_arguments)]
502fn blossom_mark_path(
503 mut x: usize,
504 b: usize,
505 child: usize,
506 base: &mut [usize],
507 parent: &mut [usize],
508 is_outer: &mut [bool],
509 queue: &mut VecDeque<usize>,
510 mate: &[usize],
511 n: usize,
512) {
513 let mut ch = child;
514 while base[x] != b {
515 let bx = base[x];
516 let bmx = base[mate[x]];
517 for slot in base.iter_mut().take(n) {
519 if *slot == bx || *slot == bmx {
520 *slot = b;
521 }
522 }
523 parent[x] = ch;
525 let mx = mate[x];
527 if !is_outer[mx] {
528 is_outer[mx] = true;
529 queue.push_back(mx);
530 }
531 ch = mx;
532 x = parent[mx];
533 }
534}
535
536fn atom_must_be_matched(mol: &Molecule, idx: AtomIdx) -> bool {
553 let atom = mol.atom(idx);
554 match atom.element.atomic_number() {
555 8 | 16 | 34 => false,
557 5 => true,
560 7 if matches!(atom.hydrogen_count, Some(h) if h > 0) => false,
562 7 if atom.charge < 0 => false,
565 7 if atom.charge == 0
570 && mol
571 .neighbors(idx)
572 .any(|(_, bidx)| mol.bond(bidx).order != BondOrder::Aromatic) =>
573 {
574 false
575 }
576 7 => true,
578 _ if atom.charge < 0 => false,
580 _ if mol.neighbors(idx).any(|(_, bidx)| {
585 let o = mol.bond(bidx).order;
586 o == BondOrder::Double || o == BondOrder::Triple
587 }) =>
588 {
589 false
590 }
591 _ => true,
593 }
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599 use crate::atom::Atom;
600 use crate::element::Element;
601 use crate::molecule::MoleculeBuilder;
602
603 fn benzene() -> Molecule {
605 let mut b = MoleculeBuilder::new();
606 let atoms: Vec<_> = (0..6)
607 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
608 .collect();
609 for i in 0..6 {
610 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
611 .unwrap();
612 }
613 b.build()
614 }
615
616 fn pyridine() -> Molecule {
618 let mut b = MoleculeBuilder::new();
619 let c1 = b.add_atom(Atom::aromatic(Element::C));
620 let c2 = b.add_atom(Atom::aromatic(Element::C));
621 let c3 = b.add_atom(Atom::aromatic(Element::C));
622 let n = b.add_atom(Atom::aromatic(Element::N));
623 let c4 = b.add_atom(Atom::aromatic(Element::C));
624 let c5 = b.add_atom(Atom::aromatic(Element::C));
625 let atoms = [c1, c2, c3, n, c4, c5];
626 for i in 0..6 {
627 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
628 .unwrap();
629 }
630 b.build()
631 }
632
633 fn furan() -> Molecule {
635 let mut b = MoleculeBuilder::new();
636 let o = b.add_atom(Atom::aromatic(Element::O));
637 let c1 = b.add_atom(Atom::aromatic(Element::C));
638 let c2 = b.add_atom(Atom::aromatic(Element::C));
639 let c3 = b.add_atom(Atom::aromatic(Element::C));
640 let c4 = b.add_atom(Atom::aromatic(Element::C));
641 let atoms = [o, c1, c2, c3, c4];
642 for i in 0..5 {
643 b.add_bond(atoms[i], atoms[(i + 1) % 5], BondOrder::Aromatic)
644 .unwrap();
645 }
646 b.build()
647 }
648
649 fn pyrrole() -> Molecule {
651 let mut b = MoleculeBuilder::new();
652 let mut n_atom = Atom::aromatic(Element::N);
654 n_atom.hydrogen_count = Some(1);
655 let n = b.add_atom(n_atom);
656 let c1 = b.add_atom(Atom::aromatic(Element::C));
657 let c2 = b.add_atom(Atom::aromatic(Element::C));
658 let c3 = b.add_atom(Atom::aromatic(Element::C));
659 let c4 = b.add_atom(Atom::aromatic(Element::C));
660 let atoms = [n, c1, c2, c3, c4];
661 for i in 0..5 {
662 b.add_bond(atoms[i], atoms[(i + 1) % 5], BondOrder::Aromatic)
663 .unwrap();
664 }
665 b.build()
666 }
667
668 #[test]
669 fn test_kekulize_benzene() {
670 let mol = benzene();
671 let result = kekulize(&mol).expect("benzene kekulization failed");
672 assert_eq!(result.len(), 6); let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
675 let singles = result.values().filter(|&&o| o == BondOrder::Single).count();
676 assert_eq!(doubles, 3, "benzene must have 3 double bonds");
677 assert_eq!(singles, 3, "benzene must have 3 single bonds");
678 }
679
680 #[test]
681 fn test_kekulize_pyridine() {
682 let mol = pyridine();
683 let result = kekulize(&mol).expect("pyridine kekulization failed");
684 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
685 assert_eq!(doubles, 3, "pyridine must have 3 double bonds");
686 }
687
688 #[test]
689 fn test_kekulize_furan() {
690 let mol = furan();
691 let result = kekulize(&mol).expect("furan kekulization failed");
692 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
693 assert_eq!(doubles, 2, "furan must have 2 double bonds");
694 }
695
696 #[test]
697 fn test_kekulize_pyrrole() {
698 let mol = pyrrole();
699 let result = kekulize(&mol).expect("pyrrole kekulization failed");
700 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
701 assert_eq!(doubles, 2, "pyrrole must have 2 double bonds");
702 }
703
704 #[test]
705 fn test_kekulize_naphthalene() {
706 let mut b = MoleculeBuilder::new();
708 let atoms: Vec<_> = (0..10)
709 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
710 .collect();
711 let ring1 = [0, 1, 2, 3, 4, 9];
713 for i in 0..ring1.len() {
714 b.add_bond(
715 atoms[ring1[i]],
716 atoms[ring1[(i + 1) % ring1.len()]],
717 BondOrder::Aromatic,
718 )
719 .unwrap();
720 }
721 let ring2 = [4, 5, 6, 7, 8, 9];
723 for i in 0..ring2.len() {
724 let a = atoms[ring2[i]];
725 let bb = atoms[ring2[(i + 1) % ring2.len()]];
726 if mol_has_no_bond_yet(&b, a, bb) {
728 b.add_bond(a, bb, BondOrder::Aromatic).unwrap();
729 }
730 }
731 let mol = b.build();
732 let result = kekulize(&mol).expect("naphthalene kekulization failed");
733 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
734 assert_eq!(doubles, 5, "naphthalene must have 5 double bonds");
735 }
736
737 #[test]
738 fn test_apply_kekule() {
739 let mol = benzene();
740 let kekule = kekulize(&mol).unwrap();
741 let kekule_mol = apply_kekule(&mol, &kekule);
742
743 for (_, bond) in kekule_mol.bonds() {
745 assert_ne!(
746 bond.order,
747 BondOrder::Aromatic,
748 "apply_kekule should remove all aromatic bonds"
749 );
750 }
751 }
752
753 #[test]
754 fn test_no_aromatic_bonds_noop() {
755 let mut b = MoleculeBuilder::new();
757 let c1 = b.add_atom(Atom::new(Element::C));
758 let c2 = b.add_atom(Atom::new(Element::C));
759 b.add_bond(c1, c2, BondOrder::Single).unwrap();
760 let mol = b.build();
761 let result = kekulize(&mol).unwrap();
762 assert!(result.is_empty());
763 }
764
765 fn mol_has_no_bond_yet(b: &MoleculeBuilder, a: AtomIdx, bb: AtomIdx) -> bool {
767 for (_, partner) in b.atom_neighbors(a) {
768 if partner == bb {
769 return false;
770 }
771 }
772 true
773 }
774
775 #[test]
780 fn test_kekulize_azulene() {
781 let mut b = MoleculeBuilder::new();
782 let a: Vec<_> = (0..10)
783 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
784 .collect();
785 for i in 0..5 {
787 b.add_bond(a[i], a[(i + 1) % 5], BondOrder::Aromatic)
788 .unwrap();
789 }
790 for (x, y) in [(4usize, 5usize), (5, 6), (6, 7), (7, 8), (8, 9), (9, 0)] {
792 if mol_has_no_bond_yet(&b, a[x], a[y]) {
793 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
794 }
795 }
796 let mol = b.build();
797 let result = kekulize(&mol).expect("azulene kekulization failed");
798 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
799 assert_eq!(doubles, 5, "azulene needs 5 double bonds");
800 }
801
802 #[test]
804 fn test_kekulize_acenaphthylene() {
805 let mut b = MoleculeBuilder::new();
809 let a: Vec<_> = (0..12)
810 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
811 .collect();
812 for (x, y) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 9), (9, 0)] {
814 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
815 }
816 for (x, y) in [(4, 5), (5, 6), (6, 7), (7, 8), (8, 9)] {
818 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
819 }
820 for (x, y) in [(0, 11), (11, 10), (10, 1)] {
822 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
823 }
824 let mol = b.build();
825 let result = kekulize(&mol).expect("acenaphthylene kekulization failed");
826 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
827 assert_eq!(doubles, 6, "acenaphthylene needs 6 double bonds");
828 }
829
830 fn biphenylene() -> Molecule {
839 let mut b = MoleculeBuilder::new();
840 let a: Vec<_> = (0..12)
841 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
842 .collect();
843 for i in 0..6 {
845 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic)
846 .unwrap();
847 }
848 for i in 0..6 {
850 b.add_bond(a[6 + i], a[6 + (i + 1) % 6], BondOrder::Aromatic)
851 .unwrap();
852 }
853 b.add_bond(a[5], a[6], BondOrder::Aromatic).unwrap();
855 b.add_bond(a[0], a[11], BondOrder::Aromatic).unwrap();
856 b.build()
857 }
858
859 fn anthracene() -> Molecule {
862 let mut b = MoleculeBuilder::new();
863 let a: Vec<_> = (0..14)
864 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
865 .collect();
866 for i in 0..6 {
868 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic)
869 .unwrap();
870 }
871 for (x, y) in [(4, 9), (9, 8), (8, 7), (7, 6), (6, 5)] {
873 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
874 }
875 for (x, y) in [(7, 13), (13, 12), (12, 11), (11, 10), (10, 6)] {
877 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
878 }
879 b.build()
880 }
881
882 #[test]
883 fn test_kekulize_biphenylene() {
884 let mol = biphenylene();
887 let result = kekulize(&mol).expect("biphenylene kekulization should succeed");
888 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
889 assert_eq!(doubles, 6, "biphenylene needs 6 double bonds");
890 }
891
892 #[test]
893 fn test_kekulize_anthracene() {
894 let mol = anthracene();
896 let result = kekulize(&mol).expect("anthracene kekulization should succeed");
897 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
898 assert_eq!(doubles, 7, "anthracene needs 7 double bonds");
899 }
900
901 #[test]
902 fn test_kekulize_biphenylene_double_bond_count() {
903 let mol = biphenylene();
905 let result = kekulize(&mol).expect("biphenylene kekulization");
906 let singles = result.values().filter(|&&o| o == BondOrder::Single).count();
907 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
908 assert_eq!(singles + doubles, 14, "biphenylene has 14 aromatic bonds");
909 }
910
911 #[test]
912 fn test_kekulize_large_fused_6rings() {
913 let mol = {
916 let mut b = MoleculeBuilder::new();
917 let a: Vec<_> = (0..16)
918 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
919 .collect();
920 for i in 0..6 {
922 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic)
923 .unwrap();
924 }
925 for (x, y) in [(4, 9), (9, 8), (8, 7), (7, 6), (6, 5)] {
927 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
928 }
929 for (x, y) in [(2, 11), (11, 10), (10, 13), (13, 12), (12, 1)] {
931 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
932 }
933 for (x, y) in [(7, 15), (15, 14), (14, 11)] {
935 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
936 }
937 b.build()
938 };
939 let result = kekulize(&mol).expect("4-ring PAH kekulization should succeed");
940 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
941 assert!(
942 doubles >= 6,
943 "4-ring PAH needs at least 6 double bonds, got {doubles}"
944 );
945 }
946
947 #[test]
948 fn test_kekulize_deterministic() {
949 let mol1 = biphenylene();
951 let mol2 = biphenylene();
952 let r1 = kekulize(&mol1).expect("pass1");
953 let r2 = kekulize(&mol2).expect("pass2");
954 assert_eq!(
955 r1.values().filter(|&&o| o == BondOrder::Double).count(),
956 r2.values().filter(|&&o| o == BondOrder::Double).count(),
957 "kekulization must be deterministic"
958 );
959 }
960
961 #[test]
963 fn test_kekulize_fluoranthene() {
964 let mut b = MoleculeBuilder::new();
973 let a: Vec<_> = (0..16)
974 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
975 .collect();
976 for i in 0..6 {
978 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic)
979 .unwrap();
980 }
981 for (x, y) in [(5, 6), (6, 7), (7, 8), (8, 9), (9, 0)] {
983 if mol_has_no_bond_yet(&b, a[x], a[y]) {
984 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
985 }
986 }
987 for (x, y) in [(2, 10), (10, 11), (11, 12), (12, 13), (13, 1)] {
989 if mol_has_no_bond_yet(&b, a[x], a[y]) {
990 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
991 }
992 }
993 for (x, y) in [(8, 14), (14, 15), (15, 13), (13, 9)] {
995 if mol_has_no_bond_yet(&b, a[x], a[y]) {
996 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
997 }
998 }
999 let mol = b.build();
1000 let result = kekulize(&mol).expect("fluoranthene-like kekulization failed");
1001 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1002 assert_eq!(
1003 doubles, 8,
1004 "fluoranthene-like structure needs 8 double bonds"
1005 );
1006 }
1007
1008 #[test]
1012 fn kekulize_indolizine() {
1013 let mut b = MoleculeBuilder::new();
1015 let c: Vec<_> = (0..9)
1016 .map(|i| {
1017 if i == 3 {
1018 b.add_atom(Atom::aromatic(Element::N))
1019 } else {
1020 b.add_atom(Atom::aromatic(Element::C))
1021 }
1022 })
1023 .collect();
1024 for (x, y) in [
1025 (0, 1),
1026 (1, 2),
1027 (2, 3),
1028 (3, 4),
1029 (4, 5),
1030 (5, 6),
1031 (6, 7),
1032 (7, 3),
1033 (7, 8),
1034 (8, 0),
1035 ] {
1036 b.add_bond(c[x], c[y], BondOrder::Aromatic).unwrap();
1037 }
1038 let mol = b.build();
1039 let result = kekulize(&mol);
1040 assert!(
1041 result.is_ok(),
1042 "indolizine kekulization failed: {:?}",
1043 result.err()
1044 );
1045 let doubles = result
1046 .unwrap()
1047 .values()
1048 .filter(|&&o| o == BondOrder::Double)
1049 .count();
1050 assert_eq!(doubles, 4, "indolizine: 4 double bonds (N lone-pair donor)");
1051 }
1052
1053 #[test]
1056 fn kekulize_quinolizine() {
1057 let mut b = MoleculeBuilder::new();
1059 let c: Vec<_> = (0..10)
1060 .map(|i| {
1061 if i == 3 {
1062 b.add_atom(Atom::aromatic(Element::N))
1063 } else {
1064 b.add_atom(Atom::aromatic(Element::C))
1065 }
1066 })
1067 .collect();
1068 for (x, y) in [
1069 (0, 1),
1070 (1, 2),
1071 (2, 3),
1072 (3, 4),
1073 (4, 5),
1074 (5, 6),
1075 (6, 7),
1076 (7, 8),
1077 (8, 3),
1078 (8, 9),
1079 (9, 0),
1080 ] {
1081 b.add_bond(c[x], c[y], BondOrder::Aromatic).unwrap();
1082 }
1083 let mol = b.build();
1084 let result = kekulize(&mol);
1085 assert!(
1086 result.is_ok(),
1087 "quinolizine kekulization failed: {:?}",
1088 result.err()
1089 );
1090 let doubles = result
1091 .unwrap()
1092 .values()
1093 .filter(|&&o| o == BondOrder::Double)
1094 .count();
1095 assert_eq!(doubles, 5, "quinolizine: 5 double bonds");
1096 }
1097
1098 #[test]
1102 fn kekulize_corannulene() {
1103 let mut b = MoleculeBuilder::new();
1109 let a: Vec<_> = (0..20)
1110 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
1111 .collect();
1112 let edges: &[(usize, usize)] = &[
1113 (0, 1),
1115 (1, 2),
1116 (2, 3),
1117 (3, 4),
1118 (4, 0),
1119 (0, 5),
1121 (1, 7),
1122 (2, 9),
1123 (3, 11),
1124 (4, 13),
1125 (5, 6),
1127 (6, 7),
1128 (7, 8),
1129 (8, 9),
1130 (9, 10),
1131 (10, 11),
1132 (11, 12),
1133 (12, 13),
1134 (13, 14),
1135 (14, 5),
1136 (5, 15),
1138 (6, 15),
1139 (7, 16),
1140 (8, 16),
1141 (9, 17),
1142 (10, 17),
1143 (11, 18),
1144 (12, 18),
1145 (13, 19),
1146 (14, 19),
1147 ];
1148 for &(x, y) in edges {
1149 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
1150 }
1151 let mol = b.build();
1152 let result = kekulize(&mol);
1153 assert!(
1154 result.is_ok(),
1155 "corannulene kekulization failed: {:?}",
1156 result.err()
1157 );
1158 let doubles = result
1159 .unwrap()
1160 .values()
1161 .filter(|&&o| o == BondOrder::Double)
1162 .count();
1163 assert_eq!(doubles, 10, "corannulene: 10 double bonds");
1164 }
1165
1166 #[test]
1170 fn kekulize_boron_azine() {
1171 let mut b = MoleculeBuilder::new();
1174 let atoms: Vec<_> = (0..6)
1175 .map(|i| {
1176 if i == 0 {
1177 b.add_atom(Atom::aromatic(Element::B))
1178 } else if i == 5 {
1179 b.add_atom(Atom::aromatic(Element::N))
1180 } else {
1181 b.add_atom(Atom::aromatic(Element::C))
1182 }
1183 })
1184 .collect();
1185 for i in 0..6 {
1186 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
1187 .unwrap();
1188 }
1189 let mol = b.build();
1190 let result = kekulize(&mol);
1191 assert!(
1192 result.is_ok(),
1193 "b1ccccn1 kekulization failed: {:?}",
1194 result.err()
1195 );
1196 let doubles = result
1197 .unwrap()
1198 .values()
1199 .filter(|&&o| o == BondOrder::Double)
1200 .count();
1201 assert_eq!(doubles, 3, "b1ccccn1: 3 double bonds");
1202 }
1203}