use std::fmt;
use crate::bond::BondOrder;
use crate::molecule::{AtomIdx, Molecule};
pub fn implicit_hcount(mol: &Molecule, idx: AtomIdx) -> u8 {
let atom = mol.atom(idx);
if atom.wildcard {
return 0;
}
if let Some(h) = atom.hydrogen_count {
return h;
}
if !atom.element.is_organic_subset() {
return 0;
}
let normal_valences = atom.element.normal_valences();
if normal_valences.is_empty() {
return 0;
}
let charge = atom.charge as i32;
let mut aromatic_count: usize = 0;
let mut non_aromatic_sum: i32 = 0;
for (_, bidx) in mol.neighbors(idx) {
let order = mol.bond(bidx).order;
if order == BondOrder::Aromatic {
aromatic_count += 1;
} else {
non_aromatic_sum += order.order_int() as i32;
}
}
if aromatic_count > 0 {
let effective_sum =
(aromatic_count as f64 * 1.5).floor() as i32 + non_aromatic_sum;
let v = normal_valences[0] as i32 + charge;
if v <= 0 || effective_sum >= v {
return 0;
}
return (v - effective_sum) as u8;
}
let bond_sum = non_aromatic_sum;
let valences_to_check: &[u8] = if atom.aromatic {
&normal_valences[..1]
} else {
normal_valences
};
for &v in valences_to_check {
let target = v as i32 + charge;
if target < 0 {
continue;
}
if target >= bond_sum {
return (target - bond_sum) as u8;
}
}
0
}
pub fn total_hcount(mol: &Molecule, idx: AtomIdx) -> u8 {
implicit_hcount(mol, idx)
}
pub fn bond_order_sum(mol: &Molecule, idx: AtomIdx) -> u8 {
mol.neighbors(idx)
.map(|(_, bidx)| mol.bond(bidx).order.order_int())
.fold(0u8, |acc, x| acc.saturating_add(x))
}
pub fn is_pi_bond(order: BondOrder) -> bool {
matches!(order, BondOrder::Double | BondOrder::Triple | BondOrder::Quadruple)
}
#[derive(Debug, Clone)]
pub struct ValenceError {
pub atom: AtomIdx,
pub actual: u8,
pub allowed: &'static [u8],
}
impl fmt::Display for ValenceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let valences_str = self
.allowed
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"atom {} has valence {} (allowed: [{}])",
self.atom.0, self.actual, valences_str
)
}
}
impl std::error::Error for ValenceError {}
pub fn validate_valence(mol: &Molecule) -> Vec<ValenceError> {
let mut errors = Vec::new();
for (idx, atom) in mol.atoms() {
if atom.wildcard {
continue;
}
let valences = atom.element.normal_valences();
if valences.is_empty() {
continue;
}
let bos = bond_order_sum(mol, idx);
let explicit_h = atom.hydrogen_count.unwrap_or(0);
let used = bos.saturating_add(explicit_h);
let charge = atom.charge as i16;
let has_valid = valences.iter().any(|&v| {
let effective = (v as i16 + charge).max(0) as u8;
effective >= used
});
if !has_valid {
errors.push(ValenceError { atom: idx, actual: used, allowed: valences });
}
}
errors
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atom::Atom;
use crate::bond::BondOrder;
use crate::element::Element;
use crate::molecule::MoleculeBuilder;
fn single_atom(elem: Element) -> Molecule {
let mut b = MoleculeBuilder::new();
b.add_atom(Atom::organic(elem));
b.build()
}
fn two_atoms(e1: Element, e2: Element, order: BondOrder) -> Molecule {
let mut b = MoleculeBuilder::new();
let a = b.add_atom(Atom::organic(e1));
let c = b.add_atom(Atom::organic(e2));
b.add_bond(a, c, order).unwrap();
b.build()
}
#[test]
fn test_methane() {
let mol = single_atom(Element::C);
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 4);
}
#[test]
fn test_ethane_c() {
let mol = two_atoms(Element::C, Element::C, BondOrder::Single);
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 3);
assert_eq!(implicit_hcount(&mol, AtomIdx(1)), 3);
}
#[test]
fn test_ethylene_c() {
let mol = two_atoms(Element::C, Element::C, BondOrder::Double);
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 2);
}
#[test]
fn test_acetylene_c() {
let mol = two_atoms(Element::C, Element::C, BondOrder::Triple);
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 1);
}
#[test]
fn test_nitrogen_amine() {
let mol = single_atom(Element::N);
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 3);
}
#[test]
fn test_nitrogen_triple() {
let mol = two_atoms(Element::N, Element::C, BondOrder::Triple);
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 0);
}
#[test]
fn test_oxygen_ether() {
let mol = single_atom(Element::O);
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 2);
}
#[test]
fn test_fluorine() {
let mol = single_atom(Element::F);
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 1);
}
#[test]
fn test_bracket_atom_explicit_h() {
let mut b = MoleculeBuilder::new();
let atom = Atom::bracket(Element::N, None, Default::default(), 4, 1, None);
b.add_atom(atom);
let mol = b.build();
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 4);
}
#[test]
fn test_hypervalent_sulfur() {
let mut b = MoleculeBuilder::new();
let s = b.add_atom(Atom::organic(Element::S));
for _ in 0..4 {
let c = b.add_atom(Atom::organic(Element::C));
b.add_bond(s, c, BondOrder::Single).unwrap();
}
let mol = b.build();
assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 0);
}
#[test]
fn test_validate_valence_valid_molecules() {
use crate::molecule::AtomIdx as AI;
let mol = single_atom(Element::C);
assert!(validate_valence(&mol).is_empty(), "isolated C must be valid");
let mol = single_atom(Element::O);
assert!(validate_valence(&mol).is_empty(), "isolated O must be valid");
let mol = two_atoms(Element::C, Element::C, BondOrder::Single);
assert!(validate_valence(&mol).is_empty(), "ethane must be valid");
let mol = two_atoms(Element::C, Element::O, BondOrder::Double);
assert!(validate_valence(&mol).is_empty(), "formaldehyde must be valid");
}
#[test]
fn test_validate_valence_pentavalent_carbon() {
let mut b = MoleculeBuilder::new();
let c = b.add_atom(Atom::organic(Element::C));
for _ in 0..5 {
let h = b.add_atom(Atom::new(Element::C));
b.add_bond(c, h, BondOrder::Single).unwrap();
}
let mol = b.build();
let errors = validate_valence(&mol);
assert_eq!(errors.len(), 1, "C with 5 bonds must produce exactly 1 error");
assert_eq!(errors[0].atom, AtomIdx(0));
assert_eq!(errors[0].actual, 5);
}
#[test]
fn test_validate_valence_trivalent_oxygen() {
let mut b = MoleculeBuilder::new();
let o = b.add_atom(Atom::organic(Element::O));
for _ in 0..3 {
let c = b.add_atom(Atom::organic(Element::C));
b.add_bond(o, c, BondOrder::Single).unwrap();
}
let mol = b.build();
let errors = validate_valence(&mol);
assert!(!errors.is_empty(), "O with 3 bonds must be flagged as over-valenced");
assert_eq!(errors[0].atom, AtomIdx(0));
}
#[test]
fn test_validate_valence_ammonium_valid() {
let mut b = MoleculeBuilder::new();
let mut n_atom = Atom::organic(Element::N);
n_atom.charge = 1;
let n = b.add_atom(n_atom);
for _ in 0..4 {
let c = b.add_atom(Atom::organic(Element::C));
b.add_bond(n, c, BondOrder::Single).unwrap();
}
let mol = b.build();
assert!(
validate_valence(&mol).is_empty(),
"N+ with 4 bonds must be valid (ammonium-like)"
);
}
#[test]
fn test_validate_valence_transition_metal_skipped() {
let mut b = MoleculeBuilder::new();
let fe = b.add_atom(Atom::new(Element::FE));
for _ in 0..6 {
let c = b.add_atom(Atom::organic(Element::C));
b.add_bond(fe, c, BondOrder::Single).unwrap();
}
let mol = b.build();
assert!(validate_valence(&mol).is_empty(), "Fe with 6 bonds must be skipped");
}
}