use chematic_core::{AtomIdx, BondOrder, Molecule, implicit_hcount};
#[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
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MmffType {
Csp3 = 1, Csp2 = 2, Ccarbonyl = 3, Caromatic = 37, Namine = 6, Namide = 9, Nsp2 = 10, NarH = 39, Nar = 38, Ocarbonyl = 7, Ohydroxyl = 32, Oether = 35, Oester = 41, Oaromatic = 59, Sthio = 15, Ssulfonyl = 18, Saromatic = 44, F = 11,
Cl = 12,
Br = 13,
I = 14,
P = 25,
Hcarbon = 5, Hamine = 23, Hamide = 28, Hoxygen = 21, Hthiol = 33, Other = 0,
}
fn is_amide_n(mol: &Molecule, n_idx: AtomIdx) -> bool {
mol.neighbors(n_idx).any(|(nb, _)| {
let an = mol.atom(nb).element.atomic_number();
match an {
6 => mol.neighbors(nb).any(|(c_nb, bi)| {
mol.atom(c_nb).element.atomic_number() == 8
&& mol.bond(bi).order == BondOrder::Double
}),
16 => mol.neighbors(nb).any(|(s_nb, bi)| {
mol.atom(s_nb).element.atomic_number() == 8
&& mol.bond(bi).order == BondOrder::Double
}),
_ => false,
}
})
}
fn is_carbonyl_oxygen(mol: &Molecule, idx: AtomIdx) -> bool {
mol.neighbors(idx)
.any(|(_, bi)| mol.bond(bi).order == BondOrder::Double)
}
pub fn assign_mmff94_type(mol: &Molecule, idx: AtomIdx) -> MmffType {
let atom = mol.atom(idx);
let an = atom.element.atomic_number();
match an {
6 => {
if atom.aromatic {
return MmffType::Caromatic;
}
if mol.neighbors(idx).any(|(nb, bi)| {
mol.atom(nb).element.atomic_number() == 8 && mol.bond(bi).order == BondOrder::Double
}) {
return MmffType::Ccarbonyl;
}
if mol
.neighbors(idx)
.any(|(_, bi)| mol.bond(bi).order == BondOrder::Double)
{
return MmffType::Csp2;
}
MmffType::Csp3
}
7 => {
if atom.aromatic {
let h = implicit_hcount(mol, idx) as usize
+ mol
.neighbors(idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() == 1)
.count();
return if h > 0 { MmffType::NarH } else { MmffType::Nar };
}
if is_amide_n(mol, idx) {
return MmffType::Namide;
}
if mol
.neighbors(idx)
.any(|(_, bi)| mol.bond(bi).order == BondOrder::Double)
{
return MmffType::Nsp2;
}
MmffType::Namine
}
8 => {
if atom.aromatic {
return MmffType::Oaromatic;
}
if is_carbonyl_oxygen(mol, idx) {
return MmffType::Ocarbonyl;
}
let h = implicit_hcount(mol, idx) as usize
+ mol
.neighbors(idx)
.filter(|(nb, _)| mol.atom(*nb).element.atomic_number() == 1)
.count();
if h > 0 {
return MmffType::Ohydroxyl;
}
let has_carbonyl_nb = mol
.neighbors(idx)
.any(|(nb, _)| mol.atom(nb).element.atomic_number() == 6 && is_carbonyl_c(mol, nb));
if has_carbonyl_nb {
return MmffType::Oester;
}
MmffType::Oether
}
16 => {
if atom.aromatic {
return MmffType::Saromatic;
}
if mol.neighbors(idx).any(|(nb, bi)| {
mol.atom(nb).element.atomic_number() == 8 && mol.bond(bi).order == BondOrder::Double
}) {
return MmffType::Ssulfonyl;
}
MmffType::Sthio
}
9 => MmffType::F,
17 => MmffType::Cl,
35 => MmffType::Br,
53 => MmffType::I,
15 => MmffType::P,
_ => MmffType::Other,
}
}
fn bci_typed(a_type: MmffType, b_type: MmffType, order: BondOrder) -> f64 {
use MmffType::*;
let (lo, hi, flipped) = {
let da = a_type as u8;
let db = b_type as u8;
if da <= db {
(a_type, b_type, false)
} else {
(b_type, a_type, true)
}
};
let ctx = BondCtx::from_order(order);
let bci: f64 = match (lo, hi, ctx) {
(Hcarbon, Csp3, _) => 0.020,
(Hcarbon, Csp2, _) => 0.020,
(Hcarbon, Ccarbonyl, _) => 0.020,
(Hcarbon, Caromatic, _) => 0.020,
(Hamine, Namine, _) => 0.160,
(Hamide, Namide, _) => 0.265, (Hamine, NarH, _) => 0.160,
(Hoxygen, Ohydroxyl, _) => 0.310,
(Hthiol, Sthio, _) => 0.175,
(Csp3, Csp3, _) => 0.000,
(Csp2, Csp2, _) => 0.000,
(Csp3, Csp2, _) => 0.000,
(Csp3, Caromatic, _) => 0.008,
(Csp2, Caromatic, _) => 0.006,
(Csp3, Namine, BondCtx::Single) => 0.020,
(Csp3, Namide, _) => 0.100,
(Ccarbonyl, Namide, _) => 0.310, (Csp2, Nsp2, _) => 0.190, (Caromatic, Nar, _) => 0.040,
(Caromatic, NarH, _) => 0.040,
(Csp3, Nsp2, BondCtx::Double) => 0.190,
(Ccarbonyl, Ocarbonyl, BondCtx::Double) => 0.470, (Csp3, Ohydroxyl, _) => 0.040, (Csp3, Oether, _) => 0.020,
(Csp3, Oester, _) => -0.070, (Ccarbonyl, Oester, _) => 0.180, (Caromatic, Oether, _) => 0.100, (Caromatic, Ohydroxyl, _) => 0.100,
(Csp2, Oether, _) => 0.050,
(Csp2, Ocarbonyl, BondCtx::Double) => 0.470,
(Csp3, Sthio, _) => 0.030,
(Csp2, Ssulfonyl, _) => 0.100,
(Caromatic, Saromatic, _) => 0.060,
(Csp3, F, _) => 0.220,
(Csp3, Cl, _) => 0.085,
(Csp3, Br, _) => 0.040,
(Csp3, I, _) => -0.025,
(Caromatic, F, _) => 0.185,
(Caromatic, Cl, _) => 0.055,
(Caromatic, Br, _) => 0.025,
(Caromatic, I, _) => -0.010,
(Csp2, F, _) => 0.200,
(Csp2, Cl, _) => 0.075,
(Csp3, P, _) => 0.050,
(Namine, Ocarbonyl, BondCtx::Double) => 0.350,
(Namine, Ocarbonyl, _) => 0.200,
(Ocarbonyl, Ssulfonyl, BondCtx::Double) => 0.400,
(Ocarbonyl, Ssulfonyl, _) => 0.250,
(Ocarbonyl, P, BondCtx::Double) => 0.400,
(Oester, P, BondCtx::Single) => 0.300,
(Oether, P, BondCtx::Single) => 0.300,
(Nar, Nar, _) => 0.000,
(NarH, NarH, _) => 0.000,
(Oaromatic, Caromatic, _) => 0.100,
(Saromatic, Caromatic, _) => 0.060,
_ => 0.000,
};
if flipped { -bci } else { bci }
}
pub fn mmff94_charges_typed(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;
}
let types: Vec<MmffType> = (0..n)
.map(|i| assign_mmff94_type(mol, AtomIdx(i as u32)))
.collect();
for (_, bond) in mol.bonds() {
let i = bond.atom1.0 as usize;
let j = bond.atom2.0 as usize;
let ai = mol.atom(bond.atom1);
let aj = mol.atom(bond.atom2);
let ti = if ai.element.atomic_number() == 1 {
h_type_for(mol, bond.atom1)
} else {
types[i]
};
let tj = if aj.element.atomic_number() == 1 {
h_type_for(mol, bond.atom2)
} else {
types[j]
};
let bci = bci_typed(ti, tj, bond.order);
q[i] += bci;
q[j] -= bci;
}
q
}
fn h_type_for(mol: &Molecule, h_idx: AtomIdx) -> MmffType {
if let Some((nb, _)) = mol.neighbors(h_idx).next() {
let t = assign_mmff94_type(mol, nb);
return match t {
MmffType::Namine => MmffType::Hamine,
MmffType::Namide => MmffType::Hamide,
MmffType::NarH => MmffType::Hamine,
MmffType::Ohydroxyl => MmffType::Hoxygen,
MmffType::Sthio => MmffType::Hthiol,
_ => MmffType::Hcarbon,
};
}
MmffType::Hcarbon
}
#[cfg(test)]
mod tests {
#![allow(clippy::manual_contains)]
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
);
}
fn total_q(q: &[f64]) -> f64 {
q.iter().sum()
}
#[test]
fn test_typed_charge_conservation() {
for smi in &[
"CCO",
"CC(C)=O",
"CC(=O)N",
"c1ccccc1",
"CC(=O)O",
"CN",
"CS(=O)(=O)N",
"CC(=O)[O-]",
"[NH4+]",
] {
let mol = parse(smi).unwrap();
let q = mmff94_charges_typed(&mol);
let fc: f64 = mol.atoms().map(|(_, a)| a.charge as f64).sum();
assert!(
(total_q(&q) - fc).abs() < 1e-9,
"charge not conserved for {smi}: sum={:.6} fc={fc}",
total_q(&q)
);
}
}
#[test]
fn test_typed_atom_type_discrimination() {
let ethanol = parse("CCO").unwrap();
let types: Vec<MmffType> = (0..ethanol.atom_count())
.map(|i| assign_mmff94_type(ðanol, AtomIdx(i as u32)))
.collect();
let has_ohydroxyl = types.iter().any(|&t| t == MmffType::Ohydroxyl);
assert!(has_ohydroxyl, "ethanol O should be Ohydroxyl");
let acetone = parse("CC(C)=O").unwrap();
let types_ac: Vec<MmffType> = (0..acetone.atom_count())
.map(|i| assign_mmff94_type(&acetone, AtomIdx(i as u32)))
.collect();
assert!(
types_ac.iter().any(|&t| t == MmffType::Ccarbonyl),
"acetone should have Ccarbonyl"
);
assert!(
types_ac.iter().any(|&t| t == MmffType::Ocarbonyl),
"acetone should have Ocarbonyl"
);
}
#[test]
fn test_typed_ester_vs_ether_oxygen() {
let ester = parse("CC(=O)OC").unwrap(); let ether = parse("COC").unwrap(); let q_ester = mmff94_charges_typed(&ester);
let q_ether = mmff94_charges_typed(ðer);
let ester_types: Vec<_> = (0..ester.atom_count())
.map(|i| assign_mmff94_type(&ester, AtomIdx(i as u32)))
.collect();
assert!(
ester_types.iter().any(|&t| t == MmffType::Oester),
"methyl acetate should have Oester type"
);
assert!((total_q(&q_ester)).abs() < 1e-9, "ester charge neutral");
assert!((total_q(&q_ether)).abs() < 1e-9, "ether charge neutral");
}
#[test]
fn test_typed_aromatic_nitrogen_types() {
let pyridine = parse("c1ccncc1").unwrap();
let pyrrole = parse("c1cc[nH]c1").unwrap();
let pyr_types: Vec<_> = (0..pyridine.atom_count())
.map(|i| assign_mmff94_type(&pyridine, AtomIdx(i as u32)))
.collect();
let rol_types: Vec<_> = (0..pyrrole.atom_count())
.map(|i| assign_mmff94_type(&pyrrole, AtomIdx(i as u32)))
.collect();
assert!(
pyr_types.iter().any(|&t| t == MmffType::Nar),
"pyridine N should be Nar"
);
assert!(
rol_types.iter().any(|&t| t == MmffType::NarH),
"pyrrole N-H should be NarH"
);
let q_pyr = mmff94_charges_typed(&pyridine);
let q_rol = mmff94_charges_typed(&pyrrole);
let n_q_pyr = pyridine
.atoms()
.filter(|(_, a)| a.element.atomic_number() == 7)
.map(|(i, _)| q_pyr[i.0 as usize])
.next()
.unwrap();
let n_q_rol = pyrrole
.atoms()
.filter(|(_, a)| a.element.atomic_number() == 7)
.map(|(i, _)| q_rol[i.0 as usize])
.next()
.unwrap();
assert!(
n_q_pyr < 0.0,
"pyridine N should be negative ({n_q_pyr:.3})"
);
assert!(n_q_pyr.abs() < 1.0, "pyridine N charge in range");
assert!(n_q_rol.abs() < 1.0, "pyrrole N charge in range");
}
#[test]
fn test_typed_ester_o_charge_differs_from_ether_o() {
let methyl_acetate = parse("CC(=O)OC").unwrap(); let dimethyl_ether = parse("COC").unwrap(); let q_ester = mmff94_charges_typed(&methyl_acetate);
let q_ether = mmff94_charges_typed(&dimethyl_ether);
let ester_o_idx = methyl_acetate
.atoms()
.find(|(idx, a)| {
a.element.atomic_number() == 8
&& assign_mmff94_type(&methyl_acetate, *idx) == MmffType::Oester
})
.map(|(i, _)| i.0 as usize);
let ether_o_idx = dimethyl_ether
.atoms()
.find(|(idx, a)| {
a.element.atomic_number() == 8
&& assign_mmff94_type(&dimethyl_ether, *idx) == MmffType::Oether
})
.map(|(i, _)| i.0 as usize);
if let (Some(ei), Some(oi)) = (ester_o_idx, ether_o_idx) {
assert!(
(q_ester[ei] - q_ether[oi]).abs() > 0.001,
"ester O ({:.4}) and ether O ({:.4}) should differ",
q_ester[ei],
q_ether[oi]
);
}
assert!((total_q(&q_ester)).abs() < 1e-9);
assert!((total_q(&q_ether)).abs() < 1e-9);
}
#[test]
fn test_typed_vs_element_bci_carbonyl() {
let acetone = parse("CC(C)=O").unwrap();
let q_t = mmff94_charges_typed(&acetone);
let q_b = mmff94_charges_bci(&acetone);
let o_idx = acetone
.atoms()
.find(|(_, a)| a.element.atomic_number() == 8)
.map(|(i, _)| i.0 as usize)
.unwrap();
assert!(
q_t[o_idx] < -0.3,
"typed acetone O should be < -0.3 (got {:.3})",
q_t[o_idx]
);
assert!(
q_b[o_idx] < -0.3,
"bci acetone O should be < -0.3 (got {:.3})",
q_b[o_idx]
);
}
}