use std::collections::{HashMap, HashSet};
use crate::chem::rings::find_rings;
use crate::system::atomistic::{AtomId, Atomistic, BondId};
use crate::system::element::Element;
use crate::system::molgraph::PropValue;
const MAX_FUSED: usize = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ElectronDonor {
NoDonor,
Vacant,
One,
Two,
}
impl ElectronDonor {
fn min_max(self) -> (i32, i32) {
match self {
ElectronDonor::One => (1, 1),
ElectronDonor::Two => (2, 2),
ElectronDonor::NoDonor | ElectronDonor::Vacant => (0, 0),
}
}
fn is_candidate_type(self) -> bool {
matches!(
self,
ElectronDonor::Vacant | ElectronDonor::One | ElectronDonor::Two
)
}
}
fn default_valence(z: u8) -> i32 {
match Element::by_number(z).map(|e| e.default_valences()) {
Some(vals) if !vals.is_empty() => vals[0] as i32,
_ => -1,
}
}
fn n_outer_elecs(z: u8) -> i32 {
match z {
1 => 1, 5 => 3, 6 => 4, 7 => 5, 8 => 6, 9 => 7, 14 => 4, 15 => 5, 16 => 6, 17 => 7, 33 => 5, 34 => 6, 52 => 6, _ => main_group_outer(z),
}
}
fn main_group_outer(z: u8) -> i32 {
match z {
3 | 11 | 19 | 37 | 55 | 87 => 1,
4 | 12 | 20 | 38 | 56 | 88 => 2,
13 | 31 | 49 | 81 => 3,
32 | 50 | 82 => 4,
51 | 83 => 5,
84 => 6,
35 | 53 | 85 => 7,
2 | 10 | 18 | 36 | 54 | 86 => 8,
_ => 0,
}
}
fn electronegativity(z: u8) -> f64 {
match z {
1 => 2.20,
5 => 2.04,
6 => 2.55,
7 => 3.04,
8 => 3.44,
9 => 3.98,
14 => 1.90,
15 => 2.19,
16 => 2.58,
17 => 3.16,
34 => 2.55,
52 => 2.10,
_ => 0.0,
}
}
fn more_electronegative(za: u8, zb: u8) -> bool {
electronegativity(za) > electronegativity(zb)
}
fn bond_order(mol: &Atomistic, bid: BondId) -> f64 {
let Ok(b) = mol.get_bond(bid) else {
return 1.0;
};
b.props
.get("kekule_order")
.or_else(|| b.props.get("order"))
.and_then(|v| v.as_f64())
.unwrap_or(1.0)
}
fn atomic_num(mol: &Atomistic, id: AtomId) -> u8 {
mol.get_atom(id)
.ok()
.and_then(|a| {
a.get_str("element")
.and_then(Element::by_symbol)
.map(|e| e.z())
})
.unwrap_or(0)
}
fn formal_charge(mol: &Atomistic, id: AtomId) -> i32 {
match mol
.get_atom(id)
.ok()
.and_then(|a| a.get("formal_charge").cloned())
{
Some(PropValue::Int(v)) => v,
Some(PropValue::F64(v)) => v as i32,
_ => 0,
}
}
fn total_degree(mol: &Atomistic, id: AtomId) -> i32 {
mol.neighbors(id).count() as i32
}
fn incident_bonds<'a>(
mol: &'a Atomistic,
id: AtomId,
) -> impl Iterator<Item = (BondId, AtomId, f64)> + 'a {
mol.incident_bond_ids(id)
.map(move |(bid, other)| (bid, other, bond_order(mol, bid)))
}
fn explicit_valence(mol: &Atomistic, id: AtomId) -> i32 {
let sum: f64 = incident_bonds(mol, id).map(|(_, _, o)| o).sum();
sum.round() as i32
}
fn incident_noncyclic_multiple_bond(
mol: &Atomistic,
rings: &crate::chem::rings::RingInfo,
id: AtomId,
) -> Option<AtomId> {
for (bid, other, order) in incident_bonds(mol, id) {
if !rings.is_bond_in_ring(bid) && order >= 2.0 {
return Some(other);
}
}
None
}
fn incident_cyclic_multiple_bond(
mol: &Atomistic,
rings: &crate::chem::rings::RingInfo,
id: AtomId,
) -> bool {
incident_bonds(mol, id).any(|(bid, _, order)| rings.is_bond_in_ring(bid) && order >= 2.0)
}
fn incident_multiple_bond(mol: &Atomistic, id: AtomId) -> bool {
explicit_valence(mol, id) != total_degree(mol, id)
}
fn count_atom_elec(mol: &Atomistic, id: AtomId) -> i32 {
let z = atomic_num(mol, id);
let dv = default_valence(z);
if dv <= 1 {
return -1;
}
let degree = total_degree(mol, id);
if degree > 3 {
return -1;
}
let nlp_raw = n_outer_elecs(z) - dv;
let nlp = (nlp_raw - formal_charge(mol, id)).max(0);
let n_radicals = 0;
let mut res = (dv - degree) + nlp - n_radicals;
if res > 1 {
let n_unsaturations = explicit_valence(mol, id) - degree;
if n_unsaturations > 1 {
res = 1;
}
}
res
}
fn atom_donor_type(
mol: &Atomistic,
rings: &crate::chem::rings::RingInfo,
id: AtomId,
) -> ElectronDonor {
let z = atomic_num(mol, id);
if z == 0 {
return if incident_cyclic_multiple_bond(mol, rings, id) {
ElectronDonor::One
} else {
ElectronDonor::One
};
}
let mut nelec = count_atom_elec(mol, id);
if nelec < 0 {
ElectronDonor::NoDonor
} else if nelec == 0 {
if incident_noncyclic_multiple_bond(mol, rings, id).is_some() {
ElectronDonor::Vacant
} else if incident_cyclic_multiple_bond(mol, rings, id) {
ElectronDonor::One
} else {
ElectronDonor::NoDonor
}
} else if nelec == 1 {
if let Some(who) = incident_noncyclic_multiple_bond(mol, rings, id) {
let z2 = atomic_num(mol, who);
if more_electronegative(z2, z) {
ElectronDonor::Vacant
} else {
ElectronDonor::One
}
} else if incident_multiple_bond(mol, id) {
ElectronDonor::One
} else if formal_charge(mol, id) == 1 {
ElectronDonor::Vacant
} else {
ElectronDonor::NoDonor
}
} else {
if let Some(who) = incident_noncyclic_multiple_bond(mol, rings, id) {
let z2 = atomic_num(mol, who);
if more_electronegative(z2, z) {
nelec -= 1;
}
}
if nelec % 2 == 1 {
ElectronDonor::One
} else {
ElectronDonor::Two
}
}
}
fn is_atom_candidate(mol: &Atomistic, id: AtomId, edon: ElectronDonor) -> bool {
let z = atomic_num(mol, id);
if z > 18 && z != 34 && z != 52 {
return false;
}
if !edon.is_candidate_type() {
return false;
}
let dv = default_valence(z);
if dv > 0 {
let total_valence = explicit_valence(mol, id);
let charge = formal_charge(mol, id);
let adj_dv = default_valence((z as i32 - charge).clamp(1, 118) as u8);
if total_valence > adj_dv {
return false;
}
}
let n_unsaturations = explicit_valence(mol, id) - total_degree(mol, id);
if n_unsaturations > 1 {
let mut n_mult = 0;
for (_, _, order) in incident_bonds(mol, id) {
if order >= 2.0 {
n_mult += 1;
}
if n_mult > 1 {
return false;
}
}
}
true
}
fn apply_huckel(ring_atoms: &[AtomId], edon: &HashMap<AtomId, ElectronDonor>) -> bool {
let mut rlw = 0;
let mut rup = 0;
for &a in ring_atoms {
let (lo, hi) = edon
.get(&a)
.copied()
.unwrap_or(ElectronDonor::NoDonor)
.min_max();
rlw += lo;
rup += hi;
}
if rup >= 6 {
let mut rie = rlw;
while rie <= rup {
if (rie - 2) % 4 == 0 {
return true;
}
rie += 1;
}
false
} else {
rup == 2
}
}
fn ring_bonds(mol: &Atomistic, ring: &[AtomId]) -> Vec<BondId> {
let n = ring.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let a = ring[i];
let b = ring[(i + 1) % n];
if let Some(bid) = find_bond(mol, a, b) {
out.push(bid);
}
}
out
}
fn find_bond(mol: &Atomistic, a: AtomId, b: AtomId) -> Option<BondId> {
mol.incident_bond_ids(a)
.find(|&(_, other)| other == b)
.map(|(bid, _)| bid)
}
fn fused_systems(ring_bond_sets: &[HashSet<BondId>]) -> Vec<Vec<usize>> {
let n = ring_bond_sets.len();
fn find(parent: &mut [usize], x: usize) -> usize {
let mut root = x;
while parent[root] != root {
root = parent[root];
}
let mut cur = x;
while parent[cur] != root {
let next = parent[cur];
parent[cur] = root;
cur = next;
}
root
}
let mut parent: Vec<usize> = (0..n).collect();
let mut bond_to_rings: HashMap<BondId, Vec<usize>> = HashMap::new();
for (ri, set) in ring_bond_sets.iter().enumerate() {
for &b in set {
bond_to_rings.entry(b).or_default().push(ri);
}
}
for rings in bond_to_rings.values() {
for pair in rings.windows(2) {
let a = find(&mut parent, pair[0]);
let b = find(&mut parent, pair[1]);
if a != b {
parent[a] = b;
}
}
}
let mut groups: HashMap<usize, Vec<usize>> = HashMap::new();
for i in 0..n {
let root = find(&mut parent, i);
groups.entry(root).or_default().push(i);
}
groups.into_values().collect()
}
fn combinations(n: usize, k: usize) -> Vec<Vec<usize>> {
let mut out = Vec::new();
if k == 0 || k > n {
return out;
}
let mut idx: Vec<usize> = (0..k).collect();
loop {
out.push(idx.clone());
let mut i = k;
loop {
if i == 0 {
return out;
}
i -= 1;
if idx[i] != i + n - k {
break;
}
if i == 0 {
return out;
}
}
idx[i] += 1;
for j in (i + 1)..k {
idx[j] = idx[j - 1] + 1;
}
}
}
fn apply_huckel_to_fused(
rings: &[Vec<AtomId>],
ring_bond_sets: &[HashSet<BondId>],
fused: &[usize],
edon: &HashMap<AtomId, ElectronDonor>,
aromatic_bonds: &mut HashSet<BondId>,
aromatic_atoms: &mut HashSet<AtomId>,
mol: &Atomistic,
) {
let nrings = fused.len();
let max_size = nrings.min(MAX_FUSED);
for cur_size in 1..=max_size {
for comb in combinations(nrings, cur_size) {
let cur_rs: Vec<usize> = comb.iter().map(|&i| fused[i]).collect();
if cur_rs.len() > 1 && !is_connected_subset(&cur_rs, ring_bond_sets) {
continue;
}
let mut counts: HashMap<AtomId, usize> = HashMap::new();
for &ri in &cur_rs {
for &a in &rings[ri] {
*counts.entry(a).or_insert(0) += 1;
}
}
let unon: Vec<AtomId> = counts
.iter()
.filter(|&(_, &c)| c == 1 || c == 2)
.map(|(&a, _)| a)
.collect();
if apply_huckel(&unon, edon) {
let mut bond_count: HashMap<BondId, usize> = HashMap::new();
for &ri in &cur_rs {
for &bid in &ring_bond_sets[ri] {
*bond_count.entry(bid).or_insert(0) += 1;
}
}
for (&bid, &cnt) in &bond_count {
if cnt == 1
&& let Ok(bond) = mol.get_bond(bid)
{
aromatic_bonds.insert(bid);
aromatic_atoms.insert(bond.nodes[0]);
aromatic_atoms.insert(bond.nodes[1]);
}
}
}
}
}
}
fn is_connected_subset(subset: &[usize], ring_bond_sets: &[HashSet<BondId>]) -> bool {
if subset.len() <= 1 {
return true;
}
let mut seen = vec![false; subset.len()];
let mut stack = vec![0usize];
seen[0] = true;
let mut count = 1;
while let Some(cur) = stack.pop() {
for j in 0..subset.len() {
if !seen[j] && !ring_bond_sets[subset[cur]].is_disjoint(&ring_bond_sets[subset[j]]) {
seen[j] = true;
count += 1;
stack.push(j);
}
}
}
count == subset.len()
}
pub fn perceive_aromaticity(mol: &mut Atomistic) -> usize {
let snapshot: Vec<(BondId, f64)> = mol
.bonds()
.filter(|(_, b)| !b.props.contains_key("kekule_order"))
.map(|(bid, b)| {
let o = b.props.get("order").and_then(|v| v.as_f64()).unwrap_or(1.0);
(bid, o)
})
.collect();
for (bid, o) in snapshot {
let _ = mol.set_bond_prop(bid, "kekule_order", PropValue::F64(o));
}
let rings_info = find_rings(mol);
let srings: Vec<Vec<AtomId>> = rings_info.rings().to_vec();
let mut edon: HashMap<AtomId, ElectronDonor> = HashMap::new();
let mut candidate: HashMap<AtomId, bool> = HashMap::new();
for ring in &srings {
for &id in ring {
if candidate.contains_key(&id) {
continue;
}
let d = atom_donor_type(mol, &rings_info, id);
edon.insert(id, d);
candidate.insert(id, is_atom_candidate(mol, id, d));
}
}
let mut cring_idx: Vec<usize> = Vec::new();
for (ri, ring) in srings.iter().enumerate() {
let all_cand = ring
.iter()
.all(|id| candidate.get(id).copied().unwrap_or(false));
let all_dummy = ring.iter().all(|&id| atomic_num(mol, id) == 0);
if all_cand && !all_dummy {
cring_idx.push(ri);
}
}
let crings: Vec<Vec<AtomId>> = cring_idx.iter().map(|&ri| srings[ri].clone()).collect();
let ring_bond_sets: Vec<HashSet<BondId>> = crings
.iter()
.map(|r| ring_bonds(mol, r).into_iter().collect())
.collect();
let mut aromatic_atoms: HashSet<AtomId> = HashSet::new();
let mut aromatic_bonds: HashSet<BondId> = HashSet::new();
for system in fused_systems(&ring_bond_sets) {
apply_huckel_to_fused(
&crings,
&ring_bond_sets,
&system,
&edon,
&mut aromatic_bonds,
&mut aromatic_atoms,
mol,
);
}
let all_atom_ids: Vec<AtomId> = mol.atoms().map(|(id, _)| id).collect();
for id in all_atom_ids {
let arom = aromatic_atoms.contains(&id);
let _ = mol.set_atom(id, "is_aromatic", PropValue::Int(if arom { 1 } else { 0 }));
}
let all_bond_ids: Vec<BondId> = mol.bonds().map(|(id, _)| id).collect();
for bid in all_bond_ids {
if aromatic_bonds.contains(&bid) {
let _ = mol.set_bond_prop(bid, "order", PropValue::F64(1.5));
let _ = mol.set_bond_prop(bid, "is_aromatic", PropValue::Int(1));
} else {
let _ = mol.set_bond_prop(bid, "is_aromatic", PropValue::Int(0));
}
}
aromatic_atoms.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::system::molgraph::Atom;
fn benzene() -> Atomistic {
let mut g = Atomistic::new();
let c: Vec<AtomId> = (0..6)
.map(|_| g.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0)))
.collect();
for i in 0..6 {
let bid = g.add_bond(c[i], c[(i + 1) % 6]).unwrap();
let order = if i % 2 == 0 { 2.0 } else { 1.0 };
g.set_bond_prop(bid, "order", PropValue::F64(order))
.unwrap();
}
for &ci in &c {
let h = g.add_atom(Atom::xyz("H", 0.0, 0.0, 0.0));
g.add_bond(ci, h).unwrap();
}
g
}
#[test]
fn test_benzene_all_aromatic() {
let mut g = benzene();
let n = perceive_aromaticity(&mut g);
assert_eq!(n, 6);
}
#[test]
fn test_cyclohexane_not_aromatic() {
let mut g = Atomistic::new();
let c: Vec<AtomId> = (0..6)
.map(|_| g.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0)))
.collect();
for i in 0..6 {
g.add_bond(c[i], c[(i + 1) % 6]).unwrap();
}
for &ci in &c {
for _ in 0..2 {
let h = g.add_atom(Atom::xyz("H", 0.0, 0.0, 0.0));
g.add_bond(ci, h).unwrap();
}
}
assert_eq!(perceive_aromaticity(&mut g), 0);
}
#[test]
fn test_idempotent() {
let mut g = benzene();
let n1 = perceive_aromaticity(&mut g);
let n2 = perceive_aromaticity(&mut g);
assert_eq!(n1, n2);
}
}