use alloc::vec::Vec;
use elements_rs::{AllowedValences, ChargedValences, Element};
use geometric_traits::traits::SparseValuedMatrix2DRef;
use super::{Smiles, SmilesAtomPolicy};
use crate::{
atom::{Atom, AtomSyntax, atom_symbol::AtomSymbol},
bond::Bond,
};
impl<AtomPolicy: crate::smiles::SmilesAtomPolicy> Smiles<AtomPolicy> {
#[inline]
#[must_use]
pub(crate) fn recompute_implicit_hydrogen_counts(&self) -> Vec<u8> {
self.nodes()
.iter()
.enumerate()
.map(|(node_id, node)| implicit_hydrogens_for_node(self, node_id, node))
.collect()
}
#[inline]
#[must_use]
pub fn implicit_hydrogen_counts(&self) -> &[u8] {
&self.implicit_hydrogen_cache
}
#[inline]
#[must_use]
pub fn implicit_hydrogen_count(&self, id: usize) -> u8 {
assert!(
id < self.implicit_hydrogen_cache.len(),
"invalid atom index {id} for graph with {} atoms",
self.implicit_hydrogen_cache.len()
);
self.implicit_hydrogen_cache[id]
}
}
#[inline]
fn implicit_hydrogens_for_node<AtomPolicy: SmilesAtomPolicy>(
smiles: &Smiles<AtomPolicy>,
node_id: usize,
node: &Atom,
) -> u8 {
let explicit_valence = saturated_explicit_valence(smiles, node_id);
match node.syntax() {
AtomSyntax::Bracket => 0,
AtomSyntax::OrganicSubset => {
match node.symbol() {
AtomSymbol::WildCard => 0,
AtomSymbol::Element(element) => {
if node.aromatic() {
aromatic_implicit_hydrogens(element, explicit_valence)
} else {
aliphatic_implicit_hydrogens(element, explicit_valence)
}
}
}
}
}
}
#[inline]
pub(super) fn implicit_hydrogens_if_written_unbracketed(
smiles: &Smiles<impl SmilesAtomPolicy>,
node_id: usize,
node: &Atom,
) -> u8 {
let explicit_valence = saturated_explicit_valence(smiles, node_id);
match node.symbol() {
AtomSymbol::WildCard => 0,
AtomSymbol::Element(element) => {
if node.aromatic() {
aromatic_implicit_hydrogens(element, explicit_valence)
} else {
aliphatic_implicit_hydrogens(element, explicit_valence)
}
}
}
}
#[inline]
pub(crate) fn explicit_valence(smiles: &Smiles<impl SmilesAtomPolicy>, node_id: usize) -> u16 {
smiles
.bond_matrix()
.sparse_row_values_ref(node_id)
.map(|entry| u16::from(bond_order(entry.bond())))
.sum()
}
#[inline]
fn saturated_explicit_valence(smiles: &Smiles<impl SmilesAtomPolicy>, node_id: usize) -> u8 {
u8::try_from(explicit_valence(smiles, node_id)).unwrap_or(u8::MAX)
}
#[inline]
pub(crate) fn bond_order(bond: Bond) -> u8 {
match bond {
Bond::Single | Bond::Up | Bond::Down => 1,
Bond::Double => 2,
Bond::Triple => 3,
Bond::Quadruple => 4,
}
}
#[inline]
fn aliphatic_implicit_hydrogens(element: Element, explicit_valence: u8) -> u8 {
target_valence(element, explicit_valence)
.map_or(0, |target| target.saturating_sub(explicit_valence))
}
#[inline]
fn target_valence(element: Element, explicit_valence: u8) -> Option<u8> {
if matches!(element, Element::F | Element::Cl | Element::Br) {
return (explicit_valence <= 1).then_some(1);
}
if element == Element::I {
return [1, 3, 5].into_iter().find(|candidate| *candidate >= explicit_valence);
}
let charged_valences = element.valences_at_charge(0);
if charged_valences.is_empty() {
let allowed_valences = element.allowed_valences();
return allowed_valences.iter().copied().find(|candidate| *candidate >= explicit_valence);
}
charged_valences.iter().copied().find(|candidate| *candidate >= explicit_valence)
}
#[inline]
fn aromatic_implicit_hydrogens(element: Element, explicit_valence: u8) -> u8 {
if element == Element::C { 3_u8.saturating_sub(explicit_valence) } else { 0 }
}
#[cfg(test)]
mod tests {
use elements_rs::{AllowedValences, ChargedValences, Element};
use super::{
Smiles, aromatic_implicit_hydrogens, bond_order, explicit_valence,
implicit_hydrogens_for_node, target_valence,
};
use crate::{bond::Bond, smiles::WildcardSmiles};
#[test]
fn bracket_atoms_never_gain_implicit_hydrogens() {
let smiles = Smiles::from_str("[CH3][OH-][NH4+]").unwrap();
assert_eq!(smiles.implicit_hydrogen_counts(), &[0, 0, 0]);
}
#[test]
fn unbracketed_halogens_stop_at_valence_one() {
let smiles = Smiles::from_str("FClF").unwrap();
assert_eq!(smiles.implicit_hydrogen_counts(), &[0, 0, 0]);
}
#[test]
fn wildcard_atoms_never_gain_implicit_hydrogens() {
let smiles = WildcardSmiles::from_str("*").unwrap();
assert_eq!(smiles.implicit_hydrogen_counts(), &[0]);
}
#[test]
fn implicit_hydrogens_for_node_covers_direct_organic_subset_path() {
let smiles = Smiles::from_str("C").unwrap();
let node = smiles.node_by_id(0).unwrap();
assert_eq!(implicit_hydrogens_for_node(&smiles, 0, node), 4);
}
#[test]
fn aromatic_defaults_match_current_policy() {
let smiles = Smiles::from_str("c1ccccc1").unwrap();
assert_eq!(smiles.implicit_hydrogen_counts(), &[1, 1, 1, 1, 1, 1]);
let pyridine = Smiles::from_str("n1ccccc1").unwrap();
assert_eq!(pyridine.implicit_hydrogen_counts(), &[0, 1, 1, 1, 1, 1]);
}
#[test]
fn implicit_hydrogen_count_accesses_node_by_id() {
let smiles = Smiles::from_str("CO").unwrap();
assert_eq!(smiles.implicit_hydrogen_count(0), 3);
assert_eq!(smiles.implicit_hydrogen_count(1), 1);
}
#[test]
#[should_panic(expected = "invalid atom index 99")]
fn implicit_hydrogen_count_panics_for_invalid_atom_id() {
let smiles = Smiles::from_str("CO").unwrap();
let _ = smiles.implicit_hydrogen_count(99);
}
#[test]
fn explicit_valence_counts_quadruple_bonds() {
let smiles = Smiles::from_str("C$C").unwrap();
assert_eq!(explicit_valence(&smiles, 0), 4);
assert_eq!(explicit_valence(&smiles, 1), 4);
assert_eq!(smiles.implicit_hydrogen_counts(), &[0, 0]);
}
#[test]
fn explicit_valence_reports_u16_sum_for_high_degree_atoms() {
let mut input = alloc::string::String::from("C");
for _ in 0..300 {
input.push_str("(C)");
}
let smiles = Smiles::from_str(&input).unwrap();
assert_eq!(explicit_valence(&smiles, 0), 300);
}
#[test]
fn bond_order_maps_double_and_triple_bonds() {
assert_eq!(bond_order(Bond::Double), 2);
assert_eq!(bond_order(Bond::Triple), 3);
}
#[test]
fn target_valence_uses_neutral_iodine_progression() {
assert_eq!(target_valence(Element::I, 2), Some(3));
assert_eq!(target_valence(Element::I, 4), Some(5));
}
#[test]
fn target_valence_falls_back_to_allowed_valences_when_neutral_table_is_empty() {
assert_eq!(Element::Xe.valences_at_charge(0), &[] as &[u8]);
assert_eq!(Element::Xe.allowed_valences(), &[0]);
assert_eq!(target_valence(Element::Xe, 0), Some(0));
}
#[test]
fn aromatic_fallback_defaults_unknown_cases_to_zero() {
assert_eq!(aromatic_implicit_hydrogens(Element::Se, 0), 0);
}
}