use omgkit_core::{
element, AtomFlags, BondData, BondFlags, BondOrder, BondStereo, ChiralTag, Hybridization,
MolBuilder,
};
use crate::gasteiger::{gasteiger_charges, DEFAULT_ITERATIONS};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AtomDescriptors {
pub atomic_num: u8,
pub total_degree: u32,
pub formal_charge: i8,
pub chiral_tag: ChiralTag,
pub total_num_hs: u32,
pub hybridization: Hybridization,
pub is_aromatic: bool,
pub is_in_ring: bool,
pub mass: f64,
pub electronegativity: Option<f64>,
pub gasteiger_charge: f64,
}
impl AtomDescriptors {
#[must_use]
pub fn gasteiger_is_valid(&self) -> bool {
self.gasteiger_charge.is_finite()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BondDescriptors {
pub begin: u32,
pub end: u32,
pub order: BondOrder,
pub is_conjugated: bool,
pub is_in_ring: bool,
pub stereo: BondStereo,
pub stereo_atoms: Option<[u32; 2]>,
}
fn atom_mass(atomic_num: u8, isotope: u16, el: Option<&'static omgkit_core::Element>) -> f64 {
if isotope == 0 {
return el.map_or(0.0, |e| e.mass);
}
element::isotope_mass(atomic_num, isotope).unwrap_or(if atomic_num == 0 {
0.0
} else {
f64::from(isotope)
})
}
#[must_use]
pub fn atom_descriptors(mol: &MolBuilder) -> Vec<AtomDescriptors> {
let charges = gasteiger_charges(mol, DEFAULT_ITERATIONS);
mol.atoms()
.iter()
.enumerate()
.map(|(i, a)| {
let total_num_hs = u32::from(a.num_explicit_hs) + u32::from(a.num_implicit_hs);
let degree = u32::try_from(mol.degree(u32::try_from(i).unwrap_or(0))).unwrap_or(0);
let el = element::by_atomic_num(a.atomic_num);
AtomDescriptors {
atomic_num: a.atomic_num,
total_degree: degree + total_num_hs,
formal_charge: a.formal_charge,
chiral_tag: a.chiral_tag,
total_num_hs,
hybridization: a.hybridization,
is_aromatic: a.flags.contains(AtomFlags::AROMATIC),
is_in_ring: a.flags.contains(AtomFlags::IN_RING),
mass: atom_mass(a.atomic_num, a.isotope, el),
electronegativity: el.and_then(|e| e.electronegativity),
gasteiger_charge: charges[i],
}
})
.collect()
}
#[must_use]
pub fn bond_descriptors(mol: &MolBuilder) -> Vec<BondDescriptors> {
mol.bonds()
.iter()
.map(|b| BondDescriptors {
begin: b.begin,
end: b.end,
order: b.order,
is_conjugated: b.flags.contains(BondFlags::CONJUGATED),
is_in_ring: b.flags.contains(BondFlags::IN_RING),
stereo: b.stereo,
stereo_atoms: if b.stereo == BondStereo::None
|| b.stereo_atoms[0] == BondData::NO_STEREO_ATOM
|| b.stereo_atoms[1] == BondData::NO_STEREO_ATOM
{
None
} else {
Some(b.stereo_atoms)
},
})
.collect()
}
#[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}"));
omgkit_io::stereo::perceive_bond_stereo(&mut m);
m
}
#[test]
fn total_degree_counts_hydrogen_either_way_but_total_hs_does_not() {
let implicit = atom_descriptors(&sanitized("C"));
assert_eq!(implicit[0].total_degree, 4);
assert_eq!(implicit[0].total_num_hs, 4);
let explicit = atom_descriptors(&sanitized("[H]C([H])([H])[H]"));
let carbon = explicit
.iter()
.find(|d| d.atomic_num == 6)
.expect("该有一个碳");
assert_eq!(carbon.total_degree, 4, "四个显式氢邻居也算进总连接度");
assert_eq!(carbon.total_num_hs, 0, "独立的 [H] 原子不计入总氢数");
}
#[test]
fn missing_is_reported_as_missing() {
let he = atom_descriptors(&sanitized("[He]"));
assert_eq!(he[0].electronegativity, None, "氦没有公认的 Pauling 值");
let na = atom_descriptors(&sanitized("[Na][Na]"));
assert!(
na.iter().any(|d| !d.gasteiger_is_valid()),
"表外元素的电荷该报失效,实得 {:?}",
na.iter().map(|d| d.gasteiger_charge).collect::<Vec<_>>()
);
}
#[test]
fn an_isotope_label_changes_the_mass() {
let d = atom_descriptors(&sanitized("[2H]C"));
let h = d.iter().find(|x| x.atomic_num == 1).expect("该有一个氢");
assert!(
(h.mass - 2.014_101_778).abs() < 1e-9,
"氘的质量应是 2.0141,实得 {}",
h.mass
);
let plain = atom_descriptors(&sanitized("[H]C"));
let h0 = plain
.iter()
.find(|x| x.atomic_num == 1)
.expect("该有一个氢");
assert!(
(h0.mass - 1.008).abs() < f64::EPSILON,
"没标同位素该用标准原子量,实得 {}",
h0.mass
);
}
#[test]
fn benzene_bonds_are_aromatic_conjugated_and_cyclic() {
let d = bond_descriptors(&sanitized("c1ccccc1"));
assert_eq!(d.len(), 6);
for b in &d {
assert_eq!(b.order, BondOrder::Aromatic);
assert!(b.is_conjugated);
assert!(b.is_in_ring);
assert_eq!(b.stereo, BondStereo::None);
}
}
#[test]
fn a_configured_double_bond_reports_its_geometry() {
let d = bond_descriptors(&sanitized("C/C=C/C"));
let ez: Vec<_> = d.iter().filter(|b| b.stereo != BondStereo::None).collect();
assert_eq!(ez.len(), 1, "该有且只有一根键带顺反:{d:?}");
assert_eq!(ez[0].order, BondOrder::Double);
assert!(ez[0].stereo_atoms.is_some(), "带顺反却没有参照原子");
for b in &d {
assert_eq!(
b.stereo == BondStereo::None,
b.stereo_atoms.is_none(),
"顺反与参照原子必须同时有或同时没有:{b:?}"
);
}
}
}