Skip to main content

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}