use omgkit_core::{BondOrder, MolBuilder};
pub fn add_explicit_hs(mol: &mut MolBuilder, order: &[u32]) -> usize {
let n = mol.num_atoms();
assert_eq!(order.len(), n, "秩与原子数必须一一对应");
let mut todo: Vec<(u32, u8)> = Vec::new();
for a in 0..u32::try_from(n).unwrap_or(u32::MAX) {
let at = mol.atoms()[a as usize];
let k = at.num_implicit_hs.saturating_add(at.num_explicit_hs);
if k > 0 {
todo.push((a, k));
}
}
todo.sort_by_key(|(a, _)| (order[*a as usize], *a));
let mut added = 0usize;
for (a, k) in todo {
for _ in 0..k {
let h = mol.add_atom(1);
if mol.add_bond(a, h, BondOrder::Single).is_ok() {
added += 1;
}
}
if let Some(at) = mol.atom_mut(a) {
at.num_implicit_hs = 0;
at.num_explicit_hs = 0;
}
}
added
}
#[cfg(test)]
mod tests {
use super::*;
fn prep(smi: &str) -> MolBuilder {
let mut m = omgkit_io::smiles::parse(smi).expect("测试用的 SMILES 该能解析");
crate::pipeline::sanitize(&mut m).expect("测试用的分子该能净化");
m
}
fn ranks(m: &MolBuilder) -> Vec<u32> {
omgkit_io::canon::classed_ranks(m)
}
#[test]
fn the_hydrogen_count_comes_out_right() {
for (smi, want) in [
("C", 4),
("CC", 6),
("CCO", 6),
("c1ccccc1", 6),
("[nH]1cccc1", 5),
("CC(=O)OC", 6),
("[Na+].[Cl-]", 0),
] {
let mut m = prep(smi);
let r = ranks(&m);
let before = m.num_atoms();
let got = add_explicit_hs(&mut m, &r);
assert_eq!(got, want, "{smi} 补出来的氢数");
assert_eq!(m.num_atoms(), before + want, "{smi} 的原子总数");
for i in before..m.num_atoms() {
assert_eq!(m.atoms()[i].atomic_num, 1, "{smi} 第 {i} 个新原子不是氢");
}
}
}
#[test]
fn every_original_index_still_means_the_same_atom() {
let mut m = prep("CC(=O)OCC");
let r = ranks(&m);
let before: Vec<(u8, i8)> = m
.atoms()
.iter()
.map(|a| (a.atomic_num, a.formal_charge))
.collect();
let bonds_before: Vec<(u32, u32, BondOrder)> = m
.bonds()
.iter()
.map(|b| (b.begin, b.end, b.order))
.collect();
add_explicit_hs(&mut m, &r);
for (i, want) in before.iter().enumerate() {
let got = (m.atoms()[i].atomic_num, m.atoms()[i].formal_charge);
assert_eq!(got, *want, "第 {i} 个原子变了");
}
for (i, want) in bonds_before.iter().enumerate() {
let b = m.bonds()[i];
assert_eq!((b.begin, b.end, b.order), *want, "第 {i} 根键变了");
}
}
#[test]
fn the_new_hydrogens_are_ordered_by_rank_not_by_storage() {
let mut seen: Option<Vec<u32>> = None;
for smi in ["CCO", "OCC", "C(O)C"] {
let mut m = prep(smi);
let r = ranks(&m);
let before = m.num_atoms();
add_explicit_hs(&mut m, &r);
let seq: Vec<u32> = (before..m.num_atoms())
.map(|h| {
let p = m
.neighbors(u32::try_from(h).unwrap())
.next()
.expect("氢总有一个父亲")
.0;
r[p as usize]
})
.collect();
match &seen {
None => seen = Some(seq),
Some(first) => assert_eq!(first, &seq, "{smi} 补氢的次序与别的写法不同"),
}
}
}
#[test]
fn adding_twice_adds_nothing_the_second_time() {
let mut m = prep("CCO");
let r = ranks(&m);
let first = add_explicit_hs(&mut m, &r);
assert_eq!(first, 6);
let r2 = ranks(&m);
let second = add_explicit_hs(&mut m, &r2);
assert_eq!(second, 0, "第二次又补了 {second} 个氢");
}
#[test]
fn the_chiral_tag_is_left_alone_on_purpose() {
let mut m = prep("N[C@@H](C)C(=O)O");
let r = ranks(&m);
let tags: Vec<_> = m.atoms().iter().map(|a| a.chiral_tag).collect();
add_explicit_hs(&mut m, &r);
for (i, want) in tags.iter().enumerate() {
assert_eq!(
m.atoms()[i].chiral_tag,
*want,
"第 {i} 个原子的手性标记被改了"
);
}
}
}