use chematic_core::{AtomIdx, BondOrder, Molecule};
#[derive(Clone, Copy, PartialEq, Eq)]
enum BondCtx {
Single,
Double,
Triple,
Aromatic,
}
impl BondCtx {
fn from_order(o: BondOrder) -> Self {
match o {
BondOrder::Single => BondCtx::Single,
BondOrder::Double => BondCtx::Double,
BondOrder::Triple => BondCtx::Triple,
BondOrder::Aromatic => BondCtx::Aromatic,
_ => BondCtx::Single,
}
}
}
fn is_carbonyl_c(mol: &Molecule, idx: AtomIdx) -> bool {
if mol.atom(idx).element.atomic_number() != 6 {
return false;
}
mol.neighbors(idx).any(|(nb, bond_idx)| {
let bond = mol.bond(bond_idx);
bond.order == BondOrder::Double
&& mol.atom(nb).element.atomic_number() == 8
})
}
fn bond_bci(mol: &Molecule, idx1: AtomIdx, idx2: AtomIdx, order: BondOrder) -> f64 {
let a1 = mol.atom(idx1);
let a2 = mol.atom(idx2);
let an1 = a1.element.atomic_number();
let an2 = a2.element.atomic_number();
let (lo_idx, lo_an, hi_an, flipped) = if an1 <= an2 {
(idx1, an1, an2, false)
} else {
(idx2, an2, an1, true)
};
let hi_idx = if flipped { idx1 } else { idx2 };
let ctx = BondCtx::from_order(order);
let bci: f64 = match (lo_an, hi_an, ctx) {
(1, 6, _) => 0.02, (1, 7, _) => 0.16, (1, 8, _) => 0.30, (1, 16, _) => 0.17,
(6, 6, _) => 0.00,
(6, 7, BondCtx::Single) => {
if is_carbonyl_c(mol, lo_idx) { 0.31 } else { 0.10 }
}
(6, 7, BondCtx::Double) => 0.20, (6, 7, BondCtx::Triple) => 0.15, (6, 7, BondCtx::Aromatic) => 0.12,
(6, 8, BondCtx::Double) => 0.47, (6, 8, BondCtx::Single) => 0.04, (6, 8, BondCtx::Aromatic) => 0.12,
(6, 9, _) => 0.22, (6, 17, _) => 0.04, (6, 35, _) => -0.01, (6, 53, _) => -0.08,
(6, 16, BondCtx::Single) => 0.03,
(6, 16, BondCtx::Double) => 0.30, (6, 16, BondCtx::Aromatic) => 0.06,
(6, 15, _) => 0.05,
(7, 8, BondCtx::Single) => 0.20,
(7, 8, BondCtx::Double) => 0.35,
(7, 8, BondCtx::Aromatic) => 0.20,
(8, 16, BondCtx::Double) => 0.40,
(8, 16, _) => 0.25,
(8, 15, BondCtx::Single) => 0.30,
(8, 15, BondCtx::Double) => 0.40,
_ => 0.00,
};
let _ = hi_idx;
if flipped { -bci } else { bci }
}
pub fn mmff94_charges_bci(mol: &Molecule) -> Vec<f64> {
let n = mol.atom_count();
let mut q = vec![0.0f64; n];
for (qi, atom) in q.iter_mut().zip(mol.atoms().map(|(_, a)| a)) {
*qi = atom.charge as f64;
}
for (_, bond) in mol.bonds() {
let bci = bond_bci(mol, bond.atom1, bond.atom2, bond.order);
q[bond.atom1.0 as usize] += bci;
q[bond.atom2.0 as usize] -= bci;
}
q
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
fn total_charge(charges: &[f64]) -> f64 {
charges.iter().sum()
}
#[test]
fn test_bci_ethanol() {
let mol = parse("CCO").unwrap();
let q = mmff94_charges_bci(&mol);
assert!((total_charge(&q)).abs() < 1e-9, "ethanol total charge should be 0");
let o_q = mol.atoms()
.filter(|(_, a)| a.element.atomic_number() == 8)
.map(|(i, _)| q[i.0 as usize])
.next().unwrap();
assert!(o_q < 0.0, "O in ethanol should be negative, got {}", o_q);
}
#[test]
fn test_bci_acetone() {
let mol = parse("CC(C)=O").unwrap();
let q = mmff94_charges_bci(&mol);
assert!((total_charge(&q)).abs() < 1e-9, "acetone total charge should be 0");
let carbonyl_c_idx = mol.atoms()
.find(|(idx, a)| {
a.element.atomic_number() == 6
&& mol.neighbors(*idx).any(|(nb, bi)| {
mol.bond(bi).order == BondOrder::Double
&& mol.atom(nb).element.atomic_number() == 8
})
})
.map(|(i, _)| i.0 as usize)
.unwrap();
let o_idx = mol.atoms()
.find(|(_, a)| a.element.atomic_number() == 8)
.map(|(i, _)| i.0 as usize)
.unwrap();
assert!(q[carbonyl_c_idx] > 0.0,
"carbonyl C should be positive, got {}", q[carbonyl_c_idx]);
assert!(q[o_idx] < -0.3,
"carbonyl O should be < -0.3, got {}", q[o_idx]);
}
#[test]
fn test_bci_methylamine() {
let mol = parse("CN").unwrap();
let q = mmff94_charges_bci(&mol);
assert!((total_charge(&q)).abs() < 1e-9, "methylamine total charge should be 0");
let n_q = mol.atoms()
.filter(|(_, a)| a.element.atomic_number() == 7)
.map(|(i, _)| q[i.0 as usize])
.next().unwrap();
assert!(n_q < 0.0, "N in methylamine should be negative, got {}", n_q);
}
#[test]
fn test_bci_acetic_acid() {
let mol = parse("CC(=O)O").unwrap();
let q = mmff94_charges_bci(&mol);
assert!((total_charge(&q)).abs() < 1e-9, "acetic acid total charge should be 0");
}
#[test]
fn test_bci_ammonium() {
let mol = parse("[NH4+]").unwrap();
let q = mmff94_charges_bci(&mol);
assert!((total_charge(&q) - 1.0).abs() < 1e-9,
"ammonium total charge should be +1, got {}", total_charge(&q));
}
#[test]
fn test_bci_acetate() {
let mol = parse("CC(=O)[O-]").unwrap();
let q = mmff94_charges_bci(&mol);
assert!((total_charge(&q) + 1.0).abs() < 1e-9,
"acetate total charge should be -1, got {}", total_charge(&q));
}
#[test]
fn test_bci_chloromethane() {
let mol = parse("CCl").unwrap();
let q = mmff94_charges_bci(&mol);
assert!((total_charge(&q)).abs() < 1e-9, "chloromethane total charge should be 0");
let c_q = mol.atoms()
.filter(|(_, a)| a.element.atomic_number() == 6)
.map(|(i, _)| q[i.0 as usize])
.next().unwrap();
let cl_q = mol.atoms()
.filter(|(_, a)| a.element.atomic_number() == 17)
.map(|(i, _)| q[i.0 as usize])
.next().unwrap();
assert!(c_q > 0.0, "C in chloromethane should be positive, got {}", c_q);
assert!(cl_q < 0.0, "Cl in chloromethane should be negative, got {}", cl_q);
}
#[test]
fn test_bci_imidazole() {
let mol = parse("c1cnc[nH]1").unwrap();
let q = mmff94_charges_bci(&mol);
assert!((total_charge(&q)).abs() < 1e-9, "imidazole total charge should be 0");
for c in &q {
assert!(c.abs() < 2.0, "imidazole charge out of range: {}", c);
}
}
#[test]
fn test_bci_amide_vs_amine_bci() {
let formamide = parse("NC=O").unwrap();
let q_amide = mmff94_charges_bci(&formamide);
let methylamine = parse("CN").unwrap();
let q_amine = mmff94_charges_bci(&methylamine);
assert!((total_charge(&q_amide)).abs() < 1e-9, "formamide total charge 0");
assert!((total_charge(&q_amine)).abs() < 1e-9, "methylamine total charge 0");
let amide_n_q = formamide.atoms()
.filter(|(_, a)| a.element.atomic_number() == 7)
.map(|(i, _)| q_amide[i.0 as usize])
.next().unwrap();
let amine_n_q = methylamine.atoms()
.filter(|(_, a)| a.element.atomic_number() == 7)
.map(|(i, _)| q_amine[i.0 as usize])
.next().unwrap();
assert!(amide_n_q < amine_n_q,
"formamide N ({:.3}) should be more negative than methylamine N ({:.3})",
amide_n_q, amine_n_q);
}
}