use omgkit_core::MolBuilder;
use crate::valence::{explicit_valence_nonstrict, implicit_hs_nonstrict};
pub fn adjust_hs(mol: &mut MolBuilder, implicit_before: &[u8]) -> usize {
assert_eq!(
implicit_before.len(),
mol.num_atoms(),
"芳香化前的隐式氢数组长度与原子数不符"
);
let mut changed = 0;
for i in 0..mol.num_atoms() as u32 {
let orig_implicit = i32::from(implicit_before[i as usize]);
let ev = explicit_valence_nonstrict(mol, i);
let new_implicit = i32::from(implicit_hs_nonstrict(mol, i, ev));
if new_implicit >= orig_implicit {
continue;
}
let gained = orig_implicit - new_implicit;
if let Some(a) = mol.atom_mut(i) {
a.num_explicit_hs = a
.num_explicit_hs
.saturating_add(u8::try_from(gained).unwrap_or(u8::MAX));
a.num_implicit_hs = u8::try_from(new_implicit).unwrap_or(0);
}
changed += 1;
}
changed
}
#[cfg(test)]
mod tests {
use omgkit_core::AtomFlags;
use omgkit_io::smiles;
use super::*;
use crate::{
assign_radicals, clean_up, kekulize, perceive_rings, set_aromaticity,
valence::update_property_cache,
};
fn upto_step9(smi: &str) -> (MolBuilder, Vec<u8>) {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{}", e.render()));
clean_up(&mut m);
update_property_cache(&mut m).expect("价键");
let _ = perceive_rings(&mut m);
kekulize(&mut m).expect("kekulize");
assign_radicals(&mut m);
set_aromaticity(&mut m);
let before: Vec<u8> = m.atoms().iter().map(|a| a.num_implicit_hs).collect();
update_property_cache(&mut m).expect("收尾价键");
(m, before)
}
fn total_hs(m: &MolBuilder, i: usize) -> u32 {
let a = m.atoms()[i];
u32::from(a.num_explicit_hs) + u32::from(a.num_implicit_hs)
}
#[test]
fn pyrrole_nitrogen_keeps_its_hydrogen() {
let (mut m, before) = upto_step9("c1cc[nH]c1");
assert_eq!(total_hs(&m, 3), 0, "第 9 步之后氢确实'消失'了");
assert_eq!(adjust_hs(&mut m, &before), 1, "应当只调整了这一个原子");
assert_eq!(m.atoms()[3].num_explicit_hs, 1);
assert_eq!(m.atoms()[3].num_implicit_hs, 0);
assert_eq!(total_hs(&m, 3), 1, "总氢数恢复");
assert!(
m.atoms()[3].flags.contains(AtomFlags::AROMATIC),
"仍是芳香的"
);
}
#[test]
fn benzene_is_untouched() {
let (mut m, implicit_before) = upto_step9("c1ccccc1");
let atoms_before: Vec<_> = m.atoms().to_vec();
assert_eq!(adjust_hs(&mut m, &implicit_before), 0);
assert_eq!(m.atoms(), &atoms_before[..]);
}
#[test]
fn substituted_nitrogen_gains_nothing() {
let (mut m, before) = upto_step9("Cn1cccc1");
adjust_hs(&mut m, &before);
assert_eq!(total_hs(&m, 1), 0, "N 上是甲基,不是氢");
}
#[test]
fn is_idempotent() {
for smi in ["c1cc[nH]c1", "c1ccc2[nH]ccc2c1", "c1ccccc1", "CCO"] {
let (mut m, before) = upto_step9(smi);
adjust_hs(&mut m, &before);
let once: Vec<_> = m.atoms().to_vec();
let now: Vec<u8> = m.atoms().iter().map(|a| a.num_implicit_hs).collect();
assert_eq!(adjust_hs(&mut m, &now), 0, "{smi}:第二次不该再有调整");
assert_eq!(m.atoms(), &once[..], "{smi}:不幂等");
}
}
}