1use std::collections::HashSet;
26
27use chematic_core::{AtomIdx, BondIdx, BondOrder, Molecule, implicit_hcount};
28
29use crate::sssr::find_sssr;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum RingAromaticity {
38 Aromatic,
40 Antiaromatic,
42 NonAromatic,
44}
45
46#[derive(Debug, Clone)]
52pub struct AromaticityModel {
53 aromatic_atoms: HashSet<AtomIdx>,
54 aromatic_bonds: HashSet<BondIdx>,
55 antiaromatic_rings: Vec<Vec<AtomIdx>>,
56 ring_classifications: Vec<(Vec<AtomIdx>, RingAromaticity, u32)>,
57}
58
59impl AromaticityModel {
60 pub fn is_atom_aromatic(&self, idx: AtomIdx) -> bool {
62 self.aromatic_atoms.contains(&idx)
63 }
64
65 pub fn is_bond_aromatic(&self, idx: BondIdx) -> bool {
67 self.aromatic_bonds.contains(&idx)
68 }
69
70 pub fn aromatic_atom_count(&self) -> usize {
72 self.aromatic_atoms.len()
73 }
74
75 pub fn ring_classifications(&self) -> &[(Vec<AtomIdx>, RingAromaticity, u32)] {
80 &self.ring_classifications
81 }
82
83 pub fn antiaromatic_rings(&self) -> &[Vec<AtomIdx>] {
85 &self.antiaromatic_rings
86 }
87
88 pub fn has_antiaromaticity(&self) -> bool {
90 !self.antiaromatic_rings.is_empty()
91 }
92}
93
94#[allow(clippy::manual_is_multiple_of)]
100fn classify_ring_aromaticity(pi_electrons: u32) -> (RingAromaticity, u32) {
101 if pi_electrons >= 2 && (pi_electrons - 2) % 4 == 0 {
102 (RingAromaticity::Aromatic, pi_electrons)
103 } else if pi_electrons > 0 && pi_electrons % 4 == 0 {
104 (RingAromaticity::Antiaromatic, pi_electrons)
105 } else {
106 (RingAromaticity::NonAromatic, pi_electrons)
107 }
108}
109
110fn mark_ring_aromatic(
112 mol: &Molecule,
113 ring: &[AtomIdx],
114 aromatic_atoms: &mut HashSet<AtomIdx>,
115 aromatic_bonds: &mut HashSet<BondIdx>,
116) {
117 for &atom in ring {
118 aromatic_atoms.insert(atom);
119 }
120 for i in 0..ring.len() {
121 let a = ring[i];
122 let b = ring[(i + 1) % ring.len()];
123 if let Some((bidx, _)) = mol.bond_between(a, b) {
124 aromatic_bonds.insert(bidx);
125 }
126 }
127}
128
129pub fn assign_aromaticity(mol: &Molecule) -> AromaticityModel {
140 let ring_set = find_sssr(mol);
141 let sssr_rings = ring_set.rings();
142
143 let rings: Vec<Vec<AtomIdx>> = augmented_ring_set(mol, sssr_rings);
147
148 let mut aromatic_atoms: HashSet<AtomIdx> = HashSet::new();
149 let mut aromatic_bonds: HashSet<BondIdx> = HashSet::new();
150 let mut antiaromatic_rings: Vec<Vec<AtomIdx>> = Vec::new();
151
152 let mut classifications: Vec<Option<(RingAromaticity, u32)>> = vec![None; rings.len()];
154
155 let mut pass2_candidates: Vec<usize> = Vec::new();
158
159 let empty_context = HashSet::new();
161 for (ring_idx, ring) in rings.iter().enumerate() {
162 match ring_pi_electrons(mol, ring, &empty_context) {
163 Some(pi) => {
164 let (cls, count) = classify_ring_aromaticity(pi);
165 classifications[ring_idx] = Some((cls, count));
166 match cls {
167 RingAromaticity::Aromatic => {
168 mark_ring_aromatic(mol, ring, &mut aromatic_atoms, &mut aromatic_bonds);
169 }
170 RingAromaticity::Antiaromatic => {
171 antiaromatic_rings.push(ring.to_vec());
172 }
174 RingAromaticity::NonAromatic => {
175 pass2_candidates.push(ring_idx);
176 }
177 }
178 }
179 None => {
180 pass2_candidates.push(ring_idx);
182 }
183 }
184 }
185
186 loop {
190 let mut any_new = false;
191 let mut still_pending: Vec<usize> = Vec::new();
192
193 for ring_idx in pass2_candidates {
194 let ring = &rings[ring_idx];
195 if !ring.iter().any(|a| aromatic_atoms.contains(a)) {
197 still_pending.push(ring_idx);
198 continue;
199 }
200 match ring_pi_electrons(mol, ring, &aromatic_atoms) {
201 Some(pi) => {
202 let (cls, count) = classify_ring_aromaticity(pi);
203 classifications[ring_idx] = Some((cls, count));
204 if matches!(cls, RingAromaticity::Aromatic) {
205 mark_ring_aromatic(mol, ring, &mut aromatic_atoms, &mut aromatic_bonds);
206 any_new = true;
207 }
208 }
210 None => {
211 still_pending.push(ring_idx);
212 }
213 }
214 }
215
216 pass2_candidates = still_pending;
217 if !any_new {
218 break;
219 }
220 }
221
222 let ring_classifications: Vec<(Vec<AtomIdx>, RingAromaticity, u32)> = rings
224 .iter()
225 .take(sssr_rings.len()) .enumerate()
227 .filter_map(|(i, ring)| classifications[i].map(|(cls, count)| (ring.to_vec(), cls, count)))
228 .collect();
229
230 AromaticityModel {
231 aromatic_atoms,
232 aromatic_bonds,
233 antiaromatic_rings,
234 ring_classifications,
235 }
236}
237
238pub fn apply_aromaticity(mol: &Molecule) -> Molecule {
247 use chematic_core::{BondOrder, MoleculeBuilder};
248
249 let model = assign_aromaticity(mol);
250 let mut builder = MoleculeBuilder::new();
251
252 for (idx, atom) in mol.atoms() {
253 let mut a = atom.clone();
254 if model.is_atom_aromatic(idx) {
255 a.aromatic = true;
256 }
257 builder.add_atom(a);
258 }
259 for (bidx, bond) in mol.bonds() {
260 let order = if model.is_bond_aromatic(bidx) {
261 BondOrder::Aromatic
262 } else {
263 bond.order
264 };
265 let _ = builder.add_bond(bond.atom1, bond.atom2, order);
266 }
267 builder.build()
268}
269
270fn ring_bond_set(mol: &Molecule, ring: &[AtomIdx]) -> Vec<BondIdx> {
276 let n = ring.len();
277 let mut bonds: Vec<BondIdx> = (0..n)
278 .filter_map(|i| {
279 let a = ring[i];
280 let b = ring[(i + 1) % n];
281 mol.bond_between(a, b).map(|(bidx, _)| bidx)
282 })
283 .collect();
284 bonds.sort();
285 bonds
286}
287
288fn bond_sym_diff(a: &[BondIdx], b: &[BondIdx]) -> Vec<BondIdx> {
290 let mut result: Vec<BondIdx> = Vec::new();
291 let mut i = 0;
292 let mut j = 0;
293 while i < a.len() && j < b.len() {
294 match a[i].cmp(&b[j]) {
295 std::cmp::Ordering::Less => {
296 result.push(a[i]);
297 i += 1;
298 }
299 std::cmp::Ordering::Greater => {
300 result.push(b[j]);
301 j += 1;
302 }
303 std::cmp::Ordering::Equal => {
304 i += 1;
305 j += 1;
306 }
307 }
308 }
309 result.extend_from_slice(&a[i..]);
310 result.extend_from_slice(&b[j..]);
311 result
312}
313
314fn ring_atoms_from_bond_set(mol: &Molecule, bonds: &[BondIdx]) -> Option<Vec<AtomIdx>> {
317 if bonds.is_empty() {
318 return None;
319 }
320 let mut adj: std::collections::HashMap<AtomIdx, [Option<AtomIdx>; 2]> =
321 std::collections::HashMap::new();
322 for &bidx in bonds {
323 let bond = mol.bond(bidx);
324 for (a, b) in [(bond.atom1, bond.atom2), (bond.atom2, bond.atom1)] {
325 let e = adj.entry(a).or_insert([None; 2]);
326 if e[0].is_none() {
327 e[0] = Some(b);
328 } else if e[1].is_none() {
329 e[1] = Some(b);
330 } else {
331 return None; }
333 }
334 }
335 if adj.values().any(|e| e[1].is_none()) {
337 return None;
338 }
339 let start = *adj.keys().next()?;
340 let mut path = vec![start];
341 let mut prev = start;
342 let mut current = adj[&start][0]?;
343 while current != start {
344 path.push(current);
345 let [n0, n1] = adj[¤t];
346 let next = if n0 == Some(prev) { n1? } else { n0? };
347 prev = current;
348 current = next;
349 }
350 if path.len() != bonds.len() {
351 return None;
352 }
353 Some(path)
354}
355
356pub fn augmented_ring_set(mol: &Molecule, sssr_rings: &[Vec<AtomIdx>]) -> Vec<Vec<AtomIdx>> {
371 let mut rings: Vec<Vec<AtomIdx>> = sssr_rings.to_vec();
372
373 let mut known: std::collections::HashSet<Vec<AtomIdx>> = sssr_rings
375 .iter()
376 .map(|r| {
377 let mut s = r.clone();
378 s.sort();
379 s
380 })
381 .collect();
382
383 loop {
392 let mut changed = false;
393 let n = rings.len();
394 let bond_sets: Vec<Vec<BondIdx>> = rings.iter().map(|r| ring_bond_set(mol, r)).collect();
395
396 for i in 0..n {
397 for j in (i + 1)..n {
398 let shares_atom = rings[i].iter().any(|a| rings[j].contains(a));
400 if !shares_atom {
401 continue;
402 }
403 let xor_bonds = bond_sym_diff(&bond_sets[i], &bond_sets[j]);
404 if xor_bonds.is_empty() {
405 continue;
406 }
407 if xor_bonds.len() >= rings[i].len().max(rings[j].len()) {
415 continue;
416 }
417 if let Some(new_ring) = ring_atoms_from_bond_set(mol, &xor_bonds) {
418 let mut key = new_ring.clone();
419 key.sort();
420 if known.insert(key) {
421 rings.push(new_ring);
422 changed = true;
423 }
424 }
425 }
426 }
427
428 if !changed {
429 break;
430 }
431 }
432
433 rings
434}
435
436pub fn count_aromatic_rings(mol: &Molecule) -> usize {
444 let sssr = crate::sssr::find_sssr(mol);
445 let aug = augmented_ring_set(mol, sssr.rings());
446
447 let aromatic: Vec<Vec<AtomIdx>> = aug
449 .into_iter()
450 .filter(|ring| ring.iter().all(|&idx| mol.atom(idx).aromatic))
451 .collect();
452
453 if aromatic.len() <= 1 {
454 return aromatic.len();
455 }
456
457 let bond_sets: Vec<Vec<BondIdx>> = aromatic.iter().map(|r| ring_bond_set(mol, r)).collect();
459
460 let n = aromatic.len();
469 let mut is_envelope = vec![false; n];
470 for i in 0..n {
471 let si = aromatic[i].len();
472
473 'jk: for j in 0..n {
475 if j == i || aromatic[j].len() >= si {
476 continue;
477 }
478 for k in (j + 1)..n {
479 if k == i || aromatic[k].len() >= si {
480 continue;
481 }
482 let xor = bond_sym_diff(&bond_sets[j], &bond_sets[k]);
483 if xor == bond_sets[i] {
484 is_envelope[i] = true;
485 break 'jk;
486 }
487 }
488 }
489
490 if !is_envelope[i] {
492 'jkl: for j in 0..n {
493 if j == i || aromatic[j].len() >= si {
494 continue;
495 }
496 for k in (j + 1)..n {
497 if k == i || aromatic[k].len() >= si {
498 continue;
499 }
500 let xor_jk = bond_sym_diff(&bond_sets[j], &bond_sets[k]);
501 for l in (k + 1)..n {
502 if l == i || aromatic[l].len() >= si {
503 continue;
504 }
505 let xor_jkl = bond_sym_diff(&xor_jk, &bond_sets[l]);
506 if xor_jkl == bond_sets[i] {
507 is_envelope[i] = true;
508 break 'jkl;
509 }
510 }
511 }
512 }
513 }
514
515 if !is_envelope[i] {
519 'jklm: for j in 0..n {
520 if j == i || aromatic[j].len() >= si { continue; }
521 for k in (j + 1)..n {
522 if k == i || aromatic[k].len() >= si { continue; }
523 let xor_jk = bond_sym_diff(&bond_sets[j], &bond_sets[k]);
524 for l in (k + 1)..n {
525 if l == i || aromatic[l].len() >= si { continue; }
526 let xor_jkl = bond_sym_diff(&xor_jk, &bond_sets[l]);
527 for m in (l + 1)..n {
528 if m == i || aromatic[m].len() >= si { continue; }
529 let xor_jklm = bond_sym_diff(&xor_jkl, &bond_sets[m]);
530 if xor_jklm == bond_sets[i] {
531 is_envelope[i] = true;
532 break 'jklm;
533 }
534 }
535 }
536 }
537 }
538 }
539 }
540
541 is_envelope.iter().filter(|&&e| !e).count()
542}
543
544fn ring_pi_electrons(
569 mol: &Molecule,
570 ring: &[AtomIdx],
571 aromatic_context: &HashSet<AtomIdx>,
572) -> Option<u32> {
573 let ring_atom_set: HashSet<AtomIdx> = ring.iter().copied().collect();
574 let mut total_pi: u32 = 0;
575
576 for &atom_idx in ring {
577 if aromatic_context.contains(&atom_idx) {
579 total_pi += 1;
580 continue;
581 }
582
583 let atom = mol.atom(atom_idx);
584 let an = atom.element.atomic_number();
585
586 let ring_degree = mol
587 .neighbors(atom_idx)
588 .filter(|(nb, _)| ring_atom_set.contains(nb))
589 .count();
590
591 let total_degree = mol.degree(atom_idx);
592
593 let has_explicit_double = mol
595 .neighbors(atom_idx)
596 .any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Double);
597
598 let has_double_any = has_explicit_double
600 || mol
601 .neighbors(atom_idx)
602 .any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Aromatic);
603
604 let has_aromatic_in_ring = mol
606 .neighbors(atom_idx)
607 .filter(|(nb, _)| ring_atom_set.contains(nb))
608 .any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Aromatic);
609
610 let pi = match an {
611 6 => {
613 if !has_double_any {
614 return None; }
616 1
617 }
618
619 7 => {
621 if implicit_hcount(mol, atom_idx) > 0 {
622 2
624 } else if has_explicit_double {
625 1
627 } else if total_degree == 3 && ring_degree < total_degree {
628 2
632 } else if has_aromatic_in_ring {
633 1
636 } else {
637 return None;
639 }
640 }
641
642 8 | 16 => {
644 if ring_degree != 2 {
645 return None;
646 }
647 2
648 }
649
650 _ => return None,
652 };
653
654 total_pi += pi;
655 }
656
657 Some(total_pi)
658}
659
660#[cfg(test)]
665mod tests {
666 use super::*;
667 use chematic_core::{Atom, BondOrder, Element, MoleculeBuilder};
668
669 fn benzene_kekule() -> chematic_core::Molecule {
674 let mut b = MoleculeBuilder::new();
675 let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
676 for i in 0..6 {
677 let order = if i % 2 == 0 {
678 BondOrder::Double
679 } else {
680 BondOrder::Single
681 };
682 b.add_bond(atoms[i], atoms[(i + 1) % 6], order).unwrap();
683 }
684 b.build()
685 }
686
687 fn cyclohexane() -> chematic_core::Molecule {
688 let mut b = MoleculeBuilder::new();
689 let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
690 for i in 0..6 {
691 b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Single)
692 .unwrap();
693 }
694 b.build()
695 }
696
697 fn pyridine_kekule() -> chematic_core::Molecule {
698 let mut b = MoleculeBuilder::new();
699 let n = b.add_atom(Atom::new(Element::N));
700 let atoms_c: Vec<_> = (0..5).map(|_| b.add_atom(Atom::new(Element::C))).collect();
701 let ring = [
702 n, atoms_c[0], atoms_c[1], atoms_c[2], atoms_c[3], atoms_c[4],
703 ];
704 for i in 0..6 {
705 let order = if i % 2 == 0 {
706 BondOrder::Double
707 } else {
708 BondOrder::Single
709 };
710 b.add_bond(ring[i], ring[(i + 1) % 6], order).unwrap();
711 }
712 b.build()
713 }
714
715 fn furan_kekule() -> chematic_core::Molecule {
716 let mut b = MoleculeBuilder::new();
717 let o = b.add_atom(Atom::new(Element::O));
718 let c1 = b.add_atom(Atom::new(Element::C));
719 let c2 = b.add_atom(Atom::new(Element::C));
720 let c3 = b.add_atom(Atom::new(Element::C));
721 let c4 = b.add_atom(Atom::new(Element::C));
722 let ring = [o, c1, c2, c3, c4];
723 b.add_bond(ring[0], ring[1], BondOrder::Single).unwrap();
724 b.add_bond(ring[1], ring[2], BondOrder::Double).unwrap();
725 b.add_bond(ring[2], ring[3], BondOrder::Single).unwrap();
726 b.add_bond(ring[3], ring[4], BondOrder::Double).unwrap();
727 b.add_bond(ring[4], ring[0], BondOrder::Single).unwrap();
728 b.build()
729 }
730
731 fn pyrrole_kekule() -> chematic_core::Molecule {
732 let mut b = MoleculeBuilder::new();
733 let mut n_atom = Atom::new(Element::N);
734 n_atom.hydrogen_count = Some(1);
735 let n = b.add_atom(n_atom);
736 let c1 = b.add_atom(Atom::new(Element::C));
737 let c2 = b.add_atom(Atom::new(Element::C));
738 let c3 = b.add_atom(Atom::new(Element::C));
739 let c4 = b.add_atom(Atom::new(Element::C));
740 let ring = [n, c1, c2, c3, c4];
741 b.add_bond(ring[0], ring[1], BondOrder::Single).unwrap();
742 b.add_bond(ring[1], ring[2], BondOrder::Double).unwrap();
743 b.add_bond(ring[2], ring[3], BondOrder::Single).unwrap();
744 b.add_bond(ring[3], ring[4], BondOrder::Double).unwrap();
745 b.add_bond(ring[4], ring[0], BondOrder::Single).unwrap();
746 b.build()
747 }
748
749 fn naphthalene_kekule() -> chematic_core::Molecule {
750 let mut b = MoleculeBuilder::new();
751 let atoms: Vec<_> = (0..10).map(|_| b.add_atom(Atom::new(Element::C))).collect();
752 let ring1 = [0usize, 1, 2, 3, 4, 9];
753 let orders1 = [
754 BondOrder::Double,
755 BondOrder::Single,
756 BondOrder::Double,
757 BondOrder::Single,
758 BondOrder::Double,
759 BondOrder::Single,
760 ];
761 for i in 0..6 {
762 b.add_bond(atoms[ring1[i]], atoms[ring1[(i + 1) % 6]], orders1[i])
763 .unwrap();
764 }
765 let ring2_extra = [(4, 5), (5, 6), (6, 7), (7, 8), (8, 9)];
766 let orders2 = [
767 BondOrder::Single,
768 BondOrder::Double,
769 BondOrder::Single,
770 BondOrder::Double,
771 BondOrder::Single,
772 ];
773 for (i, &(a, bb)) in ring2_extra.iter().enumerate() {
774 b.add_bond(atoms[a], atoms[bb], orders2[i]).unwrap();
775 }
776 b.build()
777 }
778
779 fn cyclobutadiene_kekule() -> chematic_core::Molecule {
780 let mut b = MoleculeBuilder::new();
781 let atoms: Vec<_> = (0..4).map(|_| b.add_atom(Atom::new(Element::C))).collect();
782 for i in 0..4 {
783 let order = if i % 2 == 0 {
784 BondOrder::Double
785 } else {
786 BondOrder::Single
787 };
788 b.add_bond(atoms[i], atoms[(i + 1) % 4], order).unwrap();
789 }
790 b.build()
791 }
792
793 fn cyclooctatetraene_kekule() -> chematic_core::Molecule {
794 let mut b = MoleculeBuilder::new();
795 let atoms: Vec<_> = (0..8).map(|_| b.add_atom(Atom::new(Element::C))).collect();
796 for i in 0..8 {
797 let order = if i % 2 == 0 {
798 BondOrder::Double
799 } else {
800 BondOrder::Single
801 };
802 b.add_bond(atoms[i], atoms[(i + 1) % 8], order).unwrap();
803 }
804 b.build()
805 }
806
807 #[cfg(test)]
810 fn mol_aromatic(smiles: &str) -> chematic_core::Molecule {
811 chematic_smiles::parse(smiles).expect("valid SMILES")
812 }
813
814 #[cfg(test)]
816 fn mol_kekulized(smiles: &str) -> chematic_core::Molecule {
817 let mol = chematic_smiles::parse(smiles).expect("valid SMILES");
818 let k = chematic_core::kekulize(&mol).expect("kekulizable");
819 chematic_core::apply_kekule(&mol, &k)
820 }
821
822 #[test]
827 fn test_benzene_is_aromatic() {
828 let mol = benzene_kekule();
829 let model = assign_aromaticity(&mol);
830 assert_eq!(
831 model.aromatic_atom_count(),
832 6,
833 "all 6 benzene atoms aromatic"
834 );
835 for i in 0..6u32 {
836 assert!(model.is_atom_aromatic(AtomIdx(i)));
837 }
838 }
839
840 #[test]
841 fn test_cyclohexane_not_aromatic() {
842 let mol = cyclohexane();
843 let model = assign_aromaticity(&mol);
844 assert_eq!(model.aromatic_atom_count(), 0, "cyclohexane not aromatic");
845 }
846
847 #[test]
848 fn test_pyridine_is_aromatic() {
849 let mol = pyridine_kekule();
850 let model = assign_aromaticity(&mol);
851 assert_eq!(model.aromatic_atom_count(), 6);
852 }
853
854 #[test]
855 fn test_furan_is_aromatic() {
856 let mol = furan_kekule();
857 let model = assign_aromaticity(&mol);
858 assert_eq!(model.aromatic_atom_count(), 5);
859 }
860
861 #[test]
862 fn test_pyrrole_is_aromatic() {
863 let mol = pyrrole_kekule();
864 let model = assign_aromaticity(&mol);
865 assert_eq!(model.aromatic_atom_count(), 5);
866 }
867
868 #[test]
869 fn test_naphthalene_both_rings_aromatic() {
870 let mol = naphthalene_kekule();
871 let model = assign_aromaticity(&mol);
872 assert_eq!(
873 model.aromatic_atom_count(),
874 10,
875 "all 10 naphthalene atoms aromatic"
876 );
877 }
878
879 #[test]
880 fn test_bond_aromaticity_benzene() {
881 let mol = benzene_kekule();
882 let model = assign_aromaticity(&mol);
883 let count = mol
884 .bonds()
885 .filter(|(b, _)| model.is_bond_aromatic(*b))
886 .count();
887 assert_eq!(count, 6);
888 }
889
890 #[test]
891 fn test_apply_aromaticity_benzene() {
892 let mol = benzene_kekule();
893 let aromatic = apply_aromaticity(&mol);
894 for (_, atom) in aromatic.atoms() {
895 assert!(atom.aromatic, "every benzene carbon should be aromatic");
896 }
897 let aromatic_bond_count = aromatic
898 .bonds()
899 .filter(|(_, b)| b.order == BondOrder::Aromatic)
900 .count();
901 assert_eq!(aromatic_bond_count, 6);
902 }
903
904 #[test]
905 fn test_apply_aromaticity_cyclohexane_unchanged() {
906 let mol = cyclohexane();
907 let result = apply_aromaticity(&mol);
908 for (_, atom) in result.atoms() {
909 assert!(!atom.aromatic);
910 }
911 for (_, bond) in result.bonds() {
912 assert_ne!(bond.order, BondOrder::Aromatic);
913 }
914 }
915
916 #[test]
921 fn test_cyclobutadiene_antiaromatic() {
922 let mol = cyclobutadiene_kekule();
923 let model = assign_aromaticity(&mol);
924 assert_eq!(
925 model.aromatic_atom_count(),
926 0,
927 "cyclobutadiene not aromatic"
928 );
929 assert!(model.has_antiaromaticity(), "cyclobutadiene antiaromatic");
930 assert_eq!(model.antiaromatic_rings().len(), 1);
931 let classifications = model.ring_classifications();
932 assert_eq!(classifications.len(), 1);
933 assert_eq!(classifications[0].1, RingAromaticity::Antiaromatic);
934 assert_eq!(classifications[0].2, 4);
935 }
936
937 #[test]
938 fn test_cyclooctatetraene_antiaromatic() {
939 let mol = cyclooctatetraene_kekule();
940 let model = assign_aromaticity(&mol);
941 assert_eq!(model.aromatic_atom_count(), 0, "COT not aromatic");
942 assert!(model.has_antiaromaticity(), "COT antiaromatic");
943 assert_eq!(model.antiaromatic_rings().len(), 1);
944 let cls = &model.ring_classifications()[0];
945 assert_eq!(cls.1, RingAromaticity::Antiaromatic);
946 assert_eq!(cls.2, 8);
947 }
948
949 #[test]
954 fn test_ring_classifications_benzene() {
955 let mol = benzene_kekule();
956 let model = assign_aromaticity(&mol);
957 let classifications = model.ring_classifications();
958 assert_eq!(classifications.len(), 1);
959 assert_eq!(classifications[0].1, RingAromaticity::Aromatic);
960 assert_eq!(classifications[0].2, 6);
961 }
962
963 #[test]
964 fn test_ring_classifications_naphthalene() {
965 let mol = naphthalene_kekule();
966 let model = assign_aromaticity(&mol);
967 let classifications = model.ring_classifications();
968 assert_eq!(classifications.len(), 2, "naphthalene has two rings");
969 for (_, classification, count) in classifications {
970 assert_eq!(*classification, RingAromaticity::Aromatic);
971 assert_eq!(*count, 6);
972 }
973 }
974
975 #[test]
976 fn test_non_aromatic_cyclohexane() {
977 let mol = cyclohexane();
978 let model = assign_aromaticity(&mol);
979 for (_, classification, _) in model.ring_classifications() {
980 assert_ne!(*classification, RingAromaticity::Aromatic);
981 assert_ne!(*classification, RingAromaticity::Antiaromatic);
982 }
983 }
984
985 #[test]
990 fn test_thiophene_aromatic() {
991 let mut b = MoleculeBuilder::new();
992 let s = b.add_atom(Atom::new(Element::S));
993 let c1 = b.add_atom(Atom::new(Element::C));
994 let c2 = b.add_atom(Atom::new(Element::C));
995 let c3 = b.add_atom(Atom::new(Element::C));
996 let c4 = b.add_atom(Atom::new(Element::C));
997 let ring = [s, c1, c2, c3, c4];
998 b.add_bond(ring[0], ring[1], BondOrder::Single).unwrap();
999 b.add_bond(ring[1], ring[2], BondOrder::Double).unwrap();
1000 b.add_bond(ring[2], ring[3], BondOrder::Single).unwrap();
1001 b.add_bond(ring[3], ring[4], BondOrder::Double).unwrap();
1002 b.add_bond(ring[4], ring[0], BondOrder::Single).unwrap();
1003 let mol = b.build();
1004 let model = assign_aromaticity(&mol);
1005 assert_eq!(model.aromatic_atom_count(), 5);
1006 assert_eq!(model.ring_classifications()[0].2, 6);
1007 }
1008
1009 #[test]
1010 fn test_electron_distribution_tracking() {
1011 let mol = benzene_kekule();
1012 let model = assign_aromaticity(&mol);
1013 assert_eq!(model.ring_classifications()[0].2, 6, "benzene: 6 × 1π = 6");
1014
1015 let mol = pyrrole_kekule();
1016 let model = assign_aromaticity(&mol);
1017 assert_eq!(
1018 model.ring_classifications()[0].2,
1019 6,
1020 "pyrrole: N(2π) + 4C(1π) = 6"
1021 );
1022
1023 let mol = furan_kekule();
1024 let model = assign_aromaticity(&mol);
1025 assert_eq!(
1026 model.ring_classifications()[0].2,
1027 6,
1028 "furan: O(2π) + 4C(1π) = 6"
1029 );
1030 }
1031
1032 #[test]
1038 fn test_benzene_aromatic_smiles() {
1039 let mol = mol_aromatic("c1ccccc1");
1041 let model = assign_aromaticity(&mol);
1042 assert_eq!(
1043 model.aromatic_atom_count(),
1044 6,
1045 "benzene from aromatic SMILES"
1046 );
1047 }
1048
1049 #[test]
1050 fn test_naphthalene_aromatic_smiles() {
1051 let mol = mol_aromatic("c1ccc2ccccc2c1");
1052 let model = assign_aromaticity(&mol);
1053 assert_eq!(
1054 model.aromatic_atom_count(),
1055 10,
1056 "naphthalene from aromatic SMILES"
1057 );
1058 }
1059
1060 #[test]
1061 fn test_pyridine_aromatic_smiles() {
1062 let mol = mol_aromatic("c1ccncc1");
1063 let model = assign_aromaticity(&mol);
1064 assert_eq!(
1065 model.aromatic_atom_count(),
1066 6,
1067 "pyridine from aromatic SMILES"
1068 );
1069 }
1070
1071 #[test]
1072 fn test_furan_aromatic_smiles() {
1073 let mol = mol_aromatic("c1ccoc1");
1074 let model = assign_aromaticity(&mol);
1075 assert_eq!(model.aromatic_atom_count(), 5, "furan from aromatic SMILES");
1076 }
1077
1078 #[test]
1079 fn test_pyrrole_aromatic_smiles() {
1080 let mol = mol_aromatic("c1cc[nH]c1");
1082 let model = assign_aromaticity(&mol);
1083 assert_eq!(
1084 model.aromatic_atom_count(),
1085 5,
1086 "pyrrole from aromatic SMILES"
1087 );
1088 }
1089
1090 #[test]
1091 fn test_thiophene_aromatic_smiles() {
1092 let mol = mol_aromatic("c1ccsc1");
1093 let model = assign_aromaticity(&mol);
1094 assert_eq!(
1095 model.aromatic_atom_count(),
1096 5,
1097 "thiophene from aromatic SMILES"
1098 );
1099 }
1100
1101 #[test]
1106 fn test_indole_aromatic() {
1107 let mol = mol_kekulized("c1ccc2[nH]ccc2c1");
1109 let model = assign_aromaticity(&mol);
1110 assert_eq!(
1111 model.aromatic_atom_count(),
1112 9,
1113 "all 9 indole atoms aromatic"
1114 );
1115 }
1116
1117 #[test]
1118 fn test_benzimidazole_aromatic() {
1119 let mol = mol_kekulized("c1ccc2[nH]cnc2c1");
1121 let model = assign_aromaticity(&mol);
1122 assert_eq!(model.aromatic_atom_count(), 9, "all 9 benzimidazole atoms");
1123 }
1124
1125 #[test]
1126 fn test_quinoline_aromatic() {
1127 let mol = mol_kekulized("c1ccc2ncccc2c1");
1128 let model = assign_aromaticity(&mol);
1129 assert_eq!(model.aromatic_atom_count(), 10, "all 10 quinoline atoms");
1130 }
1131
1132 #[test]
1133 fn test_acridine_aromatic() {
1134 let mol = mol_kekulized("c1ccc2nc3ccccc3cc2c1");
1136 let model = assign_aromaticity(&mol);
1137 assert_eq!(model.aromatic_atom_count(), 14, "all 14 acridine atoms");
1139 }
1140
1141 #[test]
1146 fn test_indolizine_aromatic() {
1147 let mol = mol_aromatic("c1ccn2cccc2c1");
1155 let model = assign_aromaticity(&mol);
1156 assert_eq!(
1157 model.aromatic_atom_count(),
1158 9,
1159 "all 9 indolizine atoms aromatic"
1160 );
1161 let has_aromatic_ring = model
1163 .ring_classifications()
1164 .iter()
1165 .any(|(_, cls, _)| *cls == RingAromaticity::Aromatic);
1166 assert!(has_aromatic_ring, "at least one SSSR ring aromatic");
1167 }
1168
1169 #[test]
1170 fn test_purine_aromatic() {
1171 let mol = mol_kekulized("c1cnc2[nH]cnc2n1");
1173 let model = assign_aromaticity(&mol);
1174 assert_eq!(
1175 model.aromatic_atom_count(),
1176 9,
1177 "all 9 purine atoms aromatic"
1178 );
1179 }
1180
1181 #[test]
1182 fn test_purine_aromatic_from_aromatic_smiles() {
1183 let mol = mol_aromatic("c1cnc2[nH]cnc2n1");
1184 let model = assign_aromaticity(&mol);
1185 assert_eq!(
1186 model.aromatic_atom_count(),
1187 9,
1188 "purine from aromatic SMILES"
1189 );
1190 }
1191
1192 #[test]
1193 fn test_2_pyridinone_aromatic() {
1194 let mol = mol_aromatic("O=c1ccncc1");
1200 let model = assign_aromaticity(&mol);
1201 assert_eq!(
1202 model.aromatic_atom_count(),
1203 6,
1204 "all 6 ring atoms of 2-pyridinone aromatic"
1205 );
1206 }
1207
1208 #[test]
1209 fn test_quinolone_aromatic() {
1210 let mol = mol_aromatic("O=c1ccc2ncccc2c1");
1212 let model = assign_aromaticity(&mol);
1213 assert_eq!(
1214 model.aromatic_atom_count(),
1215 10,
1216 "all 10 quinolone ring atoms aromatic"
1217 );
1218 assert_eq!(
1219 model.ring_classifications().len(),
1220 2,
1221 "two rings classified"
1222 );
1223 }
1224
1225 #[test]
1226 fn test_indole_aromatic_smiles() {
1227 let mol = mol_aromatic("c1ccc2[nH]ccc2c1");
1228 let model = assign_aromaticity(&mol);
1229 assert_eq!(
1230 model.aromatic_atom_count(),
1231 9,
1232 "indole from aromatic SMILES"
1233 );
1234 }
1235
1236 #[test]
1241 fn test_bridgehead_n_contributes_lone_pair() {
1242 let mol = mol_aromatic("c1ccn2cccc2c1");
1246 let model = assign_aromaticity(&mol);
1247 assert_eq!(model.aromatic_atom_count(), 9);
1249 assert!(
1252 model.is_atom_aromatic(AtomIdx(3)),
1253 "bridgehead N must be aromatic"
1254 );
1255 }
1256
1257 #[test]
1258 fn test_non_bridgehead_n_no_false_positive() {
1259 let mol = mol_aromatic("c1ccncn1");
1263 let model = assign_aromaticity(&mol);
1264 assert_eq!(model.aromatic_atom_count(), 6, "pyrimidine is aromatic");
1265 }
1266
1267 #[test]
1268 fn test_imidazole_aromatic() {
1269 let mol = mol_aromatic("c1cn[nH]c1");
1271 let model = assign_aromaticity(&mol);
1272 assert_eq!(model.aromatic_atom_count(), 5, "imidazole is aromatic");
1273 }
1274
1275 #[test]
1280 fn test_pass2_needed_for_indolizine_6ring() {
1281 let mol = mol_aromatic("c1ccn2cccc2c1");
1286 let model = assign_aromaticity(&mol);
1287 assert_eq!(
1288 model.aromatic_atom_count(),
1289 9,
1290 "all 9 indolizine atoms aromatic"
1291 );
1292 assert!(
1294 model.is_atom_aromatic(AtomIdx(3)),
1295 "bridgehead N is aromatic"
1296 );
1297 let aromatic_count = model
1299 .ring_classifications()
1300 .iter()
1301 .filter(|(_, cls, _)| *cls == RingAromaticity::Aromatic)
1302 .count();
1303 assert!(aromatic_count >= 1, "at least one SSSR ring is aromatic");
1304 }
1305
1306 #[test]
1307 fn test_no_pass2_needed_for_naphthalene() {
1308 let mol = naphthalene_kekule();
1311 let model = assign_aromaticity(&mol);
1312 assert_eq!(model.aromatic_atom_count(), 10);
1313 let classes = model.ring_classifications();
1314 assert_eq!(classes.len(), 2);
1315 for (_, cls, _) in classes {
1316 assert_eq!(*cls, RingAromaticity::Aromatic);
1317 }
1318 }
1319
1320 #[test]
1321 fn test_anthracene_aromatic() {
1322 let mol = mol_kekulized("c1ccc2cc3ccccc3cc2c1");
1324 let model = assign_aromaticity(&mol);
1325 assert_eq!(model.aromatic_atom_count(), 14, "all 14 anthracene atoms");
1326 }
1327
1328 #[test]
1333 fn test_kekulized_path_unaffected_by_aromatic_bond_changes() {
1334 let mol = benzene_kekule();
1337 for (_, bond) in mol.bonds() {
1339 assert_ne!(bond.order, BondOrder::Aromatic, "input must be kekulized");
1340 }
1341 let model = assign_aromaticity(&mol);
1342 assert_eq!(model.aromatic_atom_count(), 6);
1343 let aromatic_bonds = mol
1345 .bonds()
1346 .filter(|(b, _)| model.is_bond_aromatic(*b))
1347 .count();
1348 assert_eq!(aromatic_bonds, 6);
1349 }
1350
1351 #[test]
1352 fn test_keto_pyridinone_not_huckel_aromatic() {
1353 let mol = mol_kekulized("O=C1NC=CC=C1");
1360 let model = assign_aromaticity(&mol);
1361 assert_eq!(
1362 model.aromatic_atom_count(),
1363 0,
1364 "keto pyridinone is not Hückel aromatic (7π ≠ 4n+2)"
1365 );
1366 }
1367
1368 #[test]
1369 fn test_cyclopentadienyl_not_aromatic_kekulized() {
1370 let mut b = MoleculeBuilder::new();
1372 let c0 = b.add_atom(Atom::new(Element::C)); let c1 = b.add_atom(Atom::new(Element::C));
1374 let c2 = b.add_atom(Atom::new(Element::C));
1375 let c3 = b.add_atom(Atom::new(Element::C));
1376 let c4 = b.add_atom(Atom::new(Element::C));
1377 b.add_bond(c0, c1, BondOrder::Single).unwrap();
1378 b.add_bond(c1, c2, BondOrder::Double).unwrap();
1379 b.add_bond(c2, c3, BondOrder::Single).unwrap();
1380 b.add_bond(c3, c4, BondOrder::Double).unwrap();
1381 b.add_bond(c4, c0, BondOrder::Single).unwrap();
1382 let mol = b.build();
1383 let model = assign_aromaticity(&mol);
1384 assert_eq!(
1385 model.aromatic_atom_count(),
1386 0,
1387 "cyclopentadiene not aromatic"
1388 );
1389 }
1390}