use crate::bond::BondOrder;
use crate::descriptors::rings::sssr;
use crate::graph::AdjacencyList;
use crate::molecule::Molecule;
fn pi_electrons(
element: &str,
charge: i8,
bond_order_sum: f64,
in_double_bond: bool,
has_exocyclic_double: bool,
) -> Option<u8> {
let elem = element.trim();
let upper: String = elem
.chars()
.next()
.map(|c| c.to_uppercase().collect::<String>())
.unwrap_or_default()
+ &elem.chars().skip(1).collect::<String>().to_lowercase();
match upper.as_str() {
"C" => {
if in_double_bond {
Some(1) } else if charge == -1 {
Some(2) } else if has_exocyclic_double {
Some(0) } else {
None }
}
"N" => {
if in_double_bond {
Some(1) } else if charge == 0 && bond_order_sum <= 3.0 {
Some(2) } else if charge == 1 {
Some(1) } else {
Some(0)
}
}
"O" => {
if in_double_bond {
Some(1)
} else {
Some(2) }
}
"S" => {
if in_double_bond {
Some(1)
} else {
Some(2) }
}
"Se" => {
if in_double_bond {
Some(1)
} else {
Some(2)
}
}
"P" => {
if in_double_bond {
Some(1)
} else {
Some(2)
}
}
_ => None,
}
}
fn is_huckel_aromatic(ring_atoms: &[usize], mol: &Molecule, adj: &AdjacencyList) -> bool {
let ring_set: std::collections::HashSet<usize> = ring_atoms.iter().copied().collect();
let mut has_pi_bond = false;
for &atom_idx in ring_atoms {
for &(neighbor, bond_idx) in adj.neighbors(atom_idx) {
if ring_set.contains(&neighbor) {
if let Some(bond) = mol.bonds.get(bond_idx) {
if matches!(
bond.order,
BondOrder::Double
| BondOrder::Triple
| BondOrder::Aromatic
| BondOrder::DoubleOrAromatic
) {
has_pi_bond = true;
break;
}
}
}
}
if has_pi_bond {
break;
}
}
if !has_pi_bond {
return false;
}
let mut total_pi = 0u16;
for &atom_idx in ring_atoms {
let atom = match mol.atoms.get(atom_idx) {
Some(a) => a,
None => return false,
};
let mut in_ring_double = false;
let mut has_exocyclic_double = false;
let bo_sum: f64 = adj
.neighbors(atom_idx)
.iter()
.filter_map(|&(neighbor, bond_idx)| {
let bond = mol.bonds.get(bond_idx)?;
if matches!(
bond.order,
BondOrder::Double | BondOrder::Aromatic | BondOrder::DoubleOrAromatic
) {
if ring_set.contains(&neighbor) {
in_ring_double = true;
} else {
has_exocyclic_double = true;
}
}
Some(bond.order.order())
})
.sum();
match pi_electrons(
&atom.element,
atom.formal_charge,
bo_sum,
in_ring_double,
has_exocyclic_double,
) {
Some(pe) => total_pi += pe as u16,
None => return false, }
}
if total_pi < 2 {
return false;
}
(total_pi - 2) % 4 == 0
}
pub fn is_aromatic_atom(mol: &Molecule, idx: usize) -> bool {
if idx >= mol.atoms.len() {
return false;
}
for bond in &mol.bonds {
if bond.contains_atom(idx) && bond.is_aromatic() {
return true;
}
}
let rings = sssr(mol);
let adj = AdjacencyList::from_molecule(mol);
for ring in &rings {
if ring.contains_atom(idx) && is_huckel_aromatic(&ring.atoms, mol, &adj) {
return true;
}
}
false
}
pub fn is_aromatic_bond(mol: &Molecule, bond_idx: usize) -> bool {
let bond = match mol.bonds.get(bond_idx) {
Some(b) => b,
None => return false,
};
if bond.is_aromatic() {
return true;
}
let rings = sssr(mol);
let adj = AdjacencyList::from_molecule(mol);
for ring in &rings {
if ring.contains_atom(bond.atom1)
&& ring.contains_atom(bond.atom2)
&& is_huckel_aromatic(&ring.atoms, mol, &adj)
{
return true;
}
}
false
}
pub fn all_aromatic_atoms(mol: &Molecule) -> Vec<bool> {
let n = mol.atom_count();
let mut result = vec![false; n];
for bond in &mol.bonds {
if bond.is_aromatic() {
if bond.atom1 < n {
result[bond.atom1] = true;
}
if bond.atom2 < n {
result[bond.atom2] = true;
}
}
}
if result.iter().all(|&x| x) {
return result;
}
let rings = sssr(mol);
let adj = AdjacencyList::from_molecule(mol);
for ring in &rings {
if is_huckel_aromatic(&ring.atoms, mol, &adj) {
for &atom_idx in &ring.atoms {
if atom_idx < n {
result[atom_idx] = true;
}
}
}
}
result
}
pub fn all_aromatic_bonds(mol: &Molecule) -> Vec<bool> {
let m = mol.bond_count();
let mut result = vec![false; m];
for (i, bond) in mol.bonds.iter().enumerate() {
if bond.is_aromatic() {
result[i] = true;
}
}
let rings = sssr(mol);
let adj = AdjacencyList::from_molecule(mol);
for ring in &rings {
if is_huckel_aromatic(&ring.atoms, mol, &adj) {
for &bond_idx in &ring.bonds {
if bond_idx < m {
result[bond_idx] = true;
}
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atom::Atom;
use crate::bond::{Bond, BondOrder};
fn make_benzene_aromatic() -> Molecule {
let mut mol = Molecule::new("benzene");
for i in 0..6 {
mol.atoms.push(Atom::new(i, "C", 0.0, 0.0, 0.0));
}
for i in 0..6 {
mol.bonds
.push(Bond::new(i, (i + 1) % 6, BondOrder::Aromatic));
}
mol
}
fn make_benzene_kekulized() -> Molecule {
let mut mol = Molecule::new("benzene_kekule");
for i in 0..6 {
mol.atoms.push(Atom::new(i, "C", 0.0, 0.0, 0.0));
}
for i in 0..6 {
let order = if i % 2 == 0 {
BondOrder::Double
} else {
BondOrder::Single
};
mol.bonds.push(Bond::new(i, (i + 1) % 6, order));
}
mol
}
#[test]
fn test_benzene_aromatic_bonds() {
let mol = make_benzene_aromatic();
for i in 0..6 {
assert!(is_aromatic_atom(&mol, i));
}
}
#[test]
fn test_benzene_kekulized_perception() {
let mol = make_benzene_kekulized();
for i in 0..6 {
assert!(
is_aromatic_atom(&mol, i),
"Atom {} should be aromatic in Kekulized benzene",
i
);
}
}
#[test]
fn test_cyclopentane_not_aromatic() {
let mut mol = Molecule::new("cyclopentane");
for i in 0..5 {
mol.atoms.push(Atom::new(i, "C", 0.0, 0.0, 0.0));
}
for i in 0..5 {
mol.bonds.push(Bond::new(i, (i + 1) % 5, BondOrder::Single));
}
for i in 0..5 {
assert!(!is_aromatic_atom(&mol, i));
}
}
#[test]
fn test_pyrrole() {
let mut mol = Molecule::new("pyrrole");
mol.atoms.push(Atom::new(0, "N", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 1.0, 0.0, 0.0));
mol.atoms.push(Atom::new(2, "C", 1.5, 1.0, 0.0));
mol.atoms.push(Atom::new(3, "C", 0.5, 1.5, 0.0));
mol.atoms.push(Atom::new(4, "C", -0.5, 1.0, 0.0));
mol.bonds.push(Bond::new(0, 1, BondOrder::Single));
mol.bonds.push(Bond::new(1, 2, BondOrder::Double));
mol.bonds.push(Bond::new(2, 3, BondOrder::Single));
mol.bonds.push(Bond::new(3, 4, BondOrder::Double));
mol.bonds.push(Bond::new(4, 0, BondOrder::Single));
assert!(is_aromatic_atom(&mol, 0), "N should be aromatic in pyrrole");
}
#[test]
fn test_furan() {
let mut mol = Molecule::new("furan");
mol.atoms.push(Atom::new(0, "O", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 1.0, 0.0, 0.0));
mol.atoms.push(Atom::new(2, "C", 1.5, 1.0, 0.0));
mol.atoms.push(Atom::new(3, "C", 0.5, 1.5, 0.0));
mol.atoms.push(Atom::new(4, "C", -0.5, 1.0, 0.0));
mol.bonds.push(Bond::new(0, 1, BondOrder::Single));
mol.bonds.push(Bond::new(1, 2, BondOrder::Double));
mol.bonds.push(Bond::new(2, 3, BondOrder::Single));
mol.bonds.push(Bond::new(3, 4, BondOrder::Double));
mol.bonds.push(Bond::new(4, 0, BondOrder::Single));
assert!(is_aromatic_atom(&mol, 0), "O should be aromatic in furan");
}
#[test]
fn test_ethane_not_aromatic() {
let mut mol = Molecule::new("ethane");
mol.atoms.push(Atom::new(0, "C", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 1.5, 0.0, 0.0));
mol.bonds.push(Bond::new(0, 1, BondOrder::Single));
assert!(!is_aromatic_atom(&mol, 0));
}
#[test]
fn test_all_aromatic_atoms_benzene() {
let mol = make_benzene_aromatic();
let arom = all_aromatic_atoms(&mol);
assert!(arom.iter().all(|&x| x));
}
#[test]
fn test_all_aromatic_bonds_benzene() {
let mol = make_benzene_aromatic();
let arom = all_aromatic_bonds(&mol);
assert!(arom.iter().all(|&x| x));
}
#[test]
fn test_is_aromatic_bond() {
let mol = make_benzene_aromatic();
for i in 0..6 {
assert!(is_aromatic_bond(&mol, i));
}
}
#[test]
fn test_empty_molecule() {
let mol = Molecule::new("empty");
assert!(!is_aromatic_atom(&mol, 0));
let arom = all_aromatic_atoms(&mol);
assert!(arom.is_empty());
}
#[test]
fn test_saturated_ring_not_aromatic() {
let mut mol = Molecule::new("thp");
mol.atoms.push(Atom::new(0, "O", 0.0, 0.0, 0.0));
mol.atoms.push(Atom::new(1, "C", 1.0, 0.0, 0.0));
mol.atoms.push(Atom::new(2, "C", 1.5, 1.0, 0.0));
mol.atoms.push(Atom::new(3, "C", 0.5, 1.5, 0.0));
mol.atoms.push(Atom::new(4, "C", -0.5, 1.0, 0.0));
mol.atoms.push(Atom::new(5, "C", -1.0, 0.0, 0.0));
mol.bonds.push(Bond::new(0, 1, BondOrder::Single));
mol.bonds.push(Bond::new(1, 2, BondOrder::Single));
mol.bonds.push(Bond::new(2, 3, BondOrder::Single));
mol.bonds.push(Bond::new(3, 4, BondOrder::Single));
mol.bonds.push(Bond::new(4, 5, BondOrder::Single));
mol.bonds.push(Bond::new(5, 0, BondOrder::Single));
for i in 0..6 {
assert!(
!is_aromatic_atom(&mol, i),
"Atom {} in saturated ring should not be aromatic",
i
);
}
}
}