polysim_core/properties/formula.rs
1use std::collections::BTreeMap;
2
3use opensmiles::parse as parse_smiles;
4
5use crate::polymer::PolymerChain;
6
7/// Calcule la formule moléculaire brute d'une chaîne en notation Hill.
8///
9/// La notation Hill place **C** en premier, puis **H**, puis les autres éléments
10/// par ordre alphabétique du symbole. Les hydrogènes implicites sont inclus.
11///
12/// # Exemple
13///
14/// ```rust
15/// use polysim_core::{parse, builder::{linear::LinearBuilder, BuildStrategy},
16/// properties::formula::molecular_formula};
17///
18/// let bs = parse("{[]CC[]}").unwrap();
19/// let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(10))
20/// .homopolymer()
21/// .unwrap();
22/// // Polyéthylène n=10 → C₂₀H₄₂
23/// assert_eq!(molecular_formula(&chain), "C20H42");
24/// ```
25pub fn molecular_formula(chain: &PolymerChain) -> String {
26 let mol = parse_smiles(&chain.smiles).expect("chain SMILES must be valid SMILES");
27 let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new();
28
29 for node in mol.nodes() {
30 let atomic_num = node.atom().element().atomic_number();
31 if atomic_num == 0 {
32 continue; // wildcard (*)
33 }
34 if let Some(sym) = element_symbol(atomic_num) {
35 *counts.entry(sym).or_insert(0) += 1;
36 }
37 let h = node.hydrogens() as usize;
38 if h > 0 {
39 *counts.entry("H").or_insert(0) += h;
40 }
41 }
42
43 hill_notation(&counts)
44}
45
46/// Nombre total d'atomes dans la chaîne (atomes lourds + hydrogènes implicites/explicites).
47///
48/// # Exemple
49///
50/// ```rust
51/// use polysim_core::{parse, builder::{linear::LinearBuilder, BuildStrategy},
52/// properties::formula::total_atom_count};
53///
54/// let bs = parse("{[]CC[]}").unwrap();
55/// let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(1))
56/// .homopolymer()
57/// .unwrap();
58/// // CC = éthane C₂H₆ → 2 + 6 = 8 atomes
59/// assert_eq!(total_atom_count(&chain), 8);
60/// ```
61pub fn total_atom_count(chain: &PolymerChain) -> usize {
62 let mol = parse_smiles(&chain.smiles).expect("chain SMILES must be valid SMILES");
63 mol.nodes()
64 .iter()
65 .map(|node| 1 + node.hydrogens() as usize)
66 .sum()
67}
68
69/// Formate les counts en notation Hill : C en premier, H en second,
70/// puis les autres éléments par ordre alphabétique de symbole.
71fn hill_notation(counts: &BTreeMap<&'static str, usize>) -> String {
72 let mut result = String::new();
73 let has_carbon = counts.contains_key("C");
74
75 if has_carbon {
76 // C et H en premier
77 for sym in ["C", "H"] {
78 if let Some(&n) = counts.get(sym) {
79 result.push_str(sym);
80 if n > 1 {
81 result.push_str(&n.to_string());
82 }
83 }
84 }
85 // Reste par ordre alphabétique (BTreeMap est déjà trié)
86 for (&sym, &n) in counts {
87 if sym == "C" || sym == "H" {
88 continue;
89 }
90 result.push_str(sym);
91 if n > 1 {
92 result.push_str(&n.to_string());
93 }
94 }
95 } else {
96 // Pas de carbone → tout par ordre alphabétique
97 for (&sym, &n) in counts {
98 result.push_str(sym);
99 if n > 1 {
100 result.push_str(&n.to_string());
101 }
102 }
103 }
104 result
105}
106
107/// Retourne le symbole IUPAC de l'élément pour le numéro atomique donné.
108///
109/// Couvre les éléments courants en chimie des polymères.
110/// Retourne `None` pour les éléments inconnus ou rares.
111fn element_symbol(atomic_number: u8) -> Option<&'static str> {
112 match atomic_number {
113 1 => Some("H"),
114 5 => Some("B"),
115 6 => Some("C"),
116 7 => Some("N"),
117 8 => Some("O"),
118 9 => Some("F"),
119 14 => Some("Si"),
120 15 => Some("P"),
121 16 => Some("S"),
122 17 => Some("Cl"),
123 35 => Some("Br"),
124 53 => Some("I"),
125 _ => None,
126 }
127}