rustms/chemistry/
sum_formula.rs

1use std::collections::HashMap;
2use crate::algorithm::isotope::generate_isotope_distribution;
3use crate::chemistry::constants::MASS_PROTON;
4use crate::chemistry::element::atomic_weights_mono_isotopic;
5use crate::ms::spectrum::MzSpectrum;
6
7pub struct SumFormula {
8    pub formula: String,
9    pub elements: HashMap<String, i32>,
10}
11
12impl SumFormula {
13    pub fn new(formula: &str) -> Self {
14        let elements = parse_formula(formula).unwrap();
15        SumFormula {
16            formula: formula.to_string(),
17            elements,
18        }
19    }
20    /// Calculate the monoisotopic weight of the chemical formula.
21    ///
22    /// Arguments:
23    ///
24    /// None
25    ///
26    /// Returns:
27    ///
28    /// * `f64` - The monoisotopic weight of the chemical formula.
29    ///
30    /// # Example
31    ///
32    /// ```
33    /// use rustms::chemistry::sum_formula::SumFormula;
34    ///
35    /// let formula = "H2O";
36    /// let sum_formula = SumFormula::new(formula);
37    /// assert_eq!(sum_formula.monoisotopic_weight(), 18.01056468403);
38    /// ```
39    pub fn monoisotopic_weight(&self) -> f64 {
40        let atomic_weights = atomic_weights_mono_isotopic();
41        self.elements.iter().fold(0.0, |acc, (element, count)| {
42            acc + atomic_weights[element.as_str()] * *count as f64
43        })
44    }
45
46    /// Generate the isotope distribution of the chemical formula.
47    ///
48    /// Arguments:
49    ///
50    /// * `charge` - The charge state of the ion.
51    ///
52    /// Returns:
53    ///
54    /// * `MzSpectrum` - The isotope distribution of the chemical formula.
55    ///
56    /// # Example
57    ///
58    /// ```
59    /// use rustms::chemistry::sum_formula::SumFormula;
60    /// use rustms::ms::spectrum::MzSpectrum;
61    ///
62    /// let formula = "C6H12O6";
63    /// let sum_formula = SumFormula::new(formula);
64    /// let isotope_distribution = sum_formula.isotope_distribution(1);
65    /// let mut first_mz = *isotope_distribution.mz.first().unwrap();
66    /// // round to first 5 decimal places
67    /// first_mz = (first_mz * 1e5).round() / 1e5;
68    /// assert_eq!(first_mz, 181.07066);
69    /// ```
70    pub fn isotope_distribution(&self, charge: i32) -> MzSpectrum {
71        let distribution = generate_isotope_distribution(&self.elements, 1e-3, 1e-9, 200);
72        let intensity = distribution.iter().map(|(_, i)| *i).collect();
73        let mz = distribution.iter().map(|(m, _)| (*m + charge as f64 * MASS_PROTON) / charge as f64).collect();
74        MzSpectrum::new(mz, intensity)
75    }
76}
77
78/// Parse a chemical formula into a map of elements and their counts.
79///
80/// Arguments:
81///
82/// * `formula` - The chemical formula to parse.
83///
84/// Returns:
85///
86/// * `Result<HashMap<String, i32>, String>` - A map of elements and their counts.
87///
88/// # Example
89///
90/// ```
91/// use rustms::chemistry::sum_formula::parse_formula;
92///
93/// let formula = "H2O";
94/// let elements = parse_formula(formula).unwrap();
95/// assert_eq!(elements.get("H"), Some(&2));
96/// assert_eq!(elements.get("O"), Some(&1));
97/// ```
98pub fn parse_formula(formula: &str) -> Result<HashMap<String, i32>, String> {
99    let atomic_weights = atomic_weights_mono_isotopic();
100    let mut element_counts = HashMap::new();
101    let mut current_element = String::new();
102    let mut current_count = String::new();
103    let mut chars = formula.chars().peekable();
104
105    while let Some(c) = chars.next() {
106        if c.is_ascii_uppercase() {
107            if !current_element.is_empty() {
108                let count = current_count.parse::<i32>().unwrap_or(1);
109                if atomic_weights.contains_key(current_element.as_str()) {
110                    *element_counts.entry(current_element.clone()).or_insert(0) += count;
111                } else {
112                    return Err(format!("Unknown element: {}", current_element));
113                }
114            }
115            current_element = c.to_string();
116            current_count = String::new();
117        } else if c.is_ascii_digit() {
118            current_count.push(c);
119        } else if c.is_ascii_lowercase() {
120            current_element.push(c);
121        }
122
123        if chars.peek().map_or(true, |next_c| next_c.is_ascii_uppercase()) {
124            let count = current_count.parse::<i32>().unwrap_or(1);
125            if atomic_weights.contains_key(current_element.as_str()) {
126                *element_counts.entry(current_element.clone()).or_insert(0) += count;
127            } else {
128                return Err(format!("Unknown element: {}", current_element));
129            }
130            current_element = String::new();
131            current_count = String::new();
132        }
133    }
134
135    Ok(element_counts)
136}