use std::collections::HashSet;
use chematic_core::{AtomIdx, BondIdx, BondOrder, Molecule};
use crate::sssr::find_sssr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RingAromaticity {
Aromatic,
Antiaromatic,
NonAromatic,
}
#[derive(Debug, Clone)]
pub struct AromaticityModel {
aromatic_atoms: HashSet<AtomIdx>,
aromatic_bonds: HashSet<BondIdx>,
antiaromatic_rings: Vec<Vec<AtomIdx>>, ring_classifications: Vec<(Vec<AtomIdx>, RingAromaticity, u32)>, }
impl AromaticityModel {
pub fn is_atom_aromatic(&self, idx: AtomIdx) -> bool {
self.aromatic_atoms.contains(&idx)
}
pub fn is_bond_aromatic(&self, idx: BondIdx) -> bool {
self.aromatic_bonds.contains(&idx)
}
pub fn aromatic_atom_count(&self) -> usize {
self.aromatic_atoms.len()
}
pub fn ring_classifications(&self) -> &[(Vec<AtomIdx>, RingAromaticity, u32)] {
&self.ring_classifications
}
pub fn antiaromatic_rings(&self) -> &[Vec<AtomIdx>] {
&self.antiaromatic_rings
}
pub fn has_antiaromaticity(&self) -> bool {
!self.antiaromatic_rings.is_empty()
}
}
#[allow(clippy::manual_is_multiple_of)]
fn classify_ring_aromaticity(pi_electrons: u32) -> (RingAromaticity, u32) {
if pi_electrons >= 2 && (pi_electrons - 2) % 4 == 0 {
(RingAromaticity::Aromatic, pi_electrons)
}
else if pi_electrons > 0 && pi_electrons % 4 == 0 {
(RingAromaticity::Antiaromatic, pi_electrons)
}
else {
(RingAromaticity::NonAromatic, pi_electrons)
}
}
pub fn assign_aromaticity(mol: &Molecule) -> AromaticityModel {
let ring_set = find_sssr(mol);
let mut aromatic_atoms: HashSet<AtomIdx> = HashSet::new();
let mut aromatic_bonds: HashSet<BondIdx> = HashSet::new();
let mut antiaromatic_rings: Vec<Vec<AtomIdx>> = Vec::new();
let mut ring_classifications: Vec<(Vec<AtomIdx>, RingAromaticity, u32)> = Vec::new();
for ring in ring_set.rings() {
if let Some(pi_electrons) = ring_pi_electrons(mol, ring) {
let (classification, count) = classify_ring_aromaticity(pi_electrons);
ring_classifications.push((ring.to_vec(), classification, count));
match classification {
RingAromaticity::Aromatic => {
for &atom in ring {
aromatic_atoms.insert(atom);
}
for i in 0..ring.len() {
let a = ring[i];
let b = ring[(i + 1) % ring.len()];
if let Some((bidx, _)) = mol.bond_between(a, b) {
aromatic_bonds.insert(bidx);
}
}
}
RingAromaticity::Antiaromatic => {
antiaromatic_rings.push(ring.to_vec());
}
RingAromaticity::NonAromatic => {
}
}
}
}
AromaticityModel {
aromatic_atoms,
aromatic_bonds,
antiaromatic_rings,
ring_classifications,
}
}
pub fn apply_aromaticity(mol: &Molecule) -> Molecule {
use chematic_core::{BondOrder, MoleculeBuilder};
let model = assign_aromaticity(mol);
let mut builder = MoleculeBuilder::new();
for (idx, atom) in mol.atoms() {
let mut a = atom.clone();
if model.is_atom_aromatic(idx) {
a.aromatic = true;
}
builder.add_atom(a);
}
for (bidx, bond) in mol.bonds() {
let order = if model.is_bond_aromatic(bidx) {
BondOrder::Aromatic
} else {
bond.order
};
let _ = builder.add_bond(bond.atom1, bond.atom2, order);
}
builder.build()
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
struct AtomElectronContribution {
pi_electrons: u32,
is_lone_pair: bool, is_double_bond: bool, is_sp2_compatible: bool, }
fn ring_pi_electrons(mol: &Molecule, ring: &[AtomIdx]) -> Option<u32> {
let ring_atom_set: HashSet<AtomIdx> = ring.iter().copied().collect();
let mut total_pi: u32 = 0;
for &atom_idx in ring {
let atom = mol.atom(atom_idx);
let an = atom.element.atomic_number();
let ring_degree = mol
.neighbors(atom_idx)
.filter(|(nb, _)| ring_atom_set.contains(nb))
.count();
let has_double_in_ring = mol
.neighbors(atom_idx)
.filter(|(nb, _)| ring_atom_set.contains(nb))
.any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Double);
let has_double_any = mol
.neighbors(atom_idx)
.any(|(_, bidx)| mol.bond(bidx).order == BondOrder::Double);
let pi = match an {
6 => {
if !has_double_any {
return None;
}
1 }
7 => {
if hydrogen_count(mol, atom_idx) > 0 {
2
} else if has_double_in_ring || has_double_any {
1
} else {
return None;
}
}
8 | 16 => {
if ring_degree != 2 {
return None;
}
2
}
_ => return None,
};
total_pi += pi;
}
Some(total_pi)
}
fn hydrogen_count(mol: &Molecule, atom_idx: AtomIdx) -> u8 {
let atom = mol.atom(atom_idx);
if let Some(h) = atom.hydrogen_count {
return h;
}
let normal_valences = atom.element.normal_valences();
if normal_valences.is_empty() {
return 0;
}
let bond_sum: i32 = mol
.neighbors(atom_idx)
.map(|(_, bidx)| mol.bond(bidx).order.order_int() as i32)
.sum();
let charge = atom.charge as i32;
for &v in normal_valences {
let target = v as i32 + charge;
if target >= bond_sum {
return (target - bond_sum) as u8;
}
}
0
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_core::{Atom, BondOrder, Element, MoleculeBuilder};
fn benzene_kekule() -> chematic_core::Molecule {
let mut b = MoleculeBuilder::new();
let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
for i in 0..6 {
let order = if i % 2 == 0 {
BondOrder::Double
} else {
BondOrder::Single
};
b.add_bond(atoms[i], atoms[(i + 1) % 6], order).unwrap();
}
b.build()
}
fn cyclohexane() -> chematic_core::Molecule {
let mut b = MoleculeBuilder::new();
let atoms: Vec<_> = (0..6).map(|_| b.add_atom(Atom::new(Element::C))).collect();
for i in 0..6 {
b.add_bond(atoms[i], atoms[(i + 1) % 6], BondOrder::Single)
.unwrap();
}
b.build()
}
fn pyridine_kekule() -> chematic_core::Molecule {
let mut b = MoleculeBuilder::new();
let n = b.add_atom(Atom::new(Element::N));
let atoms_c: Vec<_> = (0..5).map(|_| b.add_atom(Atom::new(Element::C))).collect();
let ring = [
n, atoms_c[0], atoms_c[1], atoms_c[2], atoms_c[3], atoms_c[4],
];
for i in 0..6 {
let order = if i % 2 == 0 {
BondOrder::Double
} else {
BondOrder::Single
};
b.add_bond(ring[i], ring[(i + 1) % 6], order).unwrap();
}
b.build()
}
fn furan_kekule() -> chematic_core::Molecule {
let mut b = MoleculeBuilder::new();
let o = b.add_atom(Atom::new(Element::O));
let c1 = b.add_atom(Atom::new(Element::C));
let c2 = b.add_atom(Atom::new(Element::C));
let c3 = b.add_atom(Atom::new(Element::C));
let c4 = b.add_atom(Atom::new(Element::C));
let ring = [o, c1, c2, c3, c4];
b.add_bond(ring[0], ring[1], BondOrder::Single).unwrap();
b.add_bond(ring[1], ring[2], BondOrder::Double).unwrap();
b.add_bond(ring[2], ring[3], BondOrder::Single).unwrap();
b.add_bond(ring[3], ring[4], BondOrder::Double).unwrap();
b.add_bond(ring[4], ring[0], BondOrder::Single).unwrap();
b.build()
}
fn pyrrole_kekule() -> chematic_core::Molecule {
let mut b = MoleculeBuilder::new();
let mut n_atom = Atom::new(Element::N);
n_atom.hydrogen_count = Some(1);
let n = b.add_atom(n_atom);
let c1 = b.add_atom(Atom::new(Element::C));
let c2 = b.add_atom(Atom::new(Element::C));
let c3 = b.add_atom(Atom::new(Element::C));
let c4 = b.add_atom(Atom::new(Element::C));
let ring = [n, c1, c2, c3, c4];
b.add_bond(ring[0], ring[1], BondOrder::Single).unwrap();
b.add_bond(ring[1], ring[2], BondOrder::Double).unwrap();
b.add_bond(ring[2], ring[3], BondOrder::Single).unwrap();
b.add_bond(ring[3], ring[4], BondOrder::Double).unwrap();
b.add_bond(ring[4], ring[0], BondOrder::Single).unwrap();
b.build()
}
fn naphthalene_kekule() -> chematic_core::Molecule {
let mut b = MoleculeBuilder::new();
let atoms: Vec<_> = (0..10).map(|_| b.add_atom(Atom::new(Element::C))).collect();
let ring1 = [0usize, 1, 2, 3, 4, 9];
let orders1 = [
BondOrder::Double,
BondOrder::Single,
BondOrder::Double,
BondOrder::Single,
BondOrder::Double,
BondOrder::Single,
];
for i in 0..6 {
b.add_bond(atoms[ring1[i]], atoms[ring1[(i + 1) % 6]], orders1[i])
.unwrap();
}
let ring2_extra = [(4, 5), (5, 6), (6, 7), (7, 8), (8, 9)];
let orders2 = [
BondOrder::Single,
BondOrder::Double,
BondOrder::Single,
BondOrder::Double,
BondOrder::Single,
];
for (i, &(a, bb)) in ring2_extra.iter().enumerate() {
b.add_bond(atoms[a], atoms[bb], orders2[i]).unwrap();
}
b.build()
}
#[test]
fn test_benzene_is_aromatic() {
let mol = benzene_kekule();
let model = assign_aromaticity(&mol);
assert_eq!(
model.aromatic_atom_count(),
6,
"all 6 benzene atoms are aromatic"
);
for i in 0..6u32 {
assert!(
model.is_atom_aromatic(AtomIdx(i)),
"atom {} should be aromatic",
i
);
}
}
#[test]
fn test_cyclohexane_not_aromatic() {
let mol = cyclohexane();
let model = assign_aromaticity(&mol);
assert_eq!(
model.aromatic_atom_count(),
0,
"cyclohexane has no aromatic atoms"
);
}
#[test]
fn test_pyridine_is_aromatic() {
let mol = pyridine_kekule();
let model = assign_aromaticity(&mol);
assert_eq!(
model.aromatic_atom_count(),
6,
"all 6 pyridine atoms are aromatic"
);
}
#[test]
fn test_furan_is_aromatic() {
let mol = furan_kekule();
let model = assign_aromaticity(&mol);
assert_eq!(
model.aromatic_atom_count(),
5,
"all 5 furan atoms are aromatic"
);
}
#[test]
fn test_pyrrole_is_aromatic() {
let mol = pyrrole_kekule();
let model = assign_aromaticity(&mol);
assert_eq!(
model.aromatic_atom_count(),
5,
"all 5 pyrrole atoms are aromatic"
);
}
#[test]
fn test_naphthalene_both_rings_aromatic() {
let mol = naphthalene_kekule();
let model = assign_aromaticity(&mol);
assert_eq!(
model.aromatic_atom_count(),
10,
"all 10 naphthalene atoms are aromatic"
);
}
#[test]
fn test_bond_aromaticity_benzene() {
let mol = benzene_kekule();
let model = assign_aromaticity(&mol);
let mut count = 0;
for (bidx, _) in mol.bonds() {
if model.is_bond_aromatic(bidx) {
count += 1;
}
}
assert_eq!(count, 6, "benzene has 6 aromatic bonds");
}
#[test]
fn test_apply_aromaticity_benzene() {
use chematic_core::BondOrder;
let mol = benzene_kekule();
let aromatic = apply_aromaticity(&mol);
for (_, atom) in aromatic.atoms() {
assert!(atom.aromatic, "every benzene carbon should be aromatic");
}
let mut aromatic_bond_count = 0;
for (_, bond) in aromatic.bonds() {
if bond.order == BondOrder::Aromatic {
aromatic_bond_count += 1;
}
}
assert_eq!(aromatic_bond_count, 6);
}
#[test]
fn test_apply_aromaticity_cyclohexane_unchanged() {
use chematic_core::BondOrder;
let mol = cyclohexane();
let result = apply_aromaticity(&mol);
for (_, atom) in result.atoms() {
assert!(!atom.aromatic);
}
for (_, bond) in result.bonds() {
assert_ne!(bond.order, BondOrder::Aromatic);
}
}
fn cyclobutadiene_kekule() -> chematic_core::Molecule {
let mut b = MoleculeBuilder::new();
let atoms: Vec<_> = (0..4).map(|_| b.add_atom(Atom::new(Element::C))).collect();
for i in 0..4 {
let order = if i % 2 == 0 {
BondOrder::Double
} else {
BondOrder::Single
};
b.add_bond(atoms[i], atoms[(i + 1) % 4], order).unwrap();
}
b.build()
}
#[test]
fn test_cyclobutadiene_antiaromatic() {
let mol = cyclobutadiene_kekule();
let model = assign_aromaticity(&mol);
assert_eq!(
model.aromatic_atom_count(),
0,
"cyclobutadiene should not be aromatic"
);
assert!(
model.has_antiaromaticity(),
"cyclobutadiene should be antiaromatic"
);
assert_eq!(model.antiaromatic_rings().len(), 1, "one antiaromatic ring");
let classifications = model.ring_classifications();
assert_eq!(classifications.len(), 1);
assert_eq!(classifications[0].1, RingAromaticity::Antiaromatic);
assert_eq!(classifications[0].2, 4, "cyclobutadiene has 4 π electrons");
}
fn cyclooctatetraene_kekule() -> chematic_core::Molecule {
let mut b = MoleculeBuilder::new();
let atoms: Vec<_> = (0..8).map(|_| b.add_atom(Atom::new(Element::C))).collect();
for i in 0..8 {
let order = if i % 2 == 0 {
BondOrder::Double
} else {
BondOrder::Single
};
b.add_bond(atoms[i], atoms[(i + 1) % 8], order).unwrap();
}
b.build()
}
#[test]
fn test_cyclooctatetraene_antiaromatic() {
let mol = cyclooctatetraene_kekule();
let model = assign_aromaticity(&mol);
assert_eq!(
model.aromatic_atom_count(),
0,
"cyclooctatetraene should not be aromatic"
);
assert!(
model.has_antiaromaticity(),
"cyclooctatetraene should be antiaromatic"
);
assert_eq!(model.antiaromatic_rings().len(), 1, "one antiaromatic ring");
let classifications = model.ring_classifications();
assert_eq!(classifications[0].1, RingAromaticity::Antiaromatic);
assert_eq!(
classifications[0].2, 8,
"cyclooctatetraene has 8 π electrons"
);
}
#[test]
fn test_ring_classifications_benzene() {
let mol = benzene_kekule();
let model = assign_aromaticity(&mol);
let classifications = model.ring_classifications();
assert_eq!(classifications.len(), 1, "benzene has one ring");
assert_eq!(classifications[0].1, RingAromaticity::Aromatic);
assert_eq!(classifications[0].2, 6, "benzene has 6 π electrons");
}
#[test]
fn test_ring_classifications_mixed() {
let mol = naphthalene_kekule();
let model = assign_aromaticity(&mol);
let classifications = model.ring_classifications();
assert_eq!(classifications.len(), 2, "naphthalene has two rings");
for (_, classification, count) in classifications {
assert_eq!(*classification, RingAromaticity::Aromatic);
assert_eq!(*count, 6);
}
}
#[test]
fn test_non_aromatic_cyclohexane() {
let mol = cyclohexane();
let model = assign_aromaticity(&mol);
let classifications = model.ring_classifications();
for (_, classification, _count) in classifications {
assert_ne!(
*classification,
RingAromaticity::Aromatic,
"cyclohexane should not be aromatic"
);
assert_ne!(
*classification,
RingAromaticity::Antiaromatic,
"cyclohexane should not be antiaromatic"
);
}
}
#[test]
fn test_thiophene_aromatic() {
let mut b = MoleculeBuilder::new();
let s = b.add_atom(Atom::new(Element::S));
let c1 = b.add_atom(Atom::new(Element::C));
let c2 = b.add_atom(Atom::new(Element::C));
let c3 = b.add_atom(Atom::new(Element::C));
let c4 = b.add_atom(Atom::new(Element::C));
let ring = [s, c1, c2, c3, c4];
b.add_bond(ring[0], ring[1], BondOrder::Single).unwrap();
b.add_bond(ring[1], ring[2], BondOrder::Double).unwrap();
b.add_bond(ring[2], ring[3], BondOrder::Single).unwrap();
b.add_bond(ring[3], ring[4], BondOrder::Double).unwrap();
b.add_bond(ring[4], ring[0], BondOrder::Single).unwrap();
let mol = b.build();
let model = assign_aromaticity(&mol);
assert_eq!(model.aromatic_atom_count(), 5, "thiophene is aromatic");
let classifications = model.ring_classifications();
assert_eq!(classifications[0].1, RingAromaticity::Aromatic);
assert_eq!(classifications[0].2, 6);
}
#[test]
fn test_electron_distribution_tracking() {
let mol = benzene_kekule();
let model = assign_aromaticity(&mol);
let classifications = model.ring_classifications();
assert_eq!(
classifications[0].2, 6,
"benzene electron distribution: 6 × 1π (from C) = 6 total"
);
let mol = pyrrole_kekule();
let model = assign_aromaticity(&mol);
let classifications = model.ring_classifications();
assert_eq!(
classifications[0].2, 6,
"pyrrole electron distribution: 2π (N lone pair) + 4 × 1π (C) = 6 total"
);
let mol = furan_kekule();
let model = assign_aromaticity(&mol);
let classifications = model.ring_classifications();
assert_eq!(
classifications[0].2, 6,
"furan electron distribution: 2π (O lone pair) + 4 × 1π (C) = 6 total"
);
}
}