use omgkit_core::{element, Hybridization, MolBuilder};
const IONX_H: f64 = 20.02;
const DAMP: f64 = 0.5;
const DAMP_SCALE: f64 = 0.5;
pub const DEFAULT_ITERATIONS: usize = 12;
static PARAMS: &[(&str, &str, f64, f64, f64)] = &[
("H", "*", 7.17, 6.24, -0.56),
("C", "sp3", 7.98, 9.18, 1.88),
("C", "sp2", 8.79, 9.32, 1.51),
("C", "sp", 10.39, 9.45, 0.73),
("N", "sp3", 11.54, 10.82, 1.36),
("N", "sp2", 12.87, 11.15, 0.85),
("N", "sp", 15.68, 11.7, -0.27),
("O", "sp3", 14.18, 12.92, 1.39),
("O", "sp2", 17.07, 13.79, 0.47),
("F", "sp3", 14.66, 13.85, 2.31),
("Cl", "sp3", 11.00, 9.69, 1.35),
("Br", "sp3", 10.08, 8.47, 1.16),
("I", "sp3", 9.9, 7.96, 0.96),
("S", "sp3", 10.14, 9.13, 1.38),
("S", "so", 10.14, 9.13, 1.38),
("S", "so2", 12.00, 10.81, 1.20),
("S", "sp2", 10.88, 9.49, 1.33),
("P", "sp3", 8.90, 8.24, 0.96),
("P", "sp2", 9.665, 8.530, 0.735),
("Si", "sp3", 7.300, 6.567, 0.657),
("Si", "sp2", 7.905, 6.748, 0.443),
("Si", "sp", 9.065, 7.027, -0.002),
("B", "sp3", 5.980, 6.820, 1.605),
("B", "sp2", 6.420, 6.807, 1.322),
("Be", "sp3", 3.845, 6.755, 3.165),
("Be", "sp2", 4.005, 6.725, 3.035),
("Mg", "sp2", 3.565, 5.572, 2.197),
("Mg", "sp3", 3.300, 5.587, 2.447),
("Mg", "sp", 4.040, 5.472, 1.823),
("Al", "sp3", 5.375, 4.953, 0.867),
("Al", "sp2", 5.795, 5.020, 0.695),
];
fn params_for(symbol: &str, mode: &str) -> [f64; 3] {
for &(s, m, a, b, c) in PARAMS {
if s == symbol && m == mode {
return [a, b, c];
}
}
[0.0, 0.0, 0.0]
}
fn mode_of(mol: &MolBuilder, idx: u32) -> &'static str {
let atom = &mol.atoms()[idx as usize];
match atom.hybridization {
Hybridization::Sp3 => "sp3",
Hybridization::Sp2 => "sp2",
Hybridization::Sp => "sp",
_ => {
if atom.atomic_num == 1 {
"*"
} else if atom.atomic_num == 16 {
let n_oxygen = mol
.neighbors(idx)
.filter(|&(nbr, _)| mol.atoms()[nbr as usize].atomic_num == 8)
.count();
match n_oxygen {
2 => "so2",
1 => "so",
_ => "sp3",
}
} else {
""
}
}
}
}
fn split_charge_conjugated(mol: &MolBuilder, charges: &mut [f64]) {
let n = mol.num_atoms();
let mut marker: Vec<usize> = Vec::new();
for aix in 0..n {
let atom = &mol.atoms()[aix];
let mut formal = f64::from(atom.formal_charge);
if formal.abs() <= f64::EPSILON || charges[aix].abs() >= f64::EPSILON {
continue;
}
marker.clear();
marker.push(aix);
for (aax, b1) in mol.neighbors(u32::try_from(aix).unwrap_or(0)) {
if !mol.bonds()[b1 as usize]
.flags
.contains(omgkit_core::BondFlags::CONJUGATED)
{
continue;
}
for (yax, b2) in mol.neighbors(aax) {
if b2 == b1 {
continue;
}
if !mol.bonds()[b2 as usize]
.flags
.contains(omgkit_core::BondFlags::CONJUGATED)
{
continue;
}
if mol.atoms()[yax as usize].atomic_num == atom.atomic_num {
formal += f64::from(mol.atoms()[yax as usize].formal_charge);
marker.push(yax as usize);
}
}
}
#[allow(clippy::cast_precision_loss)]
let share = formal / marker.len() as f64;
for &m in &marker {
charges[m] = share;
}
}
}
#[must_use]
pub fn gasteiger_charges(mol: &MolBuilder, n_iter: usize) -> Vec<f64> {
let n = mol.num_atoms();
let mut charges = vec![0.0f64; n];
if n == 0 {
return charges;
}
split_charge_conjugated(mol, &mut charges);
let mut atm_ps = Vec::with_capacity(n);
let mut ionx = Vec::with_capacity(n);
for (idx, atom) in mol.atoms().iter().enumerate() {
let z = atom.atomic_num;
let symbol = element::by_atomic_num(z).map_or("*", |e| e.symbol);
let p = params_for(symbol, mode_of(mol, u32::try_from(idx).unwrap_or(0)));
ionx.push(if z == 1 { IONX_H } else { p[0] + p[1] + p[2] });
atm_ps.push(p);
}
let mut h_charges = vec![0.0f64; n];
let h_params = params_for("H", "*");
let mut energy = vec![0.0f64; n];
let mut damp = DAMP;
for _ in 0..n_iter {
for idx in 0..n {
let p = atm_ps[idx];
energy[idx] = p[0] + charges[idx] * (p[1] + p[2] * charges[idx]);
}
for idx in 0..n {
let mut dq = 0.0;
for (nbr, _) in mol.neighbors(u32::try_from(idx).unwrap_or(0)) {
let nbr = nbr as usize;
let dx = energy[nbr] - energy[idx];
let sgn = f64::from(u8::from(dx >= 0.0));
dq += dx / (sgn * (ionx[idx] - ionx[nbr]) + ionx[nbr]);
}
let n_hs = u32::from(mol.atoms()[idx].num_explicit_hs)
+ u32::from(mol.atoms()[idx].num_implicit_hs);
if n_hs > 0 {
let n_hs_f = f64::from(n_hs);
let q_hs = h_charges[idx] / n_hs_f;
let e_h = h_params[0] + q_hs * (h_params[1] + h_params[2] * q_hs);
let dx = e_h - energy[idx];
let sgn = f64::from(u8::from(dx >= 0.0));
let dq_h = dx / (sgn * (ionx[idx] - IONX_H) + IONX_H);
dq += n_hs_f * dq_h;
h_charges[idx] -= n_hs_f * dq_h * damp;
}
charges[idx] += damp * dq;
}
damp *= DAMP_SCALE;
}
charges
}
#[cfg(test)]
mod tests {
use super::*;
use omgkit_io::smiles;
fn sanitized(smi: &str) -> MolBuilder {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{smi}: {}", e.render()));
crate::pipeline::sanitize(&mut m).unwrap_or_else(|e| panic!("{smi}: {e}"));
m
}
#[test]
fn methane_carbon_is_negative_and_sums_to_zero() {
let mol = sanitized("C");
let q = gasteiger_charges(&mol, DEFAULT_ITERATIONS);
assert_eq!(q.len(), 1);
assert!(q[0] < 0.0, "甲烷的碳应带负电,实得 {}", q[0]);
}
#[test]
fn charges_match_the_external_reference() {
let cases: &[(&str, &[f64])] = &[
("C", &[-0.077_558]),
("CCO", &[-0.041_838, 0.040_221, -0.396_664]),
("CC(=O)O", &[0.033_768, 0.299_685, -0.252_820, -0.481_433]),
(
"CS(=O)(=O)C",
&[0.038_503, 0.144_104, -0.229_414, -0.229_414, 0.038_503],
),
(
"NC(N)=[NH2+]",
&[-0.291_178, 0.335_948, -0.291_178, -0.291_178],
),
];
for &(smi, want) in cases {
let got = gasteiger_charges(&sanitized(smi), DEFAULT_ITERATIONS);
assert_eq!(got.len(), want.len(), "{smi}:原子数对不上");
for (i, (&g, &w)) in got.iter().zip(want).enumerate() {
assert!(
(g - w).abs() < 5e-7,
"{smi} 第 {i} 个原子:本实现 {g},参照 {w}"
);
}
}
}
#[test]
fn an_element_outside_the_table_yields_a_non_finite_charge() {
let mol = sanitized("[Na][Na]");
let q = gasteiger_charges(&mol, DEFAULT_ITERATIONS);
assert!(
q.iter().any(|v| !v.is_finite()),
"表外元素应给出非有限值,实得 {q:?}"
);
}
}