use omgkit_core::valence::{explicit_valence_of, implicit_hs_of};
use omgkit_core::MolBuilder;
pub use omgkit_core::valence::{
explicit_valence_nonstrict, implicit_hs_nonstrict, is_aromatic_atom, total_valence_nonstrict,
valence_shift, ValenceError, ValenceErrorKind,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValenceResult {
pub explicit_valence: Vec<i32>,
pub implicit_hs: Vec<u8>,
}
pub fn update_property_cache(mol: &mut MolBuilder) -> Result<ValenceResult, ValenceError> {
let n = mol.num_atoms();
let mut explicit_valence = vec![0i32; n];
let mut implicit_hs = vec![0u8; n];
for i in 0..n as u32 {
let ev = explicit_valence_of(mol, i, true)?;
explicit_valence[i as usize] = ev;
let ih = implicit_hs_of(mol, i, ev, true)?;
implicit_hs[i as usize] = ih;
}
for (i, &h) in implicit_hs.iter().enumerate() {
if let Some(a) = mol.atom_mut(i as u32) {
a.num_implicit_hs = h;
}
}
Ok(ValenceResult {
explicit_valence,
implicit_hs,
})
}
#[cfg(test)]
mod tests {
use omgkit_io::smiles;
use super::*;
fn calc(smi: &str) -> (Vec<i32>, Vec<u8>) {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{}", e.render()));
let r = update_property_cache(&mut m).unwrap_or_else(|e| panic!("{smi}: {e}"));
(r.explicit_valence, r.implicit_hs)
}
#[test]
fn simple_organics() {
assert_eq!(calc("CCO"), (vec![1, 2, 1], vec![3, 2, 1]));
assert_eq!(calc("C"), (vec![0], vec![4]));
assert_eq!(calc("CC(=O)O"), (vec![1, 4, 2, 1], vec![3, 0, 0, 1]));
}
#[test]
fn bracket_atoms_get_no_implicit_hs() {
let (ev, ih) = calc("[CH4]");
assert_eq!(ev, vec![4]);
assert_eq!(ih, vec![0], "NO_IMPLICIT 置位时隐式氢恒为 0");
}
#[test]
fn aromatic_ring_atoms() {
let (ev, ih) = calc("c1ccccc1");
assert!(ev.iter().all(|&v| v == 3), "实际 {ev:?}");
assert!(ih.iter().all(|&h| h == 1), "实际 {ih:?}");
}
#[test]
fn kekulized_form_gives_same_counts() {
let (_, ih_arom) = calc("c1ccccc1");
let (_, ih_kek) = calc("C1=CC=CC=C1");
assert_eq!(ih_arom, ih_kek);
}
#[test]
fn charged_atoms_use_effective_atomic_number() {
let (ev, ih) = calc("[NH4+]");
assert_eq!(ev, vec![4]);
assert_eq!(ih, vec![0]);
let (ev, _) = calc("CC(=O)[O-]");
assert_eq!(ev[3], 1);
}
#[test]
fn hypervalent_sulfur() {
let (ev, ih) = calc("O=S(=O)(O)O");
assert_eq!(ev[1], 6, "S 应为 6 价");
assert_eq!(ih[1], 0);
}
#[test]
fn wildcard_gets_no_implicit_hs() {
let (_, ih) = calc("*CC");
assert_eq!(ih[0], 0, "通配原子不补氢");
}
#[test]
fn overvalent_carbon_is_rejected() {
let mut m = smiles::parse("C(C)(C)(C)(C)C").unwrap();
let err = update_property_cache(&mut m).expect_err("五键碳应当被拒绝");
assert_eq!(err.atom, 0);
assert_eq!(err.kind, ValenceErrorKind::ExplicitValenceTooHigh);
assert_eq!(err.valence, 5);
}
#[test]
fn implicit_hs_written_back_to_atoms() {
let mut m = smiles::parse("CCO").unwrap();
let r = update_property_cache(&mut m).unwrap();
for (i, a) in m.atoms().iter().enumerate() {
assert_eq!(a.num_implicit_hs, r.implicit_hs[i]);
}
}
#[test]
fn dative_bond_asymmetry_flows_through() {
let mut m = smiles::parse("CC").unwrap();
m.bond_mut(0)
.unwrap()
.set_order(omgkit_core::BondOrder::Dative);
let r = update_property_cache(&mut m).unwrap();
assert_eq!(r.explicit_valence[0], 0, "给体(起点)显式价为 0");
assert_eq!(r.explicit_valence[1], 1, "受体(终点)显式价为 1");
assert_eq!(r.implicit_hs, vec![4, 3]);
}
}