use crate::system::atomistic::{AtomId, Atomistic, BondId};
use crate::system::element::Element;
use crate::system::molgraph::Atom;
pub fn add_hydrogens(mol: &Atomistic) -> Atomistic {
let mut new_mol = mol.clone();
let additions: Vec<(AtomId, u32)> = new_mol
.atoms()
.filter_map(|(id, atom)| {
let sym = atom.get_str("element")?;
if sym.eq_ignore_ascii_case("H") {
return None; }
let n = implicit_h_count(&new_mol, id)?;
if n == 0 { None } else { Some((id, n)) }
})
.collect();
for (heavy_id, n) in additions {
for _ in 0..n {
let mut h = Atom::new();
h.set("element", "H");
h.set("mass", 1.008_f64);
let h_id = new_mol.add_atom(h);
if let Ok(bid) = new_mol.add_bond(heavy_id, h_id) {
let _ = new_mol.set_bond_prop(bid, "order", 1.0_f64);
}
}
}
new_mol
}
pub fn remove_hydrogens(mol: &Atomistic) -> Atomistic {
let mut new_mol = mol.clone();
let h_ids: Vec<AtomId> = new_mol
.atoms()
.filter_map(|(id, atom)| {
let sym = atom.get_str("element")?;
if !sym.eq_ignore_ascii_case("H") {
return None;
}
if new_mol.neighbors(id).count() == 1 {
Some(id)
} else {
None
}
})
.collect();
for h_id in h_ids {
let _ = new_mol.remove_atom(h_id);
}
new_mol
}
pub fn implicit_h_count(mol: &Atomistic, atom_id: AtomId) -> Option<u32> {
let atom = mol.get_atom(atom_id).ok()?;
let sym = atom.get_str("element")?;
let element = Element::by_symbol(sym)?;
let formal_charge = atom
.get("formal_charge")
.and_then(|v| v.as_f64())
.unwrap_or(0.0)
.round() as i32;
let effective = element.effective_atomic_number(formal_charge)?;
let valences = effective.default_valences();
if valences.is_empty() {
return None; }
let demand: f64 = bond_order_sum(mol, atom_id);
let target = valences
.iter()
.copied()
.find(|&v| v as f64 >= demand - 1e-6);
let target = target?; let n = target as f64 - demand;
if n <= 0.5 {
Some(0)
} else {
Some(n.round() as u32)
}
}
fn bond_order_sum(mol: &Atomistic, atom_id: AtomId) -> f64 {
bond_ids_for(mol, atom_id)
.into_iter()
.filter_map(|bid| mol.get_bond(bid).ok())
.map(|bond| {
bond.props
.get("order")
.and_then(|v| v.as_f64())
.unwrap_or(1.0)
})
.sum()
}
fn bond_ids_for(mol: &Atomistic, atom_id: AtomId) -> Vec<BondId> {
mol.bonds()
.filter_map(|(bid, bond)| {
if bond.nodes[0] == atom_id || bond.nodes[1] == atom_id {
Some(bid)
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::system::molgraph::PropValue;
fn atom(sym: &str) -> Atom {
let mut a = Atom::new();
a.set("element", sym);
a
}
fn bond_with_order(mol: &mut Atomistic, a: AtomId, b: AtomId, order: f64) {
if let Ok(bid) = mol.add_bond(a, b) {
let _ = mol.set_bond_prop(bid, "order", PropValue::F64(order));
}
}
#[test]
fn test_methane_skeleton() {
let mut g = Atomistic::new();
let c = g.add_atom(atom("C"));
let result = add_hydrogens(&g);
assert_eq!(g.n_atoms(), 1);
assert_eq!(result.n_atoms(), 5);
assert_eq!(result.n_bonds(), 4);
let n_h = result
.atoms()
.filter(|(_, a)| a.get_str("element") == Some("H"))
.count();
assert_eq!(n_h, 4);
let _ = c; }
#[test]
fn test_ethane_c_c() {
let mut g = Atomistic::new();
let c1 = g.add_atom(atom("C"));
let c2 = g.add_atom(atom("C"));
bond_with_order(&mut g, c1, c2, 1.0);
let result = add_hydrogens(&g);
assert_eq!(result.n_atoms(), 8); }
#[test]
fn test_ethylene_c_double_c() {
let mut g = Atomistic::new();
let c1 = g.add_atom(atom("C"));
let c2 = g.add_atom(atom("C"));
bond_with_order(&mut g, c1, c2, 2.0);
let result = add_hydrogens(&g);
assert_eq!(result.n_atoms(), 6); }
#[test]
fn test_benzene_aromatic() {
let mut g = Atomistic::new();
let ids: Vec<AtomId> = (0..6).map(|_| g.add_atom(atom("C"))).collect();
for i in 0..6 {
bond_with_order(&mut g, ids[i], ids[(i + 1) % 6], 1.5);
}
let result = add_hydrogens(&g);
assert_eq!(result.n_atoms(), 12); }
#[test]
fn test_benzene_kekule() {
let mut g = Atomistic::new();
let ids: Vec<AtomId> = (0..6).map(|_| g.add_atom(atom("C"))).collect();
let orders = [2.0, 1.0, 2.0, 1.0, 2.0, 1.0];
for i in 0..6 {
bond_with_order(&mut g, ids[i], ids[(i + 1) % 6], orders[i]);
}
let result = add_hydrogens(&g);
let n_h = result
.atoms()
.filter(|(_, a)| a.get_str("element") == Some("H"))
.count();
assert_eq!(n_h, 6, "Kekule benzene should get 6 H, got {}", n_h);
assert_eq!(result.n_atoms(), 12); }
#[test]
fn test_ethylene_round_trip_frame() {
let mut g = Atomistic::new();
let c1 = g.add_atom(atom("C"));
let c2 = g.add_atom(atom("C"));
bond_with_order(&mut g, c1, c2, 2.0);
let frame = g.to_frame();
let g2 = Atomistic::from_frame(&frame).unwrap();
let result = add_hydrogens(&g2);
assert_eq!(result.n_atoms(), 6, "C=C round-trip should give 2C + 4H");
}
#[test]
fn test_acetylene_round_trip_frame() {
let mut g = Atomistic::new();
let c1 = g.add_atom(atom("C"));
let c2 = g.add_atom(atom("C"));
bond_with_order(&mut g, c1, c2, 3.0);
let frame = g.to_frame();
let g2 = Atomistic::from_frame(&frame).unwrap();
let result = add_hydrogens(&g2);
assert_eq!(result.n_atoms(), 4, "C#C round-trip should give 2C + 2H");
}
#[test]
fn test_water() {
let mut g = Atomistic::new();
let _o = g.add_atom(atom("O"));
let result = add_hydrogens(&g);
assert_eq!(result.n_atoms(), 3);
}
#[test]
fn test_ammonia_like() {
let mut g = Atomistic::new();
let n = g.add_atom(atom("N"));
let c = g.add_atom(atom("C"));
bond_with_order(&mut g, n, c, 1.0);
let result = add_hydrogens(&g);
assert_eq!(result.n_atoms(), 7);
}
#[test]
fn test_nh4_plus() {
let mut g = Atomistic::new();
let mut n_atom = Atom::new();
n_atom.set("element", "N");
n_atom.set("formal_charge", 1.0_f64);
let n = g.add_atom(n_atom);
let count = implicit_h_count(&g, n).unwrap();
assert_eq!(count, 4);
}
fn charged_atom_h(sym: &str, fc: f64) -> u32 {
let mut g = Atomistic::new();
let mut a = Atom::new();
a.set("element", sym);
a.set("formal_charge", fc);
let id = g.add_atom(a);
implicit_h_count(&g, id).unwrap_or(0)
}
#[test]
fn test_rdkit_charged_valence_parity() {
assert_eq!(charged_atom_h("C", 1.0), 3, "[CH3+] -> 3 H");
assert_eq!(charged_atom_h("C", -1.0), 3, "[CH3-] -> 3 H");
assert_eq!(charged_atom_h("B", -1.0), 4, "[BH4-] -> 4 H");
assert_eq!(charged_atom_h("N", 1.0), 4, "[NH4+] -> 4 H");
assert_eq!(charged_atom_h("O", -1.0), 1, "[OH-] -> 1 H");
assert_eq!(charged_atom_h("N", -1.0), 2, "[NH2-] -> 2 H");
assert_eq!(charged_atom_h("C", 0.0), 4, "methane C -> 4 H");
assert_eq!(charged_atom_h("O", 0.0), 2, "water O -> 2 H");
assert_eq!(charged_atom_h("N", 0.0), 3, "ammonia N -> 3 H");
}
fn charged_atom_h_int(sym: &str, fc: i32) -> u32 {
let mut g = Atomistic::new();
let mut a = Atom::new();
a.set("element", sym);
a.set("formal_charge", fc); let id = g.add_atom(a);
implicit_h_count(&g, id).unwrap_or(0)
}
#[test]
fn test_int_formal_charge_parity() {
assert_eq!(charged_atom_h_int("N", -1), 2, "[NH2-] (int fc) -> 2 H");
assert_eq!(charged_atom_h_int("N", 1), 4, "[NH4+] (int fc) -> 4 H");
assert_eq!(charged_atom_h_int("O", -1), 1, "[OH-] (int fc) -> 1 H");
assert_eq!(charged_atom_h_int("C", 1), 3, "[CH3+] (int fc) -> 3 H");
assert_eq!(charged_atom_h_int("N", 0), 3, "neutral N -> 3 H");
}
#[test]
fn test_sulfonimide_anion_not_protonated() {
let mut g = Atomistic::new();
let mut n_atom = Atom::new();
n_atom.set("element", "N");
n_atom.set("formal_charge", -1_i32);
let n = g.add_atom(n_atom);
let s1 = g.add_atom(atom("S"));
let s2 = g.add_atom(atom("S"));
bond_with_order(&mut g, n, s1, 1.0);
bond_with_order(&mut g, n, s2, 1.0);
assert_eq!(h_at(&g, n), 0, "sulfonimide [N-] with two bonds -> 0 H");
}
fn h_at(g: &Atomistic, id: AtomId) -> u32 {
implicit_h_count(g, id).unwrap_or(0)
}
#[test]
fn test_rdkit_multi_atom_parity() {
let mut g = Atomistic::new();
let c1 = g.add_atom(atom("C"));
let c2 = g.add_atom(atom("C"));
bond_with_order(&mut g, c1, c2, 1.0);
assert_eq!(h_at(&g, c1), 3, "ethane C -> 3 H");
assert_eq!(h_at(&g, c2), 3, "ethane C -> 3 H");
let mut g = Atomistic::new();
let c1 = g.add_atom(atom("C"));
let c2 = g.add_atom(atom("C"));
bond_with_order(&mut g, c1, c2, 2.0);
assert_eq!(h_at(&g, c1), 2, "ethylene C -> 2 H");
let mut g = Atomistic::new();
let ids: Vec<AtomId> = (0..6).map(|_| g.add_atom(atom("C"))).collect();
for i in 0..6 {
bond_with_order(&mut g, ids[i], ids[(i + 1) % 6], 1.5);
}
assert_eq!(h_at(&g, ids[0]), 1, "benzene C -> 1 H");
let mut g = Atomistic::new();
let c_me = g.add_atom(atom("C"));
let c_carb = g.add_atom(atom("C"));
let o_dbl = g.add_atom(atom("O"));
let mut o_minus = Atom::new();
o_minus.set("element", "O");
o_minus.set("formal_charge", -1.0_f64);
let o_minus = g.add_atom(o_minus);
bond_with_order(&mut g, c_me, c_carb, 1.0);
bond_with_order(&mut g, c_carb, o_dbl, 2.0);
bond_with_order(&mut g, c_carb, o_minus, 1.0);
assert_eq!(h_at(&g, c_me), 3, "acetate methyl C -> 3 H");
assert_eq!(h_at(&g, c_carb), 0, "acetate carbonyl C -> 0 H");
assert_eq!(h_at(&g, o_dbl), 0, "acetate =O -> 0 H");
assert_eq!(h_at(&g, o_minus), 0, "acetate [O-] -> 0 H");
}
#[test]
fn test_no_double_h_on_existing_hydrogen() {
let mut g = Atomistic::new();
let c = g.add_atom(atom("C"));
let h = g.add_atom(atom("H"));
bond_with_order(&mut g, c, h, 1.0);
let result = add_hydrogens(&g);
let n_h = result
.atoms()
.filter(|(_, a)| a.get_str("element") == Some("H"))
.count();
assert_eq!(n_h, 4); }
#[test]
fn test_remove_hydrogens_methane() {
let mut g = Atomistic::new();
g.add_atom(atom("C"));
let with_h = add_hydrogens(&g);
assert_eq!(with_h.n_atoms(), 5);
let stripped = remove_hydrogens(&with_h);
assert_eq!(stripped.n_atoms(), 1);
assert_eq!(stripped.n_bonds(), 0);
}
#[test]
fn test_remove_hydrogens_ethane() {
let mut g = Atomistic::new();
let c1 = g.add_atom(atom("C"));
let c2 = g.add_atom(atom("C"));
bond_with_order(&mut g, c1, c2, 1.0);
let with_h = add_hydrogens(&g);
assert_eq!(with_h.n_atoms(), 8);
let stripped = remove_hydrogens(&with_h);
assert_eq!(stripped.n_atoms(), 2);
assert_eq!(stripped.n_bonds(), 1);
}
#[test]
fn test_remove_hydrogens_immutable() {
let mut g = Atomistic::new();
g.add_atom(atom("C"));
let with_h = add_hydrogens(&g);
let before = with_h.n_atoms();
let _stripped = remove_hydrogens(&with_h);
assert_eq!(with_h.n_atoms(), before);
}
#[test]
fn test_remove_hydrogens_no_h_present() {
let mut g = Atomistic::new();
let c1 = g.add_atom(atom("C"));
let c2 = g.add_atom(atom("C"));
bond_with_order(&mut g, c1, c2, 2.0);
let stripped = remove_hydrogens(&g);
assert_eq!(stripped.n_atoms(), 2);
assert_eq!(stripped.n_bonds(), 1);
}
#[test]
fn test_remove_hydrogens_cascades_angles() {
let mut g = Atomistic::new();
let c = g.add_atom(atom("C"));
let h1 = g.add_atom(atom("H"));
let h2 = g.add_atom(atom("H"));
bond_with_order(&mut g, c, h1, 1.0);
bond_with_order(&mut g, c, h2, 1.0);
g.add_angle(h1, c, h2).expect("add angle");
assert_eq!(g.n_angles(), 1);
let stripped = remove_hydrogens(&g);
assert_eq!(stripped.n_atoms(), 1);
assert_eq!(stripped.n_bonds(), 0);
assert_eq!(stripped.n_angles(), 0);
}
}