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
237pub fn 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 {
283 use crate::molecule::MoleculeBuilder;
284
285 if kekule.is_empty() {
286 return mol.clone();
287 }
288
289 let mut builder = MoleculeBuilder::new();
290
291 for (_, atom) in mol.atoms() {
293 builder.add_atom(atom.clone());
294 }
295
296 for (bidx, bond) in mol.bonds() {
298 let order = kekule.get(&bidx).copied().unwrap_or(bond.order);
299 builder
300 .add_bond(bond.atom1, bond.atom2, order)
301 .expect("duplicate bond during apply_kekule");
302 }
303
304 builder.copy_stereo_groups_from(mol);
305 builder.copy_stereo_from(mol);
306 builder.copy_bond_directions_from(mol);
307
308 builder.build()
309}
310
311fn augment(
319 start: AtomIdx,
320 adj: &HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>>,
321 matching: &mut HashMap<AtomIdx, AtomIdx>,
322 visited: &mut HashSet<AtomIdx>,
323) -> bool {
324 let mut parent: HashMap<AtomIdx, AtomIdx> = HashMap::new();
326 let mut queue: std::collections::VecDeque<AtomIdx> = std::collections::VecDeque::new();
327 queue.push_back(start);
328
329 'bfs: while let Some(v) = queue.pop_front() {
330 let Some(neighbors) = adj.get(&v) else {
331 continue;
332 };
333 for &(u, _) in neighbors {
334 if !visited.insert(u) {
335 continue;
336 }
337 parent.insert(u, v);
338
339 match matching.get(&u).copied() {
340 None => {
341 let mut cur = u;
344 loop {
345 let prev = parent[&cur];
346 let prev_old_match = matching.get(&prev).copied();
347 matching.insert(prev, cur);
348 matching.insert(cur, prev);
349 match prev_old_match {
350 None | Some(_) if prev == start => break,
351 Some(m) => cur = m,
352 None => break,
353 }
354 }
355 break 'bfs;
356 }
357 Some(partner) => {
358 if visited.insert(partner) {
360 parent.insert(partner, u);
361 queue.push_back(partner);
362 }
363 }
364 }
365 }
366 }
367
368 matching.contains_key(&start)
370}
371
372fn run_matching_pass(
378 atoms: &[AtomIdx],
379 adj: &HashMap<AtomIdx, Vec<(AtomIdx, BondIdx)>>,
380 matching: &mut HashMap<AtomIdx, AtomIdx>,
381) {
382 for &start in atoms {
383 if matching.contains_key(&start) {
384 continue;
385 }
386 let mut visited: HashSet<AtomIdx> = HashSet::new();
387 visited.insert(start);
388 augment(start, adj, matching, &mut visited);
389 }
390}
391
392const NONE: usize = usize::MAX;
401
402fn blossom_max_matching(n: usize, adj: &[Vec<usize>]) -> Vec<usize> {
404 let mut mate = vec![NONE; n];
405 for v in 0..n {
406 if mate[v] == NONE {
407 blossom_augment(v, n, adj, &mut mate);
408 }
409 }
410 mate
411}
412
413fn blossom_augment(root: usize, n: usize, adj: &[Vec<usize>], mate: &mut [usize]) {
415 let mut base: Vec<usize> = (0..n).collect();
417 let mut parent: Vec<usize> = vec![NONE; n];
419 let mut is_outer: Vec<bool> = vec![false; n];
421
422 is_outer[root] = true;
423 let mut queue: VecDeque<usize> = VecDeque::new();
424 queue.push_back(root);
425
426 'bfs: while let Some(v) = queue.pop_front() {
427 for &w in &adj[v] {
428 if base[v] == base[w] {
429 continue;
430 } if mate[v] == w {
432 continue;
433 } if is_outer[w] {
436 let b = blossom_lca(v, w, &base, &parent, mate, n);
438 blossom_mark_path(
439 v,
440 b,
441 w,
442 &mut base,
443 &mut parent,
444 &mut is_outer,
445 &mut queue,
446 mate,
447 n,
448 );
449 blossom_mark_path(
450 w,
451 b,
452 v,
453 &mut base,
454 &mut parent,
455 &mut is_outer,
456 &mut queue,
457 mate,
458 n,
459 );
460 } else if parent[w] == NONE {
461 parent[w] = v;
463 if mate[w] == NONE {
464 let mut cur = w;
466 while cur != NONE {
467 let prev = parent[cur];
468 let prev_old = mate[prev];
469 mate[cur] = prev;
470 mate[prev] = cur;
471 cur = prev_old;
472 }
473 break 'bfs;
474 }
475 let u = mate[w];
477 if !is_outer[u] {
478 is_outer[u] = true;
479 parent[u] = w;
480 queue.push_back(u);
481 }
482 }
483 }
485 }
486}
487
488fn blossom_lca(
493 mut a: usize,
494 mut b: usize,
495 base: &[usize],
496 parent: &[usize],
497 mate: &[usize],
498 n: usize,
499) -> usize {
500 let mut visited = vec![false; n];
501 loop {
502 a = base[a];
503 visited[a] = true;
504 if mate[a] == NONE {
505 break;
506 } a = parent[mate[a]]; }
509 loop {
510 b = base[b];
511 if visited[b] {
512 return b;
513 } b = parent[mate[b]];
515 }
516}
517
518#[allow(clippy::too_many_arguments)]
521fn blossom_mark_path(
522 mut x: usize,
523 b: usize,
524 child: usize,
525 base: &mut [usize],
526 parent: &mut [usize],
527 is_outer: &mut [bool],
528 queue: &mut VecDeque<usize>,
529 mate: &[usize],
530 n: usize,
531) {
532 let mut ch = child;
533 while base[x] != b {
534 let bx = base[x];
535 let bmx = base[mate[x]];
536 for slot in base.iter_mut().take(n) {
538 if *slot == bx || *slot == bmx {
539 *slot = b;
540 }
541 }
542 parent[x] = ch;
544 let mx = mate[x];
546 if !is_outer[mx] {
547 is_outer[mx] = true;
548 queue.push_back(mx);
549 }
550 ch = mx;
551 x = parent[mx];
552 }
553}
554
555pub fn atom_must_be_matched(mol: &Molecule, idx: AtomIdx) -> bool {
596 let atom = mol.atom(idx);
597 match atom.element.atomic_number() {
598 8 | 16 | 34 | 52 if atom.charge <= 0 => false,
603 5 => true,
606 7 | 15 if atom.charge <= 0 && matches!(atom.hydrogen_count, Some(h) if h > 0) => false,
610 7 | 15 if atom.charge < 0 => false,
613 7 | 15
618 if atom.charge == 0
619 && mol
620 .neighbors(idx)
621 .any(|(_, bidx)| mol.bond(bidx).order != BondOrder::Aromatic) =>
622 {
623 false
624 }
625 7 | 15 => true,
628 6 if atom.charge > 0 => false,
634 _ if atom.charge < 0 => false,
636 _ if mol.neighbors(idx).any(|(_, bidx)| {
641 let o = mol.bond(bidx).order;
642 o == BondOrder::Double || o == BondOrder::Triple
643 }) =>
644 {
645 false
646 }
647 _ => true,
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655 use crate::atom::Atom;
656 use crate::element::Element;
657 use crate::molecule::{MoleculeBuilder, STEREO_H_SENTINEL};
658 use crate::stereo_group::StereoGroup;
659
660 fn benzene() -> Molecule {
662 let mut b = MoleculeBuilder::new();
663 let atoms: Vec<_> = (0..6)
664 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
665 .collect();
666 for i in 0..6 {
667 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
668 .unwrap();
669 }
670 b.build()
671 }
672
673 fn pyridine() -> Molecule {
675 let mut b = MoleculeBuilder::new();
676 let c1 = b.add_atom(Atom::aromatic(Element::C));
677 let c2 = b.add_atom(Atom::aromatic(Element::C));
678 let c3 = b.add_atom(Atom::aromatic(Element::C));
679 let n = b.add_atom(Atom::aromatic(Element::N));
680 let c4 = b.add_atom(Atom::aromatic(Element::C));
681 let c5 = b.add_atom(Atom::aromatic(Element::C));
682 let atoms = [c1, c2, c3, n, c4, c5];
683 for i in 0..6 {
684 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
685 .unwrap();
686 }
687 b.build()
688 }
689
690 fn furan() -> Molecule {
692 let mut b = MoleculeBuilder::new();
693 let o = b.add_atom(Atom::aromatic(Element::O));
694 let c1 = b.add_atom(Atom::aromatic(Element::C));
695 let c2 = b.add_atom(Atom::aromatic(Element::C));
696 let c3 = b.add_atom(Atom::aromatic(Element::C));
697 let c4 = b.add_atom(Atom::aromatic(Element::C));
698 let atoms = [o, c1, c2, c3, c4];
699 for i in 0..5 {
700 b.add_bond(atoms[i], atoms[(i + 1) % 5], BondOrder::Aromatic)
701 .unwrap();
702 }
703 b.build()
704 }
705
706 fn pyrrole() -> Molecule {
708 let mut b = MoleculeBuilder::new();
709 let mut n_atom = Atom::aromatic(Element::N);
711 n_atom.hydrogen_count = Some(1);
712 let n = b.add_atom(n_atom);
713 let c1 = b.add_atom(Atom::aromatic(Element::C));
714 let c2 = b.add_atom(Atom::aromatic(Element::C));
715 let c3 = b.add_atom(Atom::aromatic(Element::C));
716 let c4 = b.add_atom(Atom::aromatic(Element::C));
717 let atoms = [n, c1, c2, c3, c4];
718 for i in 0..5 {
719 b.add_bond(atoms[i], atoms[(i + 1) % 5], BondOrder::Aromatic)
720 .unwrap();
721 }
722 b.build()
723 }
724
725 #[test]
726 fn test_kekulize_benzene() {
727 let mol = benzene();
728 let result = kekulize(&mol).expect("benzene kekulization failed");
729 assert_eq!(result.len(), 6); let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
732 let singles = result.values().filter(|&&o| o == BondOrder::Single).count();
733 assert_eq!(doubles, 3, "benzene must have 3 double bonds");
734 assert_eq!(singles, 3, "benzene must have 3 single bonds");
735 }
736
737 #[test]
738 fn test_kekulize_pyridine() {
739 let mol = pyridine();
740 let result = kekulize(&mol).expect("pyridine kekulization failed");
741 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
742 assert_eq!(doubles, 3, "pyridine must have 3 double bonds");
743 }
744
745 #[test]
746 fn test_kekulize_furan() {
747 let mol = furan();
748 let result = kekulize(&mol).expect("furan kekulization failed");
749 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
750 assert_eq!(doubles, 2, "furan must have 2 double bonds");
751 }
752
753 #[test]
754 fn test_kekulize_pyrrole() {
755 let mol = pyrrole();
756 let result = kekulize(&mol).expect("pyrrole kekulization failed");
757 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
758 assert_eq!(doubles, 2, "pyrrole must have 2 double bonds");
759 }
760
761 #[test]
762 fn test_kekulize_naphthalene() {
763 let mut b = MoleculeBuilder::new();
765 let atoms: Vec<_> = (0..10)
766 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
767 .collect();
768 let ring1 = [0, 1, 2, 3, 4, 9];
770 for i in 0..ring1.len() {
771 b.add_bond(
772 atoms[ring1[i]],
773 atoms[ring1[(i + 1) % ring1.len()]],
774 BondOrder::Aromatic,
775 )
776 .unwrap();
777 }
778 let ring2 = [4, 5, 6, 7, 8, 9];
780 for i in 0..ring2.len() {
781 let a = atoms[ring2[i]];
782 let bb = atoms[ring2[(i + 1) % ring2.len()]];
783 if mol_has_no_bond_yet(&b, a, bb) {
785 b.add_bond(a, bb, BondOrder::Aromatic).unwrap();
786 }
787 }
788 let mol = b.build();
789 let result = kekulize(&mol).expect("naphthalene kekulization failed");
790 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
791 assert_eq!(doubles, 5, "naphthalene must have 5 double bonds");
792 }
793
794 #[test]
795 fn test_apply_kekule() {
796 let mol = benzene();
797 let kekule = kekulize(&mol).unwrap();
798 let kekule_mol = apply_kekule(&mol, &kekule);
799
800 for (_, bond) in kekule_mol.bonds() {
802 assert_ne!(
803 bond.order,
804 BondOrder::Aromatic,
805 "apply_kekule should remove all aromatic bonds"
806 );
807 }
808 }
809
810 #[test]
811 fn test_no_aromatic_bonds_noop() {
812 let mut b = MoleculeBuilder::new();
814 let c1 = b.add_atom(Atom::new(Element::C));
815 let c2 = b.add_atom(Atom::new(Element::C));
816 b.add_bond(c1, c2, BondOrder::Single).unwrap();
817 let mol = b.build();
818 let result = kekulize(&mol).unwrap();
819 assert!(result.is_empty());
820 }
821
822 fn mol_has_no_bond_yet(b: &MoleculeBuilder, a: AtomIdx, bb: AtomIdx) -> bool {
824 for (_, partner) in b.atom_neighbors(a) {
825 if partner == bb {
826 return false;
827 }
828 }
829 true
830 }
831
832 #[test]
837 fn test_kekulize_azulene() {
838 let mut b = MoleculeBuilder::new();
839 let a: Vec<_> = (0..10)
840 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
841 .collect();
842 for i in 0..5 {
844 b.add_bond(a[i], a[(i + 1) % 5], BondOrder::Aromatic)
845 .unwrap();
846 }
847 for (x, y) in [(4usize, 5usize), (5, 6), (6, 7), (7, 8), (8, 9), (9, 0)] {
849 if mol_has_no_bond_yet(&b, a[x], a[y]) {
850 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
851 }
852 }
853 let mol = b.build();
854 let result = kekulize(&mol).expect("azulene kekulization failed");
855 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
856 assert_eq!(doubles, 5, "azulene needs 5 double bonds");
857 }
858
859 #[test]
861 fn test_kekulize_acenaphthylene() {
862 let mut b = MoleculeBuilder::new();
866 let a: Vec<_> = (0..12)
867 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
868 .collect();
869 for (x, y) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 9), (9, 0)] {
871 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
872 }
873 for (x, y) in [(4, 5), (5, 6), (6, 7), (7, 8), (8, 9)] {
875 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
876 }
877 for (x, y) in [(0, 11), (11, 10), (10, 1)] {
879 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
880 }
881 let mol = b.build();
882 let result = kekulize(&mol).expect("acenaphthylene kekulization failed");
883 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
884 assert_eq!(doubles, 6, "acenaphthylene needs 6 double bonds");
885 }
886
887 fn biphenylene() -> Molecule {
896 let mut b = MoleculeBuilder::new();
897 let a: Vec<_> = (0..12)
898 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
899 .collect();
900 for i in 0..6 {
902 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic)
903 .unwrap();
904 }
905 for i in 0..6 {
907 b.add_bond(a[6 + i], a[6 + (i + 1) % 6], BondOrder::Aromatic)
908 .unwrap();
909 }
910 b.add_bond(a[5], a[6], BondOrder::Aromatic).unwrap();
912 b.add_bond(a[0], a[11], BondOrder::Aromatic).unwrap();
913 b.build()
914 }
915
916 fn anthracene() -> Molecule {
919 let mut b = MoleculeBuilder::new();
920 let a: Vec<_> = (0..14)
921 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
922 .collect();
923 for i in 0..6 {
925 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic)
926 .unwrap();
927 }
928 for (x, y) in [(4, 9), (9, 8), (8, 7), (7, 6), (6, 5)] {
930 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
931 }
932 for (x, y) in [(7, 13), (13, 12), (12, 11), (11, 10), (10, 6)] {
934 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
935 }
936 b.build()
937 }
938
939 #[test]
940 fn test_kekulize_biphenylene() {
941 let mol = biphenylene();
944 let result = kekulize(&mol).expect("biphenylene kekulization should succeed");
945 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
946 assert_eq!(doubles, 6, "biphenylene needs 6 double bonds");
947 }
948
949 #[test]
950 fn test_kekulize_anthracene() {
951 let mol = anthracene();
953 let result = kekulize(&mol).expect("anthracene kekulization should succeed");
954 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
955 assert_eq!(doubles, 7, "anthracene needs 7 double bonds");
956 }
957
958 #[test]
959 fn test_kekulize_biphenylene_double_bond_count() {
960 let mol = biphenylene();
962 let result = kekulize(&mol).expect("biphenylene kekulization");
963 let singles = result.values().filter(|&&o| o == BondOrder::Single).count();
964 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
965 assert_eq!(singles + doubles, 14, "biphenylene has 14 aromatic bonds");
966 }
967
968 #[test]
969 fn test_kekulize_large_fused_6rings() {
970 let mol = {
973 let mut b = MoleculeBuilder::new();
974 let a: Vec<_> = (0..16)
975 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
976 .collect();
977 for i in 0..6 {
979 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic)
980 .unwrap();
981 }
982 for (x, y) in [(4, 9), (9, 8), (8, 7), (7, 6), (6, 5)] {
984 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
985 }
986 for (x, y) in [(2, 11), (11, 10), (10, 13), (13, 12), (12, 1)] {
988 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
989 }
990 for (x, y) in [(7, 15), (15, 14), (14, 11)] {
992 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
993 }
994 b.build()
995 };
996 let result = kekulize(&mol).expect("4-ring PAH kekulization should succeed");
997 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
998 assert!(
999 doubles >= 6,
1000 "4-ring PAH needs at least 6 double bonds, got {doubles}"
1001 );
1002 }
1003
1004 #[test]
1005 fn test_kekulize_deterministic() {
1006 let mol1 = biphenylene();
1008 let mol2 = biphenylene();
1009 let r1 = kekulize(&mol1).expect("pass1");
1010 let r2 = kekulize(&mol2).expect("pass2");
1011 assert_eq!(
1012 r1.values().filter(|&&o| o == BondOrder::Double).count(),
1013 r2.values().filter(|&&o| o == BondOrder::Double).count(),
1014 "kekulization must be deterministic"
1015 );
1016 }
1017
1018 #[test]
1020 fn test_kekulize_fluoranthene() {
1021 let mut b = MoleculeBuilder::new();
1030 let a: Vec<_> = (0..16)
1031 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
1032 .collect();
1033 for i in 0..6 {
1035 b.add_bond(a[i], a[(i + 1) % 6], BondOrder::Aromatic)
1036 .unwrap();
1037 }
1038 for (x, y) in [(5, 6), (6, 7), (7, 8), (8, 9), (9, 0)] {
1040 if mol_has_no_bond_yet(&b, a[x], a[y]) {
1041 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
1042 }
1043 }
1044 for (x, y) in [(2, 10), (10, 11), (11, 12), (12, 13), (13, 1)] {
1046 if mol_has_no_bond_yet(&b, a[x], a[y]) {
1047 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
1048 }
1049 }
1050 for (x, y) in [(8, 14), (14, 15), (15, 13), (13, 9)] {
1052 if mol_has_no_bond_yet(&b, a[x], a[y]) {
1053 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
1054 }
1055 }
1056 let mol = b.build();
1057 let result = kekulize(&mol).expect("fluoranthene-like kekulization failed");
1058 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1059 assert_eq!(
1060 doubles, 8,
1061 "fluoranthene-like structure needs 8 double bonds"
1062 );
1063 }
1064
1065 #[test]
1069 fn kekulize_indolizine() {
1070 let mut b = MoleculeBuilder::new();
1072 let c: Vec<_> = (0..9)
1073 .map(|i| {
1074 if i == 3 {
1075 b.add_atom(Atom::aromatic(Element::N))
1076 } else {
1077 b.add_atom(Atom::aromatic(Element::C))
1078 }
1079 })
1080 .collect();
1081 for (x, y) in [
1082 (0, 1),
1083 (1, 2),
1084 (2, 3),
1085 (3, 4),
1086 (4, 5),
1087 (5, 6),
1088 (6, 7),
1089 (7, 3),
1090 (7, 8),
1091 (8, 0),
1092 ] {
1093 b.add_bond(c[x], c[y], BondOrder::Aromatic).unwrap();
1094 }
1095 let mol = b.build();
1096 let result = kekulize(&mol);
1097 assert!(
1098 result.is_ok(),
1099 "indolizine kekulization failed: {:?}",
1100 result.err()
1101 );
1102 let doubles = result
1103 .unwrap()
1104 .values()
1105 .filter(|&&o| o == BondOrder::Double)
1106 .count();
1107 assert_eq!(doubles, 4, "indolizine: 4 double bonds (N lone-pair donor)");
1108 }
1109
1110 #[test]
1113 fn kekulize_quinolizine() {
1114 let mut b = MoleculeBuilder::new();
1116 let c: Vec<_> = (0..10)
1117 .map(|i| {
1118 if i == 3 {
1119 b.add_atom(Atom::aromatic(Element::N))
1120 } else {
1121 b.add_atom(Atom::aromatic(Element::C))
1122 }
1123 })
1124 .collect();
1125 for (x, y) in [
1126 (0, 1),
1127 (1, 2),
1128 (2, 3),
1129 (3, 4),
1130 (4, 5),
1131 (5, 6),
1132 (6, 7),
1133 (7, 8),
1134 (8, 3),
1135 (8, 9),
1136 (9, 0),
1137 ] {
1138 b.add_bond(c[x], c[y], BondOrder::Aromatic).unwrap();
1139 }
1140 let mol = b.build();
1141 let result = kekulize(&mol);
1142 assert!(
1143 result.is_ok(),
1144 "quinolizine kekulization failed: {:?}",
1145 result.err()
1146 );
1147 let doubles = result
1148 .unwrap()
1149 .values()
1150 .filter(|&&o| o == BondOrder::Double)
1151 .count();
1152 assert_eq!(doubles, 5, "quinolizine: 5 double bonds");
1153 }
1154
1155 #[test]
1159 fn kekulize_corannulene() {
1160 let mut b = MoleculeBuilder::new();
1166 let a: Vec<_> = (0..20)
1167 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
1168 .collect();
1169 let edges: &[(usize, usize)] = &[
1170 (0, 1),
1172 (1, 2),
1173 (2, 3),
1174 (3, 4),
1175 (4, 0),
1176 (0, 5),
1178 (1, 7),
1179 (2, 9),
1180 (3, 11),
1181 (4, 13),
1182 (5, 6),
1184 (6, 7),
1185 (7, 8),
1186 (8, 9),
1187 (9, 10),
1188 (10, 11),
1189 (11, 12),
1190 (12, 13),
1191 (13, 14),
1192 (14, 5),
1193 (5, 15),
1195 (6, 15),
1196 (7, 16),
1197 (8, 16),
1198 (9, 17),
1199 (10, 17),
1200 (11, 18),
1201 (12, 18),
1202 (13, 19),
1203 (14, 19),
1204 ];
1205 for &(x, y) in edges {
1206 b.add_bond(a[x], a[y], BondOrder::Aromatic).unwrap();
1207 }
1208 let mol = b.build();
1209 let result = kekulize(&mol);
1210 assert!(
1211 result.is_ok(),
1212 "corannulene kekulization failed: {:?}",
1213 result.err()
1214 );
1215 let doubles = result
1216 .unwrap()
1217 .values()
1218 .filter(|&&o| o == BondOrder::Double)
1219 .count();
1220 assert_eq!(doubles, 10, "corannulene: 10 double bonds");
1221 }
1222
1223 #[test]
1227 fn kekulize_boron_azine() {
1228 let mut b = MoleculeBuilder::new();
1231 let atoms: Vec<_> = (0..6)
1232 .map(|i| {
1233 if i == 0 {
1234 b.add_atom(Atom::aromatic(Element::B))
1235 } else if i == 5 {
1236 b.add_atom(Atom::aromatic(Element::N))
1237 } else {
1238 b.add_atom(Atom::aromatic(Element::C))
1239 }
1240 })
1241 .collect();
1242 for i in 0..6 {
1243 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
1244 .unwrap();
1245 }
1246 let mol = b.build();
1247 let result = kekulize(&mol);
1248 assert!(
1249 result.is_ok(),
1250 "b1ccccn1 kekulization failed: {:?}",
1251 result.err()
1252 );
1253 let doubles = result
1254 .unwrap()
1255 .values()
1256 .filter(|&&o| o == BondOrder::Double)
1257 .count();
1258 assert_eq!(doubles, 3, "b1ccccn1: 3 double bonds");
1259 }
1260
1261 fn chiral_aromatic_with_stereo_metadata() -> (Molecule, AtomIdx, BondIdx) {
1275 let mut b = MoleculeBuilder::new();
1276 let c1 = b.add_atom(Atom::aromatic(Element::C));
1277 let c2 = b.add_atom(Atom::aromatic(Element::C));
1278 let c3 = b.add_atom(Atom::aromatic(Element::C));
1279 let c4 = b.add_atom(Atom::aromatic(Element::C));
1280 let o = b.add_atom(Atom::aromatic(Element::O));
1281 for i in 0..4 {
1282 let ring = [c1, c2, c3, c4, o];
1283 b.add_bond(ring[i], ring[i + 1], BondOrder::Aromatic)
1284 .unwrap();
1285 }
1286 b.add_bond(o, c1, BondOrder::Aromatic).unwrap();
1287
1288 let mut chiral = Atom::new(Element::C);
1290 chiral.chirality = crate::atom::Chirality::CounterClockwise;
1291 let ch = b.add_atom(chiral);
1292 let f = b.add_atom(Atom::new(Element::F));
1293 let cl = b.add_atom(Atom::new(Element::CL));
1294 let exocyclic_bond = b.add_bond(c1, ch, BondOrder::Single).unwrap();
1295 b.add_bond(ch, f, BondOrder::Single).unwrap();
1296 b.add_bond(ch, cl, BondOrder::Single).unwrap();
1297
1298 let mut mol = b.build();
1299 mol.set_stereo_neighbor_order(ch, vec![c1.0, STEREO_H_SENTINEL, f.0, cl.0]);
1301 mol.add_stereo_group(StereoGroup::new(
1302 crate::stereo_group::StereoGroupKind::Absolute,
1303 vec![ch],
1304 ));
1305 mol.set_bond_direction(exocyclic_bond, BondOrder::Up);
1306
1307 (mol, ch, exocyclic_bond)
1308 }
1309
1310 #[test]
1311 fn apply_kekule_preserves_stereo_neighbor_order() {
1312 let (mol, ch, _) = chiral_aromatic_with_stereo_metadata();
1313 let before = mol.stereo_neighbor_order(ch).map(|s| s.to_vec());
1314 assert!(before.is_some(), "test setup sanity");
1315
1316 let result = kekulize(&mol).expect("kekulizable");
1317 assert!(
1318 !result.is_empty(),
1319 "test setup sanity: ring must need kekulization"
1320 );
1321 let kekulized = apply_kekule(&mol, &result);
1322
1323 assert_eq!(
1324 kekulized.stereo_neighbor_order(ch).map(|s| s.to_vec()),
1325 before,
1326 "stereo_neighbor_order must survive apply_kekule verbatim"
1327 );
1328 }
1329
1330 #[test]
1331 fn apply_kekule_preserves_stereo_groups() {
1332 let (mol, _, _) = chiral_aromatic_with_stereo_metadata();
1333 let before = mol.stereo_groups().to_vec();
1334 assert!(!before.is_empty(), "test setup sanity");
1335
1336 let result = kekulize(&mol).expect("kekulizable");
1337 let kekulized = apply_kekule(&mol, &result);
1338
1339 assert_eq!(
1340 kekulized.stereo_groups(),
1341 before.as_slice(),
1342 "stereo_groups must survive apply_kekule verbatim"
1343 );
1344 }
1345
1346 #[test]
1347 fn apply_kekule_preserves_bond_directions() {
1348 let (mol, _, exocyclic_bond) = chiral_aromatic_with_stereo_metadata();
1349 let before = mol.bond_direction(exocyclic_bond);
1350 assert!(before.is_some(), "test setup sanity");
1351
1352 let result = kekulize(&mol).expect("kekulizable");
1353 let kekulized = apply_kekule(&mol, &result);
1354
1355 assert_eq!(
1356 kekulized.bond_direction(exocyclic_bond),
1357 before,
1358 "bond_directions must survive apply_kekule verbatim"
1359 );
1360 }
1361
1362 #[test]
1363 fn apply_kekule_preserves_atom_and_bond_index_mapping() {
1364 let (mol, _, _) = chiral_aromatic_with_stereo_metadata();
1365 let result = kekulize(&mol).expect("kekulizable");
1366 let kekulized = apply_kekule(&mol, &result);
1367
1368 assert_eq!(kekulized.atom_count(), mol.atom_count());
1369 assert_eq!(kekulized.bond_count(), mol.bond_count());
1370 for (idx, atom) in mol.atoms() {
1371 let after = kekulized.atom(idx);
1372 assert_eq!(atom.element, after.element, "atom {idx:?} element moved");
1373 assert_eq!(
1374 atom.chirality, after.chirality,
1375 "atom {idx:?} chirality moved"
1376 );
1377 }
1378 for (bidx, bond) in mol.bonds() {
1379 let after = kekulized.bond(bidx);
1380 assert_eq!(bond.atom1, after.atom1, "bond {bidx:?} atom1 moved");
1381 assert_eq!(bond.atom2, after.atom2, "bond {bidx:?} atom2 moved");
1382 }
1383 }
1384
1385 #[test]
1386 fn apply_kekule_empty_result_is_full_clone() {
1387 let mut b = MoleculeBuilder::new();
1391 let mut chiral = Atom::new(Element::C);
1392 chiral.chirality = crate::atom::Chirality::Clockwise;
1393 let c = b.add_atom(chiral);
1394 let f = b.add_atom(Atom::new(Element::F));
1395 let cl = b.add_atom(Atom::new(Element::CL));
1396 let br = b.add_atom(Atom::new(Element::BR));
1397 let h_bond = b.add_atom(Atom::new(Element::I));
1398 b.add_bond(c, f, BondOrder::Single).unwrap();
1399 b.add_bond(c, cl, BondOrder::Single).unwrap();
1400 b.add_bond(c, br, BondOrder::Single).unwrap();
1401 b.add_bond(c, h_bond, BondOrder::Single).unwrap();
1402
1403 let mut mol = b.build();
1404 mol.set_stereo_neighbor_order(c, vec![f.0, cl.0, br.0, h_bond.0]);
1405
1406 let result = kekulize(&mol).expect("no aromatic bonds -- trivially kekulizable");
1407 assert!(result.is_empty(), "test setup sanity: no aromatic bonds");
1408
1409 let kekulized = apply_kekule(&mol, &result);
1410 assert_eq!(
1411 kekulized.stereo_neighbor_order(c).map(|s| s.to_vec()),
1412 mol.stereo_neighbor_order(c).map(|s| s.to_vec())
1413 );
1414 assert_eq!(kekulized.atom_count(), mol.atom_count());
1415 assert_eq!(kekulized.bond_count(), mol.bond_count());
1416 }
1417
1418 fn tropylium_cation() -> (Molecule, AtomIdx) {
1430 let mut b = MoleculeBuilder::new();
1431 let mut atoms: Vec<AtomIdx> = (0..4)
1432 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
1433 .collect();
1434 let mut cation = Atom::aromatic(Element::C);
1435 cation.charge = 1;
1436 cation.hydrogen_count = Some(1);
1437 let cation_idx = b.add_atom(cation); atoms.push(cation_idx);
1439 atoms.extend((0..2).map(|_| b.add_atom(Atom::aromatic(Element::C))));
1440 for i in 0..7 {
1441 b.add_bond(atoms[i], atoms[(i + 1) % 7], BondOrder::Aromatic)
1442 .unwrap();
1443 }
1444 (b.build(), cation_idx)
1445 }
1446
1447 fn imidazolium() -> Molecule {
1450 let mut b = MoleculeBuilder::new();
1451 let c0 = b.add_atom(Atom::aromatic(Element::C));
1452 let c1 = b.add_atom(Atom::aromatic(Element::C));
1453 let mut n_plus = Atom::aromatic(Element::N);
1454 n_plus.charge = 1;
1455 n_plus.hydrogen_count = Some(1);
1456 let n2 = b.add_atom(n_plus);
1457 let c3 = b.add_atom(Atom::aromatic(Element::C));
1458 let mut n_neutral = Atom::aromatic(Element::N);
1459 n_neutral.hydrogen_count = Some(1);
1460 let n4 = b.add_atom(n_neutral);
1461 let atoms = [c0, c1, n2, c3, n4];
1462 for i in 0..5 {
1463 b.add_bond(atoms[i], atoms[(i + 1) % 5], BondOrder::Aromatic)
1464 .unwrap();
1465 }
1466 b.build()
1467 }
1468
1469 fn pyridinium() -> Molecule {
1471 let mut b = MoleculeBuilder::new();
1472 let mut atoms: Vec<AtomIdx> = (0..5)
1473 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
1474 .collect();
1475 let mut n_plus = Atom::aromatic(Element::N);
1476 n_plus.charge = 1;
1477 n_plus.hydrogen_count = Some(1);
1478 atoms.insert(3, b.add_atom(n_plus)); for i in 0..6 {
1480 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
1481 .unwrap();
1482 }
1483 b.build()
1484 }
1485
1486 fn pyrylium() -> Molecule {
1488 let mut b = MoleculeBuilder::new();
1489 let mut atoms: Vec<AtomIdx> = (0..5)
1490 .map(|_| b.add_atom(Atom::aromatic(Element::C)))
1491 .collect();
1492 let mut o_plus = Atom::aromatic(Element::O);
1493 o_plus.charge = 1;
1494 atoms.insert(3, b.add_atom(o_plus)); for i in 0..6 {
1496 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Aromatic)
1497 .unwrap();
1498 }
1499 b.build()
1500 }
1501
1502 fn tellurophene() -> Molecule {
1504 let mut b = MoleculeBuilder::new();
1505 let te = b.add_atom(Atom::aromatic(Element::TE));
1506 let c1 = b.add_atom(Atom::aromatic(Element::C));
1507 let c2 = b.add_atom(Atom::aromatic(Element::C));
1508 let c3 = b.add_atom(Atom::aromatic(Element::C));
1509 let c4 = b.add_atom(Atom::aromatic(Element::C));
1510 let ring = [te, c1, c2, c3, c4];
1511 for i in 0..5 {
1512 b.add_bond(ring[i], ring[(i + 1) % 5], BondOrder::Aromatic)
1513 .unwrap();
1514 }
1515 b.build()
1516 }
1517
1518 fn phosphole() -> Molecule {
1520 let mut b = MoleculeBuilder::new();
1521 let mut p_atom = Atom::aromatic(Element::P);
1522 p_atom.hydrogen_count = Some(1);
1523 let p = b.add_atom(p_atom);
1524 let c1 = b.add_atom(Atom::aromatic(Element::C));
1525 let c2 = b.add_atom(Atom::aromatic(Element::C));
1526 let c3 = b.add_atom(Atom::aromatic(Element::C));
1527 let c4 = b.add_atom(Atom::aromatic(Element::C));
1528 let ring = [p, c1, c2, c3, c4];
1529 for i in 0..5 {
1530 b.add_bond(ring[i], ring[(i + 1) % 5], BondOrder::Aromatic)
1531 .unwrap();
1532 }
1533 b.build()
1534 }
1535
1536 fn thiophene() -> Molecule {
1539 let mut b = MoleculeBuilder::new();
1540 let s = b.add_atom(Atom::aromatic(Element::S));
1541 let c1 = b.add_atom(Atom::aromatic(Element::C));
1542 let c2 = b.add_atom(Atom::aromatic(Element::C));
1543 let c3 = b.add_atom(Atom::aromatic(Element::C));
1544 let c4 = b.add_atom(Atom::aromatic(Element::C));
1545 let ring = [s, c1, c2, c3, c4];
1546 for i in 0..5 {
1547 b.add_bond(ring[i], ring[(i + 1) % 5], BondOrder::Aromatic)
1548 .unwrap();
1549 }
1550 b.build()
1551 }
1552
1553 fn selenophene() -> Molecule {
1556 let mut b = MoleculeBuilder::new();
1557 let se = b.add_atom(Atom::aromatic(Element::SE));
1558 let c1 = b.add_atom(Atom::aromatic(Element::C));
1559 let c2 = b.add_atom(Atom::aromatic(Element::C));
1560 let c3 = b.add_atom(Atom::aromatic(Element::C));
1561 let c4 = b.add_atom(Atom::aromatic(Element::C));
1562 let ring = [se, c1, c2, c3, c4];
1563 for i in 0..5 {
1564 b.add_bond(ring[i], ring[(i + 1) % 5], BondOrder::Aromatic)
1565 .unwrap();
1566 }
1567 b.build()
1568 }
1569
1570 #[test]
1571 fn test_kekulize_tropylium_cation() {
1572 let (mol, cation_idx) = tropylium_cation();
1573 let result = kekulize(&mol).expect("tropylium cation kekulization failed");
1574 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1575 assert_eq!(
1576 doubles, 3,
1577 "tropylium: 6 neutral C alternate into 3 double bonds"
1578 );
1579 for (bidx, order) in &result {
1581 let bond = mol.bond(*bidx);
1582 if bond.atom1 == cation_idx || bond.atom2 == cation_idx {
1583 assert_eq!(
1584 *order,
1585 BondOrder::Single,
1586 "cationic C must not get a double bond"
1587 );
1588 }
1589 }
1590 }
1591
1592 #[test]
1593 fn test_kekulize_imidazolium() {
1594 let mol = imidazolium();
1595 let result = kekulize(&mol).expect("imidazolium kekulization failed");
1596 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1597 assert_eq!(
1598 doubles, 2,
1599 "imidazolium: 2 double bonds ([nH+] matched, [nH] not)"
1600 );
1601 }
1602
1603 #[test]
1604 fn test_kekulize_pyridinium() {
1605 let mol = pyridinium();
1606 let result = kekulize(&mol).expect("pyridinium kekulization failed");
1607 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1608 assert_eq!(
1609 doubles, 3,
1610 "pyridinium: 3 double bonds, same as neutral pyridine"
1611 );
1612 }
1613
1614 #[test]
1615 fn test_kekulize_pyrylium() {
1616 let mol = pyrylium();
1617 let result = kekulize(&mol).expect("pyrylium kekulization failed");
1618 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1619 assert_eq!(
1620 doubles, 3,
1621 "pyrylium: 3 double bonds, O+ matched like pyridine's N"
1622 );
1623 }
1624
1625 #[test]
1626 fn test_kekulize_tellurophene() {
1627 let mol = tellurophene();
1628 let result = kekulize(&mol).expect("tellurophene kekulization failed");
1629 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1630 assert_eq!(
1631 doubles, 2,
1632 "tellurophene: 2 double bonds, Te donates its lone pair"
1633 );
1634 }
1635
1636 #[test]
1637 fn test_kekulize_phosphole() {
1638 let mol = phosphole();
1639 let result = kekulize(&mol).expect("phosphole kekulization failed");
1640 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1641 assert_eq!(
1642 doubles, 2,
1643 "phosphole: 2 double bonds, [pH] donates its lone pair like [nH]"
1644 );
1645 }
1646
1647 #[test]
1648 fn test_kekulize_thiophene_regression() {
1649 let mol = thiophene();
1650 let result = kekulize(&mol).expect("thiophene kekulization failed");
1651 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1652 assert_eq!(
1653 doubles, 2,
1654 "thiophene must still kekulize after the K1 charge-aware fix"
1655 );
1656 }
1657
1658 #[test]
1659 fn test_kekulize_selenophene_regression() {
1660 let mol = selenophene();
1661 let result = kekulize(&mol).expect("selenophene kekulization failed");
1662 let doubles = result.values().filter(|&&o| o == BondOrder::Double).count();
1663 assert_eq!(
1664 doubles, 2,
1665 "selenophene must still kekulize after the K1 charge-aware fix"
1666 );
1667 }
1668}