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 valence_inferred_hcount(mol, idx)
39}
40
41pub fn valence_inferred_hcount(mol: &Molecule, idx: AtomIdx) -> u8 {
54 let atom = mol.atom(idx);
55
56 if atom.wildcard {
57 return 0;
58 }
59
60 if !atom.element.is_organic_subset() {
62 return 0;
63 }
64
65 let normal_valences = atom.element.normal_valences();
66 if normal_valences.is_empty() {
67 return 0;
68 }
69
70 let charge = atom.charge as i32;
71
72 let mut aromatic_count: usize = 0;
74 let mut non_aromatic_sum: i32 = 0;
75 for (_, bidx) in mol.neighbors(idx) {
76 let order = mol.bond(bidx).order;
77 if order == BondOrder::Aromatic {
78 aromatic_count += 1;
79 } else {
80 non_aromatic_sum += order.order_int() as i32;
81 }
82 }
83
84 if aromatic_count > 0 {
85 let aromatic_contribution = (aromatic_count as f64 * 1.5).floor() as i32;
96 let effective_sum = aromatic_contribution.saturating_add(non_aromatic_sum);
97 let v = normal_valences[0] as i32 + charge;
98 if v <= 0 || effective_sum >= v {
99 return 0;
100 }
101 return (v - effective_sum) as u8;
102 }
103
104 let bond_sum = non_aromatic_sum;
106
107 let valences_to_check: &[u8] = if atom.aromatic {
113 &normal_valences[..1]
114 } else {
115 normal_valences
116 };
117
118 for &v in valences_to_check {
120 let target = v as i32 + charge;
121 if target < 0 {
122 continue;
123 }
124 if target >= bond_sum {
125 return (target - bond_sum) as u8;
126 }
127 }
128
129 0
131}
132
133#[deprecated(
134 since = "0.1.95",
135 note = "use `implicit_hcount` directly — the two functions are identical"
136)]
137pub fn total_hcount(mol: &Molecule, idx: AtomIdx) -> u8 {
139 implicit_hcount(mol, idx)
140}
141
142pub fn bond_order_sum(mol: &Molecule, idx: AtomIdx) -> u8 {
145 mol.neighbors(idx)
146 .map(|(_, bidx)| mol.bond(bidx).order.order_int())
147 .fold(0u8, |acc, x| acc.saturating_add(x))
148}
149
150pub fn is_pi_bond(order: BondOrder) -> bool {
152 matches!(
153 order,
154 BondOrder::Double | BondOrder::Triple | BondOrder::Quadruple
155 )
156}
157
158#[derive(Debug, Clone)]
167pub struct ValenceError {
168 pub atom: AtomIdx,
170 pub actual: u8,
172 pub allowed: &'static [u8],
174}
175
176impl fmt::Display for ValenceError {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 let valences_str = self
179 .allowed
180 .iter()
181 .map(|v| v.to_string())
182 .collect::<Vec<_>>()
183 .join(", ");
184 write!(
185 f,
186 "atom {} has valence {} (allowed: [{}])",
187 self.atom.0, self.actual, valences_str
188 )
189 }
190}
191
192impl std::error::Error for ValenceError {}
193
194pub fn validate_valence(mol: &Molecule) -> Vec<ValenceError> {
207 let mut errors = Vec::new();
208 for (idx, atom) in mol.atoms() {
209 if atom.wildcard {
210 continue;
211 }
212 let valences = atom.element.normal_valences();
213 if valences.is_empty() {
214 continue;
215 }
216
217 let bos = bond_order_sum(mol, idx);
218 let explicit_h = atom.hydrogen_count.unwrap_or(0);
219 let used = bos.saturating_add(explicit_h);
220 let charge = atom.charge as i16;
221
222 let has_valid = valences.iter().any(|&v| {
223 let effective = (v as i16 + charge).max(0) as u8;
224 effective >= used
225 });
226
227 if !has_valid {
228 errors.push(ValenceError {
229 atom: idx,
230 actual: used,
231 allowed: valences,
232 });
233 }
234 }
235 errors
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::atom::Atom;
242 use crate::bond::BondOrder;
243 use crate::element::Element;
244 use crate::molecule::MoleculeBuilder;
245
246 fn single_atom(elem: Element) -> Molecule {
247 let mut b = MoleculeBuilder::new();
248 b.add_atom(Atom::organic(elem));
249 b.build()
250 }
251
252 fn two_atoms(e1: Element, e2: Element, order: BondOrder) -> Molecule {
253 let mut b = MoleculeBuilder::new();
254 let a = b.add_atom(Atom::organic(e1));
255 let c = b.add_atom(Atom::organic(e2));
256 b.add_bond(a, c, order).unwrap();
257 b.build()
258 }
259
260 #[test]
261 fn test_methane() {
262 let mol = single_atom(Element::C);
264 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 4);
265 }
266
267 #[test]
268 fn test_ethane_c() {
269 let mol = two_atoms(Element::C, Element::C, BondOrder::Single);
271 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 3);
272 assert_eq!(implicit_hcount(&mol, AtomIdx(1)), 3);
273 }
274
275 #[test]
276 fn test_ethylene_c() {
277 let mol = two_atoms(Element::C, Element::C, BondOrder::Double);
279 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 2);
280 }
281
282 #[test]
283 fn test_acetylene_c() {
284 let mol = two_atoms(Element::C, Element::C, BondOrder::Triple);
286 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 1);
287 }
288
289 #[test]
290 fn test_nitrogen_amine() {
291 let mol = single_atom(Element::N);
293 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 3);
294 }
295
296 #[test]
297 fn test_nitrogen_triple() {
298 let mol = two_atoms(Element::N, Element::C, BondOrder::Triple);
300 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 0);
301 }
302
303 #[test]
304 fn test_oxygen_ether() {
305 let mol = single_atom(Element::O);
307 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 2);
308 }
309
310 #[test]
311 fn test_fluorine() {
312 let mol = single_atom(Element::F);
314 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 1);
315 }
316
317 #[test]
318 fn test_bracket_atom_explicit_h() {
319 let mut b = MoleculeBuilder::new();
321 let atom = Atom::bracket(Element::N, None, Default::default(), 4, 1, None);
322 b.add_atom(atom);
323 let mol = b.build();
324 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 4);
325 }
326
327 #[test]
328 fn test_hypervalent_sulfur() {
329 let mut b = MoleculeBuilder::new();
331 let s = b.add_atom(Atom::organic(Element::S));
332 for _ in 0..4 {
333 let c = b.add_atom(Atom::organic(Element::C));
334 b.add_bond(s, c, BondOrder::Single).unwrap();
335 }
336 let mol = b.build();
337 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 0);
338 }
339
340 #[test]
345 fn test_validate_valence_valid_molecules() {
346 let mol = single_atom(Element::C);
349 assert!(
350 validate_valence(&mol).is_empty(),
351 "isolated C must be valid"
352 );
353
354 let mol = single_atom(Element::O);
356 assert!(
357 validate_valence(&mol).is_empty(),
358 "isolated O must be valid"
359 );
360
361 let mol = two_atoms(Element::C, Element::C, BondOrder::Single);
363 assert!(validate_valence(&mol).is_empty(), "ethane must be valid");
364
365 let mol = two_atoms(Element::C, Element::O, BondOrder::Double);
367 assert!(
368 validate_valence(&mol).is_empty(),
369 "formaldehyde must be valid"
370 );
371 }
372
373 #[test]
374 fn test_validate_valence_pentavalent_carbon() {
375 let mut b = MoleculeBuilder::new();
377 let c = b.add_atom(Atom::organic(Element::C));
378 for _ in 0..5 {
379 let h = b.add_atom(Atom::new(Element::C));
380 b.add_bond(c, h, BondOrder::Single).unwrap();
381 }
382 let mol = b.build();
383 let errors = validate_valence(&mol);
384 assert_eq!(
385 errors.len(),
386 1,
387 "C with 5 bonds must produce exactly 1 error"
388 );
389 assert_eq!(errors[0].atom, AtomIdx(0));
390 assert_eq!(errors[0].actual, 5);
391 }
392
393 #[test]
394 fn test_validate_valence_trivalent_oxygen() {
395 let mut b = MoleculeBuilder::new();
397 let o = b.add_atom(Atom::organic(Element::O));
398 for _ in 0..3 {
399 let c = b.add_atom(Atom::organic(Element::C));
400 b.add_bond(o, c, BondOrder::Single).unwrap();
401 }
402 let mol = b.build();
403 let errors = validate_valence(&mol);
404 assert!(
405 !errors.is_empty(),
406 "O with 3 bonds must be flagged as over-valenced"
407 );
408 assert_eq!(errors[0].atom, AtomIdx(0));
409 }
410
411 #[test]
412 fn test_validate_valence_ammonium_valid() {
413 let mut b = MoleculeBuilder::new();
415 let mut n_atom = Atom::organic(Element::N);
416 n_atom.charge = 1;
417 let n = b.add_atom(n_atom);
418 for _ in 0..4 {
419 let c = b.add_atom(Atom::organic(Element::C));
420 b.add_bond(n, c, BondOrder::Single).unwrap();
421 }
422 let mol = b.build();
423 assert!(
424 validate_valence(&mol).is_empty(),
425 "N+ with 4 bonds must be valid (ammonium-like)"
426 );
427 }
428
429 #[test]
437 fn test_te_implicit_hcount_no_explicit_h_stays_zero() {
438 let mol = single_atom(Element::TE);
442 assert_eq!(implicit_hcount(&mol, AtomIdx(0)), 0);
443 }
444
445 #[test]
446 fn test_validate_valence_te_divalent_neutral() {
447 let mut b = MoleculeBuilder::new();
450 let te = b.add_atom(Atom::organic(Element::TE));
451 for _ in 0..2 {
452 let c = b.add_atom(Atom::organic(Element::C));
453 b.add_bond(te, c, BondOrder::Single).unwrap();
454 }
455 let mol = b.build();
456 assert!(
457 validate_valence(&mol).is_empty(),
458 "divalent Te must be valid (RDKit sanitizes C[Te]C OK)"
459 );
460 }
461
462 #[test]
463 fn test_validate_valence_te_tetravalent_neutral() {
464 let mut b = MoleculeBuilder::new();
466 let te = b.add_atom(Atom::organic(Element::TE));
467 for _ in 0..4 {
468 let c = b.add_atom(Atom::organic(Element::C));
469 b.add_bond(te, c, BondOrder::Single).unwrap();
470 }
471 let mol = b.build();
472 assert!(
473 validate_valence(&mol).is_empty(),
474 "tetravalent Te must be valid (RDKit sanitizes TeCl4-analog OK)"
475 );
476 }
477
478 #[test]
479 fn test_validate_valence_te_hexavalent_neutral() {
480 let mut b = MoleculeBuilder::new();
482 let te = b.add_atom(Atom::organic(Element::TE));
483 for _ in 0..6 {
484 let c = b.add_atom(Atom::organic(Element::C));
485 b.add_bond(te, c, BondOrder::Single).unwrap();
486 }
487 let mol = b.build();
488 assert!(
489 validate_valence(&mol).is_empty(),
490 "hexavalent Te must be valid (RDKit sanitizes TeF6-analog OK)"
491 );
492 }
493
494 #[test]
495 fn test_validate_valence_te_overvalent_invalid() {
496 let mut b = MoleculeBuilder::new();
499 let te = b.add_atom(Atom::organic(Element::TE));
500 for _ in 0..8 {
501 let c = b.add_atom(Atom::organic(Element::C));
502 b.add_bond(te, c, BondOrder::Single).unwrap();
503 }
504 let mol = b.build();
505 let errors = validate_valence(&mol);
506 assert_eq!(
507 errors.len(),
508 1,
509 "8-bonded Te must be flagged invalid, matching RDKit's AtomValenceException"
510 );
511 assert_eq!(errors[0].actual, 8);
512 assert_eq!(errors[0].allowed, &[2, 4, 6]);
513 }
514
515 #[test]
516 fn test_validate_valence_te_cation_telluronium() {
517 let mut b = MoleculeBuilder::new();
520 let mut te_atom = Atom::organic(Element::TE);
521 te_atom.charge = 1;
522 let te = b.add_atom(te_atom);
523 for _ in 0..3 {
524 let c = b.add_atom(Atom::organic(Element::C));
525 b.add_bond(te, c, BondOrder::Single).unwrap();
526 }
527 let mol = b.build();
528 assert!(
529 validate_valence(&mol).is_empty(),
530 "Te+ telluronium (3 bonds) must be valid, matching RDKit"
531 );
532 }
533
534 #[test]
535 fn test_validate_valence_te_anion_telluride() {
536 let mut b = MoleculeBuilder::new();
539 let te_atom = Atom::bracket(Element::TE, None, Default::default(), 0, -2, None);
540 b.add_atom(te_atom);
541 let mol = b.build();
542 assert!(
543 validate_valence(&mol).is_empty(),
544 "[Te-2] must be valid, matching RDKit"
545 );
546 }
547
548 #[test]
549 fn test_validate_valence_te_anion_hydrotelluride() {
550 let mut b = MoleculeBuilder::new();
552 let te_atom = Atom::bracket(Element::TE, None, Default::default(), 1, -1, None);
553 b.add_atom(te_atom);
554 let mol = b.build();
555 assert!(
556 validate_valence(&mol).is_empty(),
557 "[TeH-] must be valid, matching RDKit"
558 );
559 }
560
561 #[test]
562 fn test_te_bracket_h2_implicit_and_valid() {
563 let mut b = MoleculeBuilder::new();
566 let te_atom = Atom::bracket(Element::TE, None, Default::default(), 2, 0, None);
567 let te = b.add_atom(te_atom);
568 let mol = b.build();
569 assert_eq!(implicit_hcount(&mol, te), 2);
570 assert!(
571 validate_valence(&mol).is_empty(),
572 "[TeH2] must be valid, matching RDKit"
573 );
574 }
575
576 #[test]
577 fn test_validate_valence_transition_metal_skipped() {
578 let mut b = MoleculeBuilder::new();
580 let fe = b.add_atom(Atom::new(Element::FE));
581 for _ in 0..6 {
582 let c = b.add_atom(Atom::organic(Element::C));
583 b.add_bond(fe, c, BondOrder::Single).unwrap();
584 }
585 let mol = b.build();
586 assert!(
587 validate_valence(&mol).is_empty(),
588 "Fe with 6 bonds must be skipped"
589 );
590 }
591}