1use crate::bond::BondOrder;
7use crate::molecule::{AtomIdx, Molecule};
8use std::fmt;
9
10pub fn implicit_hcount(mol: &Molecule, idx: AtomIdx) -> u8 {
26 let atom = mol.atom(idx);
27
28 if atom.wildcard {
30 return 0;
31 }
32
33 if let Some(h) = atom.hydrogen_count {
35 return h;
36 }
37
38 if !atom.element.is_organic_subset() {
40 return 0;
41 }
42
43 let normal_valences = atom.element.normal_valences();
44 if normal_valences.is_empty() {
45 return 0;
46 }
47
48 let charge = atom.charge as i32;
49
50 let mut aromatic_count: usize = 0;
52 let mut non_aromatic_sum: i32 = 0;
53 for (_, bidx) in mol.neighbors(idx) {
54 let order = mol.bond(bidx).order;
55 if order == BondOrder::Aromatic {
56 aromatic_count += 1;
57 } else {
58 non_aromatic_sum += order.order_int() as i32;
59 }
60 }
61
62 if aromatic_count > 0 {
63 let effective_sum = (aromatic_count as f64 * 1.5).floor() as i32 + non_aromatic_sum;
74 let v = normal_valences[0] as i32 + charge;
75 if v <= 0 || effective_sum >= v {
76 return 0;
77 }
78 return (v - effective_sum) as u8;
79 }
80
81 let bond_sum = non_aromatic_sum;
83
84 let valences_to_check: &[u8] = if atom.aromatic {
90 &normal_valences[..1]
91 } else {
92 normal_valences
93 };
94
95 for &v in valences_to_check {
97 let target = v as i32 + charge;
98 if target < 0 {
99 continue;
100 }
101 if target >= bond_sum {
102 return (target - bond_sum) as u8;
103 }
104 }
105
106 0
108}
109
110#[deprecated(since = "0.1.95", note = "use `implicit_hcount` directly — the two functions are identical")]
111pub fn total_hcount(mol: &Molecule, idx: AtomIdx) -> u8 {
113 implicit_hcount(mol, idx)
114}
115
116pub fn bond_order_sum(mol: &Molecule, idx: AtomIdx) -> u8 {
119 mol.neighbors(idx)
120 .map(|(_, bidx)| mol.bond(bidx).order.order_int())
121 .fold(0u8, |acc, x| acc.saturating_add(x))
122}
123
124pub fn is_pi_bond(order: BondOrder) -> bool {
126 matches!(
127 order,
128 BondOrder::Double | BondOrder::Triple | BondOrder::Quadruple
129 )
130}
131
132#[derive(Debug, Clone)]
141pub struct ValenceError {
142 pub atom: AtomIdx,
144 pub actual: u8,
146 pub allowed: &'static [u8],
148}
149
150impl fmt::Display for ValenceError {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 let valences_str = self
153 .allowed
154 .iter()
155 .map(|v| v.to_string())
156 .collect::<Vec<_>>()
157 .join(", ");
158 write!(
159 f,
160 "atom {} has valence {} (allowed: [{}])",
161 self.atom.0, self.actual, valences_str
162 )
163 }
164}
165
166impl std::error::Error for ValenceError {}
167
168pub fn validate_valence(mol: &Molecule) -> Vec<ValenceError> {
181 let mut errors = Vec::new();
182 for (idx, atom) in mol.atoms() {
183 if atom.wildcard {
184 continue;
185 }
186 let valences = atom.element.normal_valences();
187 if valences.is_empty() {
188 continue;
189 }
190
191 let bos = bond_order_sum(mol, idx);
192 let explicit_h = atom.hydrogen_count.unwrap_or(0);
193 let used = bos.saturating_add(explicit_h);
194 let charge = atom.charge as i16;
195
196 let has_valid = valences.iter().any(|&v| {
197 let effective = (v as i16 + charge).max(0) as u8;
198 effective >= used
199 });
200
201 if !has_valid {
202 errors.push(ValenceError {
203 atom: idx,
204 actual: used,
205 allowed: valences,
206 });
207 }
208 }
209 errors
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::atom::Atom;
216 use crate::bond::BondOrder;
217 use crate::element::Element;
218 use crate::molecule::MoleculeBuilder;
219
220 fn single_atom(elem: Element) -> Molecule {
221 let mut b = MoleculeBuilder::new();
222 b.add_atom(Atom::organic(elem));
223 b.build()
224 }
225
226 fn two_atoms(e1: Element, e2: Element, order: BondOrder) -> Molecule {
227 let mut b = MoleculeBuilder::new();
228 let a = b.add_atom(Atom::organic(e1));
229 let c = b.add_atom(Atom::organic(e2));
230 b.add_bond(a, c, order).unwrap();
231 b.build()
232 }
233
234 #[test]
235 fn test_methane() {
236 let mol = single_atom(Element::C);
238 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 4);
239 }
240
241 #[test]
242 fn test_ethane_c() {
243 let mol = two_atoms(Element::C, Element::C, BondOrder::Single);
245 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 3);
246 assert_eq!(implicit_hcount(&mol, AtomIdx(1)), 3);
247 }
248
249 #[test]
250 fn test_ethylene_c() {
251 let mol = two_atoms(Element::C, Element::C, BondOrder::Double);
253 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 2);
254 }
255
256 #[test]
257 fn test_acetylene_c() {
258 let mol = two_atoms(Element::C, Element::C, BondOrder::Triple);
260 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 1);
261 }
262
263 #[test]
264 fn test_nitrogen_amine() {
265 let mol = single_atom(Element::N);
267 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 3);
268 }
269
270 #[test]
271 fn test_nitrogen_triple() {
272 let mol = two_atoms(Element::N, Element::C, BondOrder::Triple);
274 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 0);
275 }
276
277 #[test]
278 fn test_oxygen_ether() {
279 let mol = single_atom(Element::O);
281 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 2);
282 }
283
284 #[test]
285 fn test_fluorine() {
286 let mol = single_atom(Element::F);
288 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 1);
289 }
290
291 #[test]
292 fn test_bracket_atom_explicit_h() {
293 let mut b = MoleculeBuilder::new();
295 let atom = Atom::bracket(Element::N, None, Default::default(), 4, 1, None);
296 b.add_atom(atom);
297 let mol = b.build();
298 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 4);
299 }
300
301 #[test]
302 fn test_hypervalent_sulfur() {
303 let mut b = MoleculeBuilder::new();
305 let s = b.add_atom(Atom::organic(Element::S));
306 for _ in 0..4 {
307 let c = b.add_atom(Atom::organic(Element::C));
308 b.add_bond(s, c, BondOrder::Single).unwrap();
309 }
310 let mol = b.build();
311 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 0);
312 }
313
314 #[test]
319 fn test_validate_valence_valid_molecules() {
320 let mol = single_atom(Element::C);
323 assert!(
324 validate_valence(&mol).is_empty(),
325 "isolated C must be valid"
326 );
327
328 let mol = single_atom(Element::O);
330 assert!(
331 validate_valence(&mol).is_empty(),
332 "isolated O must be valid"
333 );
334
335 let mol = two_atoms(Element::C, Element::C, BondOrder::Single);
337 assert!(validate_valence(&mol).is_empty(), "ethane must be valid");
338
339 let mol = two_atoms(Element::C, Element::O, BondOrder::Double);
341 assert!(
342 validate_valence(&mol).is_empty(),
343 "formaldehyde must be valid"
344 );
345 }
346
347 #[test]
348 fn test_validate_valence_pentavalent_carbon() {
349 let mut b = MoleculeBuilder::new();
351 let c = b.add_atom(Atom::organic(Element::C));
352 for _ in 0..5 {
353 let h = b.add_atom(Atom::new(Element::C));
354 b.add_bond(c, h, BondOrder::Single).unwrap();
355 }
356 let mol = b.build();
357 let errors = validate_valence(&mol);
358 assert_eq!(
359 errors.len(),
360 1,
361 "C with 5 bonds must produce exactly 1 error"
362 );
363 assert_eq!(errors[0].atom, AtomIdx(0));
364 assert_eq!(errors[0].actual, 5);
365 }
366
367 #[test]
368 fn test_validate_valence_trivalent_oxygen() {
369 let mut b = MoleculeBuilder::new();
371 let o = b.add_atom(Atom::organic(Element::O));
372 for _ in 0..3 {
373 let c = b.add_atom(Atom::organic(Element::C));
374 b.add_bond(o, c, BondOrder::Single).unwrap();
375 }
376 let mol = b.build();
377 let errors = validate_valence(&mol);
378 assert!(
379 !errors.is_empty(),
380 "O with 3 bonds must be flagged as over-valenced"
381 );
382 assert_eq!(errors[0].atom, AtomIdx(0));
383 }
384
385 #[test]
386 fn test_validate_valence_ammonium_valid() {
387 let mut b = MoleculeBuilder::new();
389 let mut n_atom = Atom::organic(Element::N);
390 n_atom.charge = 1;
391 let n = b.add_atom(n_atom);
392 for _ in 0..4 {
393 let c = b.add_atom(Atom::organic(Element::C));
394 b.add_bond(n, c, BondOrder::Single).unwrap();
395 }
396 let mol = b.build();
397 assert!(
398 validate_valence(&mol).is_empty(),
399 "N+ with 4 bonds must be valid (ammonium-like)"
400 );
401 }
402
403 #[test]
404 fn test_validate_valence_transition_metal_skipped() {
405 let mut b = MoleculeBuilder::new();
407 let fe = b.add_atom(Atom::new(Element::FE));
408 for _ in 0..6 {
409 let c = b.add_atom(Atom::organic(Element::C));
410 b.add_bond(fe, c, BondOrder::Single).unwrap();
411 }
412 let mol = b.build();
413 assert!(
414 validate_valence(&mol).is_empty(),
415 "Fe with 6 bonds must be skipped"
416 );
417 }
418}