use crate::mmff94::MMFF94Type;
use chematic_core::Molecule;
pub fn bci(from: MMFF94Type, to: MMFF94Type) -> f64 {
use MMFF94Type::*;
match (from, to) {
(C_sp3, O_Alcohol) => -0.2959, (C_sp3, O_Ether) => -0.2488, (C_sp3, O_Phenol) => -0.2959, (C_sp3, N_sp3_Amine) => -0.2088, (C_sp3, S_Thiol) => -0.1278, (C_sp3, S_Thioether) => -0.1148, (C_sp3, H_Carbon) => 0.0240, (C_sp3, C_sp3) => 0.0, (C_sp3, C_sp2_Alkene) => -0.0500, (C_sp3, C_Aromatic) => -0.0380, (C_sp3, F) => -0.4578, (C_sp3, Cl) => -0.2523, (C_sp3, Br) => -0.1754, (C_sp3, I) => -0.0906,
(C_sp2_Alkene, C_sp2_Alkene) => 0.0,
(C_sp2_Alkene, H_Carbon) => 0.0380, (C_sp2_Alkene, C_sp3) => 0.0500,
(C_Aromatic, H_Aromatic) => 0.0780, (C_Aromatic, C_Aromatic) => 0.0,
(C_Aromatic, N_Aromatic_6ring | N_Aromatic_Pyridine) => -0.1500,
(C_Carbonyl, O_Carbonyl) => -0.4500, (C_Carbonyl, C_sp3) => 0.1000, (C_Carbonyl, H_Carbon) => 0.0500,
(C_Carboxylic, O_Carbonyl) => -0.4400,
(C_Carboxylic, O_Carboxylic) => -0.3600, (C_Ester, O_Carbonyl) => -0.4300,
(C_Ester, O_Ester) => -0.3200,
(C_Amide, O_Amide) => -0.4200,
(C_Amide, N_Amide) => -0.2800,
(O_Alcohol, H_Oxygen) => 0.1806, (O_Phenol, H_Oxygen) => 0.2106, (O_Carboxylic, H_Oxygen) => 0.2500,
(N_sp3_Amine, H_Nitrogen) => 0.1760, (N_Amide, H_Nitrogen) => 0.1900,
(S_Thiol, H_Sulfur) => 0.1000,
(a, b) => -bci_lookup(b, a), }
}
fn bci_lookup(from: MMFF94Type, to: MMFF94Type) -> f64 {
use MMFF94Type::*;
match (from, to) {
(C_sp3, O_Alcohol) => -0.2959,
(C_sp3, O_Ether) => -0.2488,
(C_sp3, O_Phenol) => -0.2959,
(C_sp3, N_sp3_Amine) => -0.2088,
(C_sp3, S_Thiol) => -0.1278,
(C_sp3, S_Thioether) => -0.1148,
(C_sp3, H_Carbon) => 0.0240,
(C_sp3, C_sp3) => 0.0,
(C_sp3, C_sp2_Alkene) => -0.0500,
(C_sp3, C_Aromatic) => -0.0380,
(C_sp3, F) => -0.4578,
(C_sp3, Cl) => -0.2523,
(C_sp3, Br) => -0.1754,
(C_sp3, I) => -0.0906,
(C_sp2_Alkene, C_sp2_Alkene) => 0.0,
(C_sp2_Alkene, H_Carbon) => 0.0380,
(C_sp2_Alkene, C_sp3) => 0.0500,
(C_Aromatic, H_Aromatic) => 0.0780,
(C_Aromatic, C_Aromatic) => 0.0,
(C_Aromatic, N_Aromatic_6ring | N_Aromatic_Pyridine) => -0.1500,
(C_Carbonyl, O_Carbonyl) => -0.4500,
(C_Carbonyl, C_sp3) => 0.1000,
(C_Carbonyl, H_Carbon) => 0.0500,
(C_Carboxylic, O_Carbonyl) => -0.4400,
(C_Carboxylic, O_Carboxylic) => -0.3600,
(C_Ester, O_Carbonyl) => -0.4300,
(C_Ester, O_Ester) => -0.3200,
(C_Amide, O_Amide) => -0.4200,
(C_Amide, N_Amide) => -0.2800,
(O_Alcohol, H_Oxygen) => 0.1806,
(O_Phenol, H_Oxygen) => 0.2106,
(O_Carboxylic, H_Oxygen) => 0.2500,
(N_sp3_Amine, H_Nitrogen) => 0.1760,
(N_Amide, H_Nitrogen) => 0.1900,
(S_Thiol, H_Sulfur) => 0.1000,
_ => 0.0,
}
}
pub fn mmff94_formal_charge(_t: MMFF94Type) -> f64 {
0.0
}
pub fn mmff94_charges_bci(mol: &Molecule) -> Result<Vec<f64>, crate::mmff94::AssignError> {
let types = crate::mmff94::assign_mmff94_types(mol)?;
let n = mol.atom_count();
let mut charges: Vec<f64> = (0..n)
.map(|i| {
let atom = mol.atom(chematic_core::AtomIdx(i as u32));
mmff94_formal_charge(types[i]) + atom.charge as f64
})
.collect();
for (_, bond) in mol.bonds() {
let i = bond.atom1.0 as usize;
let j = bond.atom2.0 as usize;
let ti = types[i];
let tj = types[j];
let delta = bci(ti, tj);
charges[j] += delta;
charges[i] -= delta; }
Ok(charges)
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
fn mol(s: &str) -> chematic_core::Molecule { parse(s).unwrap() }
#[test]
fn test_bci_antisymmetry() {
use MMFF94Type::*;
let pairs = [
(C_sp3, O_Alcohol),
(C_sp3, N_sp3_Amine),
(C_sp3, F),
(O_Alcohol, H_Oxygen),
(N_sp3_Amine, H_Nitrogen),
];
for (a, b) in pairs {
let forward = bci(a, b);
let reverse = bci(b, a);
assert!(
(forward + reverse).abs() < 1e-10,
"bci({:?},{:?}) = {:.4} but bci({:?},{:?}) = {:.4} (should be negatives)",
a, b, forward, b, a, reverse
);
}
}
#[test]
fn test_bci_charge_neutrality() {
for smiles in &["C", "CO", "CC", "CCN", "CC(=O)C", "c1ccccc1"] {
let m = mol(smiles);
let charges = mmff94_charges_bci(&m).expect("charge calculation failed");
let total: f64 = charges.iter().sum();
assert!(
total.abs() < 0.05,
"{smiles}: total charge = {total:.4} (should be ~0)"
);
}
}
#[test]
fn test_bci_qualitative_methanol() {
let m = mol("CO");
let charges = mmff94_charges_bci(&m).unwrap();
let o_idx = m.atoms()
.find(|(_, a)| a.element.atomic_number() == 8)
.map(|(i, _)| i.0 as usize)
.unwrap();
assert!(charges[o_idx] < -0.20,
"methanol O charge = {:.3}, expected < -0.20", charges[o_idx]);
assert!(charges[o_idx] > -0.60,
"methanol O charge = {:.3}, expected > -0.60", charges[o_idx]);
}
#[test]
fn test_bci_qualitative_amine() {
let m = mol("CCN");
let charges = mmff94_charges_bci(&m).unwrap();
let n_idx = m.atoms()
.find(|(_, a)| a.element.atomic_number() == 7)
.map(|(i, _)| i.0 as usize)
.unwrap();
assert!(charges[n_idx] < -0.10,
"amine N = {:.3}, expected negative", charges[n_idx]);
}
#[test]
fn test_bci_qualitative_halogens() {
let cf = mol("CF");
let ccl = mol("CCl");
let cbr = mol("CBr");
let f_charge = |m: &chematic_core::Molecule| {
mmff94_charges_bci(m).unwrap().into_iter()
.zip(m.atoms())
.find(|(_, (_, a))| matches!(a.element.atomic_number(), 9 | 17 | 35))
.map(|(q, _)| q)
.unwrap()
};
let qf = f_charge(&cf);
let qcl = f_charge(&ccl);
let qbr = f_charge(&cbr);
assert!(qf < qcl, "F ({qf:.3}) should be more negative than Cl ({qcl:.3})");
assert!(qcl < qbr, "Cl ({qcl:.3}) should be more negative than Br ({qbr:.3})");
}
#[test]
fn test_bci_ketone_oxygen() {
let m = mol("CC(=O)C"); let charges = mmff94_charges_bci(&m).unwrap();
let o_idx = m.atoms()
.find(|(_, a)| a.element.atomic_number() == 8)
.map(|(i, _)| i.0 as usize)
.unwrap();
assert!(charges[o_idx] < -0.30,
"ketone O = {:.3}, expected < -0.30", charges[o_idx]);
}
}