polysim_core/properties/molecular_weight.rs
1use opensmiles::{parse as parse_smiles, AtomSymbol};
2
3use crate::polymer::PolymerChain;
4
5/// Masse standard de l'hydrogène (IUPAC 2021), en g/mol.
6const H_AVERAGE_MASS: f64 = 1.008;
7
8/// Masse du proton (¹H), en g/mol.
9const H_MONO_MASS: f64 = 1.00782503207;
10
11/// Calcule la masse moléculaire moyenne (poids atomiques IUPAC) de la chaîne, en g/mol.
12///
13/// Chaque atome lourd contribue par sa masse standard (moyenne isotopique), et les
14/// hydrogènes implicites/explicites sont ajoutés avec la masse standard de l'hydrogène.
15///
16/// # Exemple
17///
18/// ```rust
19/// use polysim_core::{parse, builder::{linear::LinearBuilder, BuildStrategy},
20/// properties::molecular_weight::average_mass};
21///
22/// let bs = parse("{[]CC[]}").unwrap();
23/// let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(1))
24/// .homopolymer()
25/// .unwrap();
26/// // CC = éthane C₂H₆ ≈ 30.07 g/mol
27/// let mw = average_mass(&chain);
28/// assert!((mw - 30.070).abs() < 0.01, "got {mw}");
29/// ```
30pub fn average_mass(chain: &PolymerChain) -> f64 {
31 let mol = parse_smiles(&chain.smiles).expect("chain SMILES must be valid SMILES");
32 mol.nodes().iter().fold(0.0, |acc, node| {
33 // atom.mass() renvoie la masse standard (ou la masse isotopique si explicite [¹³C])
34 acc + node.atom().mass() + node.hydrogens() as f64 * H_AVERAGE_MASS
35 })
36}
37
38/// Calcule la masse monoisotopique de la chaîne (nucléide le plus abondant), en g/mol.
39///
40/// Pour les atomes sans isotope explicite, utilise le nucléide le plus abondant de chaque
41/// élément (ex. ¹²C = 12.000, ¹⁶O = 15.9949…). Pour les atomes avec isotope explicite
42/// (`[13C]`), respecte l'isotope spécifié.
43///
44/// # Exemple
45///
46/// ```rust
47/// use polysim_core::{parse, builder::{linear::LinearBuilder, BuildStrategy},
48/// properties::molecular_weight::monoisotopic_mass};
49///
50/// let bs = parse("{[]CC[]}").unwrap();
51/// let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(1))
52/// .homopolymer()
53/// .unwrap();
54/// // CC = éthane C₂H₆, masse monoisotopique ≈ 30.047 g/mol
55/// let m = monoisotopic_mass(&chain);
56/// assert!((m - 30.047).abs() < 0.01, "got {m}");
57/// ```
58pub fn monoisotopic_mass(chain: &PolymerChain) -> f64 {
59 let mol = parse_smiles(&chain.smiles).expect("chain SMILES must be valid SMILES");
60 mol.nodes().iter().fold(0.0, |acc, node| {
61 let atom = node.atom();
62 let heavy_mass = if atom.isotope().is_some() {
63 // Isotope explicitement spécifié → respecter (ex. [13C])
64 atom.mass()
65 } else {
66 most_abundant_isotope_mass(atom.element())
67 };
68 acc + heavy_mass + node.hydrogens() as f64 * H_MONO_MASS
69 })
70}
71
72/// Retourne la masse du nucléide le plus abondant pour chaque élément.
73///
74/// Pour les éléments organiques courants en chimie des polymères, les valeurs exactes
75/// sont codées en dur. Pour les éléments rares, la masse standard IUPAC est utilisée
76/// comme approximation.
77fn most_abundant_isotope_mass(element: &AtomSymbol) -> f64 {
78 match element.atomic_number() {
79 0 => 0.0, // Wildcard (*)
80 1 => H_MONO_MASS, // ¹H (99.985 %)
81 5 => 11.0093054, // ¹¹B (80.1 %)
82 6 => 12.0, // ¹²C (98.89 %)
83 7 => 14.0030740048, // ¹⁴N (99.63 %)
84 8 => 15.9949146221, // ¹⁶O (99.76 %)
85 9 => 18.9984032, // ¹⁹F (100 %)
86 14 => 27.9769265325, // ²⁸Si (92.23 %)
87 15 => 30.97376163, // ³¹P (100 %)
88 16 => 31.97207100, // ³²S (95.02 %)
89 17 => 34.96885268, // ³⁵Cl (75.77 %)
90 35 => 78.9183371, // ⁷⁹Br (50.69 %)
91 53 => 126.904468, // ¹²⁷I (100 %)
92 _ => element.standard_mass(), // fallback : masse IUPAC pour éléments rares
93 }
94}