corems.molecular_formula.factory.MolecularFormulaFactory

  1import re
  2
  3from corems.encapsulation.constant import Atoms, Labels
  4from corems.molecular_formula.calc.MolecularFormulaCalc import MolecularFormulaCalc
  5
  6__author__ = "Yuri E. Corilo"
  7__date__ = "Jun 24, 2019"
  8
  9
 10class MolecularFormulaBase(MolecularFormulaCalc):
 11    """Base class for representing a molecular formula.
 12
 13    Parameters
 14    ----------
 15    molecular_formula : dict, list, str
 16        The molecular formula.
 17    ion_charge : int
 18        The ion charge.
 19    ion_type : str, optional
 20        The ion type. Defaults to None.
 21    adduct_atom : str, optional
 22        The adduct atom. Defaults to None.
 23    mspeak_parent : _MSPeak, optional
 24        The parent mass spectrum peak object instance. Defaults to None.
 25    external_mz : float, optional
 26        The external m/z value. Defaults to None.
 27
 28    Raises
 29    ------
 30    TypeError
 31        If the ion type is not 'DE_OR_PROTONATED', 'RADICAL' or  'ADDUCT'.
 32
 33    Attributes
 34    ----------
 35    isotopologue_count_percentile : float
 36        The isotopologue count percentile.
 37    O_C : float
 38        The O/C ratio.
 39    H_C : float
 40        The H/C ratio.
 41    dbe : float
 42        The double bond equivalent.
 43    mz_nominal_calc : int
 44        The nominal m/z value.
 45    mz_error : float
 46        The m/z error.
 47    mz_calc : float
 48        The m/z value.
 49    protonated_mz : float
 50        The protonated or deprotonated m/z value.
 51    radical_mz : float
 52        The radical m/z value.
 53    neutral_mass : float
 54        The neutral mass.
 55    ion_type : str
 56        The ion type.
 57    ion_charge : int
 58        The ion charge.
 59    atoms : list
 60        The atoms in the molecular formula.
 61    confidence_score : float
 62        Composite assignment confidence score in about [0, 1] (higher is
 63        better). Weighted sum of ``average_mz_error_score`` and
 64        ``isotopologue_similarity`` using
 65        ``mz_error_score_weight`` / ``isotopologue_score_weight`` from the
 66        parent spectrum's molecular search settings. See the
 67        ``corems.molecular_formula`` package documentation for the full
 68        formulation and literature reference.
 69    isotopologue_similarity : float
 70        Similarity between expected and observed isotopologue abundance
 71        patterns (about [0, 1]). Zero when no isotopologues are expected or
 72        none were computed during search.
 73    average_mz_error_score : float
 74        Mean Gaussian mass-accuracy score over the monoisotopic peak and
 75        expected isotopologue peaks. Missing expected isotopologues
 76        contribute 0.
 77    mz_error_score : float
 78        Gaussian mass-accuracy score for this formula on its parent peak
 79        only (ppm error vs ``predicted_std`` or a 1.66 ppm fallback).
 80    kmd : float
 81        The Kendrick mass defect (KMD).
 82    kendrick_mass : float
 83        The Kendrick mass.
 84    knm : float
 85        The nominal Kendrick mass.
 86    string : str
 87        The molecular formula string.
 88    string_formated : str
 89        The molecular formula string formated with subscripts and superscripts.
 90    class_label : str
 91        The class label.
 92    class_dict : dict
 93        The class dictionary.
 94
 95    Methods
 96    -------
 97    * change_kendrick_base(kendrick_dict_base).
 98        Change the Kendrick base.
 99    * isotopologues(min_abundance, current_mono_abundance, dynamic_range).
100        Calculate the isotopologues.
101    * atoms_qnt(atom).
102        Get the atom quantity.
103    * atoms_symbol(atom).
104        Get the atom symbol without the mass number.
105    * to_dict().
106        Get the molecular formula as a dictionary.
107    * to_list().
108        Get the molecular formula as a list.
109    """
110
111    def __init__(
112        self,
113        molecular_formula,
114        ion_charge,
115        ion_type=None,
116        adduct_atom=None,
117        mspeak_parent=None,
118        external_mz=None,
119    ):
120        # clear dictionary of atoms with 0 value
121        if type(molecular_formula) is dict:
122            self._from_dict(molecular_formula, ion_type, adduct_atom)
123
124        elif type(molecular_formula) is list:
125            self._from_list(molecular_formula, ion_type, adduct_atom)
126
127        elif type(molecular_formula) is str:
128            self._from_str(molecular_formula, ion_type, adduct_atom)
129
130        self._ion_charge = ion_charge
131        self._external_mz = external_mz
132        self._confidence_score = None
133        self._isotopologue_similarity = None
134        self._mz_error_score = None
135        self._mass_error_average_score = None
136
137        self.is_isotopologue = False
138
139        # parent mass spectrum peak obj instance
140        self._mspeak_parent = mspeak_parent
141
142        self.expected_isotopologues = []
143        self.mspeak_mf_isotopologues_indexes = []
144
145        if self._mspeak_parent:
146            kendrick_dict_base = (
147                self._mspeak_parent._ms_parent.mspeaks_settings.kendrick_base
148            )
149        else:
150            kendrick_dict_base = {"C": 1, "H": 2}
151        self._kmd, self._kendrick_mass, self._nominal_km = self._calc_kmd(
152            kendrick_dict_base
153        )
154
155    def __repr__(self):
156        return "MolecularFormula({0},{1},ion type = {2}".format(
157            self._d_molecular_formula, self.ion_charge, self.ion_type
158        )
159
160    def __str__(self):
161        return "MolecularFormula {0}, ion_charge:{1}, ion type:{2}, m/z:{3} ".format(
162            self.string, self.ion_charge, self.ion_type, self.mz_calc
163        )
164
165    def __len__(self):
166        # crash if keys are not ordered
167        return len(self._d_molecular_formula.keys())
168
169    def __getitem__(self, atom):
170        # atom = list(self._d_molecular_formula.keys())[position]
171        if atom in self._d_molecular_formula.keys():
172            return self._d_molecular_formula[atom]
173        else:
174            return 0
175
176    def get(self, atom):
177        """Get the atom quantity of a specific atom.
178
179        Parameters
180        ----------
181        atom : str
182            The atom symbol.
183
184        Returns
185        -------
186        int
187            The atom quantity.
188        """
189        # atom = list(self._d_molecular_formula.keys())[position]
190        if atom in self._d_molecular_formula.keys():
191            return self._d_molecular_formula[atom]
192        else:
193            return 0
194
195    def _from_dict(self, molecular_formula, ion_type, adduct_atom):
196        self._d_molecular_formula = {
197            key: val for key, val in molecular_formula.items() if val != 0
198        }
199
200        if ion_type is not None:
201            self._d_molecular_formula[Labels.ion_type] = ion_type
202
203        if adduct_atom:
204            if adduct_atom in self._d_molecular_formula:
205                self._d_molecular_formula[adduct_atom] += 1
206            else:
207                self._d_molecular_formula[adduct_atom] = 1
208        self.adduct_atom = adduct_atom
209
210    def _from_list(self, molecular_formula_list, ion_type, adduct_atom):
211        # list has to be in the format
212        # ['C', 10, 'H', 21, '13C', 1, 'Cl', 1, etc]
213        self._d_molecular_formula = {}
214        for each in range(0, len(molecular_formula_list), 2):
215            atoms_label = molecular_formula_list[each]
216            atoms_count = int(molecular_formula_list[each + 1])
217
218            if atoms_count > 0:
219                self._d_molecular_formula[atoms_label] = int(atoms_count)
220
221        self._d_molecular_formula[Labels.ion_type] = ion_type
222        if adduct_atom:
223            self.adduct_atom = adduct_atom
224            if adduct_atom in self._d_molecular_formula:
225                self._d_molecular_formula[adduct_atom] += 1
226            else:
227                self._d_molecular_formula[adduct_atom] = 1
228        else:
229            self.adduct_atom = None
230
231    def _from_str(self, molecular_formula_str, ion_type, adduct_atom):
232        # string has to be in the format
233        #'C10 H21 13C1 Cl1 37Cl1 etc'
234        # Check if there are spaces in the string
235        if " " not in molecular_formula_str:
236            raise ValueError(
237                "The molecular formula string should have spaces, input: %s"
238                % molecular_formula_str
239            )
240
241        # Split the string by spaces
242        # Grab the text before a digit for each element after splitting on spaces (atoms)
243        elements = [re.sub(r"\d+$", "", x) for x in molecular_formula_str.split()]
244        # Grab the digits at the end of each element after splitting on spaces (counts)
245        counts = [re.findall(r"\d+$", x)[0] for x in molecular_formula_str.split()]
246        # Check that the number of elements and counts are the same
247        if len(elements) != len(counts):
248            raise ValueError(
249                "The number of elements and counts do not match, input: %s"
250                % molecular_formula_str
251            )
252
253        # Create a dictionary from the elements and counts and add it to the molecular formula
254        dict_ = dict(zip(elements, counts))
255        # Cast counts to integers
256        dict_ = {key: int(val) for key, val in dict_.items()}
257        self._from_dict(dict_, ion_type, adduct_atom)
258
259    def split(self, delimiters, string, maxsplit=0):  # pragma: no cover
260        """Splits the molecular formula string.
261
262        Parameters
263        ----------
264        delimiters : list
265            The list of delimiters.
266        string : str
267            The molecular formula string.
268        maxsplit : int, optional
269            The maximum number of splits. Defaults to 0.
270
271        Returns
272        -------
273        list
274            The molecular formula list.
275
276        Notes
277        -----
278        Does not work when formula has atoms with same characters in a row that below to different atoms, i.e. C10H21NNa.
279        """
280        regexPattern = "|".join(map(re.escape, delimiters))  # pragma: no cover
281        isotopes = re.findall(regexPattern, string)  # pragma: no cover
282        counts = re.split(regexPattern, string, maxsplit)  # pragma: no cover
283
284        return [isotopes[0], int(counts[1])]
285
286    @property
287    def isotopologue_count_percentile(
288        self,
289    ):
290        if not len(self.expected_isotopologues) == 0:
291            return (
292                len(self.mspeak_mf_isotopologues_indexes)
293                / len(self.expected_isotopologues)
294            ) * 100
295        else:
296            return 100
297
298    @property
299    def O_C(self):
300        if "O" in self._d_molecular_formula.keys():
301            # gather all the Os and Hs, regardless of the isotopic composition
302            Os = sum(
303                [
304                    self._d_molecular_formula.get(key)
305                    for key in ["O"] + Atoms.isotopes["O"][1]
306                    if key in self._d_molecular_formula.keys()
307                ]
308            )
309            Cs = sum(
310                [
311                    self._d_molecular_formula.get(key)
312                    for key in ["C"] + Atoms.isotopes["C"][1]
313                    if key in self._d_molecular_formula.keys()
314                ]
315            )
316            return Os / Cs
317        else:
318            return 0
319
320    @property
321    def H_C(self):
322        # gather all the Cs and Hs, regardless of the isotopic composition
323        Cs = sum(
324            [
325                self._d_molecular_formula.get(key)
326                for key in ["C"] + Atoms.isotopes["C"][1]
327                if key in self._d_molecular_formula.keys()
328            ]
329        )
330        Hs = sum(
331            [
332                self._d_molecular_formula.get(key)
333                for key in ["H"] + Atoms.isotopes["H"][1]
334                if key in self._d_molecular_formula.keys()
335            ]
336        )
337        return Hs / Cs
338
339    @property
340    def A_I(self):
341        """Aromaticity index"""
342        return self._calc_aromaticity_index()
343
344    @property
345    def A_I_mod(self):
346        """Modified aromaticity index"""
347        return self._calc_aromaticity_index_mod()
348
349    @property
350    def nosc(self):
351        """Nominal oxidation state of carbon"""
352        return self._calc_nosc()
353
354    @property
355    def dbe(self):
356        return self._calc_dbe()
357
358    @property
359    def mz_nominal_calc(self):
360        return int(self._calc_mz())
361
362    @property
363    def mz_error(self):
364        return self._calc_assignment_mass_error()
365
366    @property
367    def mz_calc(self):
368        return self._calc_mz()
369
370    @property
371    def protonated_mz(self):
372        return self._protonated_mz(self.ion_charge)
373
374    @property
375    def radical_mz(self):
376        return self._radical_mz(self.ion_charge)
377
378    @property
379    def neutral_mass(self):
380        return self._neutral_mass()
381
382    def adduct_mz(self, adduct_atom):
383        """Get m/z of an adducted ion version of the molecular formula.
384
385        Parameters
386        ----------
387        adduct_atom : str
388            The adduct atom.
389
390        Returns
391        -------
392        float
393            The m/z value of the adducted ion version of the molecular formula.
394        """
395        return self._adduct_mz(adduct_atom, self.ion_charge)
396
397    @property
398    def ion_type(self):
399        ion_type = self._d_molecular_formula.get(Labels.ion_type)
400        if ion_type == Labels.protonated_de_ion:
401            if self.ion_charge > 0:
402                return Labels.protonated
403            else:
404                return Labels.de_protonated
405        else:
406            return ion_type
407
408    @ion_type.setter
409    def ion_type(self, ion_type):
410        if ion_type in [
411            Labels.protonated_de_ion,
412            Labels.adduct_ion,
413            Labels.radical_ion,
414        ]:
415            self._d_molecular_formula[Labels.ion_type] = ion_type
416        else:
417            raise TypeError(
418                "Ion type can only be: 'DE_OR_PROTONATED', 'RADICAL' or  'ADDUCT', not %s"
419                % ion_type
420            )
421
422    @property
423    def ion_charge(self):
424        return self._ion_charge
425
426    @property
427    def atoms(self):
428        """Get the atoms in the molecular formula."""
429        # if there is an adduct_atom, them reduce it from the atoms list
430        if self.adduct_atom is None:
431            return [
432                key
433                for key in self._d_molecular_formula.keys()
434                if key != Labels.ion_type
435            ]
436        else:
437            temp_dict = self._d_molecular_formula.copy()
438            temp_dict[self.adduct_atom] -= 1
439            return [
440                key
441                for key, val in temp_dict.items()
442                if key != Labels.ion_type and val > 0
443            ]
444
445    @property
446    def confidence_score(self):
447        """Composite molecular formula assignment confidence score.
448
449        Combines formula-level mass accuracy and isotopologue pattern
450        agreement:
451
452        ```
453        CS = (w_err * m_err) + (w_iso * m_iso)
454        ```
455
456        where ``m_err`` is :attr:`average_mz_error_score`, ``m_iso`` is
457        :attr:`isotopologue_similarity`, and the weights are
458        ``molecular_search_settings.mz_error_score_weight`` (default 0.6)
459        and ``isotopologue_score_weight`` (default 0.4).
460
461        Returns
462        -------
463        float
464            Score typically in about [0, 1]; higher values indicate better
465            agreement with mass error and (when available) isotopologue
466            evidence. Used for ranking when ``score_method`` is
467            ``\"prob_score\"``.
468
469        Notes
470        -----
471        Requires an associated parent mass spectral peak. Isotopologue
472        terms are only informative when search was run with isotopologue
473        detection enabled. See the ``corems.molecular_formula`` package
474        documentation and Dewey et al., *Anal. Chem.* 2025, 97, 13031–13039
475        (https://doi.org/10.1021/acs.analchem.4c06826).
476        """
477        if not self._confidence_score:
478            self._confidence_score = self._calc_confidence_score()
479
480        return self._confidence_score
481
482    @property
483    def isotopologue_similarity(self):
484        """Isotopologue abundance pattern similarity for this formula.
485
486        Compares expected natural-abundance isotopologue intensities
487        (from dynamic-range-limited expansion) to abundances of peaks
488        assigned as those partners. Implemented as a normalized Manhattan
489        distance mapped to a similarity in about [0, 1]
490        (higher is better).
491
492        Returns
493        -------
494        float
495            Similarity score. Returns 0.0 when no isotopologues are
496            expected for this monoisotopic formula (or none were computed).
497
498        Notes
499        -----
500        This is the ``m_iso`` term in :attr:`confidence_score`. See the
501        ``corems.molecular_formula`` package documentation for details.
502        """
503        if not self._isotopologue_similarity:
504            self._isotopologue_similarity = self._calc_isotopologue_confidence()
505
506        return self._isotopologue_similarity
507
508    @property
509    def average_mz_error_score(self):
510        """Mean mass-accuracy score over mono and expected isotopologues.
511
512        Averages :attr:`mz_error_score` for the monoisotopic assignment and
513        for each expected isotopologue formula that has a parent peak.
514        Expected isotopologues without a matched peak contribute 0.
515
516        Returns
517        -------
518        float
519            Mean Gaussian mass-error score in about [0, 1].
520
521        Notes
522        -----
523        This is the ``m_err`` term in :attr:`confidence_score`.
524        """
525        # includes the isotopologues
526
527        if not self._mass_error_average_score:
528            self._mass_error_average_score = self._calc_average_mz_score()
529
530        return self._mass_error_average_score
531
532    @property
533    def mz_error_score(self):
534        """Gaussian mass-accuracy score for this formula on its parent peak.
535
536        Uses the assignment mass error ``delta`` (ppm) and a width ``sigma``
537        equal to the parent peak's ``predicted_std`` when set, otherwise a
538        fallback of 1.66 ppm:
539
540        ```
541        m = exp( -((delta - mu) ** 2) / (2 * (sigma ** 2)) )
542        ```
543
544        with mean ``mu = 0`` (calibrated spectrum with near-zero mean error
545        assumed).
546
547        Returns
548        -------
549        float
550            Score typically in about 0 to 1; exact mass match is 1,
551            and larger ppm error yields values closer to 0.
552
553        Notes
554        -----
555        Formula-level ranking uses :attr:`average_mz_error_score`, which
556        folds in expected isotopologue peaks as well.
557        """
558        if not self._mz_error_score:
559            self._mz_error_score = self._calc_mz_confidence()
560
561        return self._mz_error_score
562
563    @property
564    def kmd(self):
565        return self._kmd
566
567    @property
568    def kendrick_mass(self):
569        return self._kendrick_mass
570
571    @property
572    def knm(self):
573        return self._nominal_km
574
575    def change_kendrick_base(self, kendrick_dict_base):
576        """Change the Kendrick base.
577
578        Parameters
579        ----------
580        kendrick_dict_base : dict
581            The Kendrick base dictionary. Ex: {"C": 1, "H": 2}
582        """
583        self._kmd, self._kendrick_mass, self._nominal_km = self._calc_kmd(
584            kendrick_dict_base
585        )
586
587    def isotopologues(self, min_abundance, current_mono_abundance, dynamic_range):
588        """Calculate the isotopologues for a given molecular formula.
589
590        Parameters
591        ----------
592        min_abundance : float
593            The minimum abundance.
594        current_mono_abundance : float
595            The current monoisotopic abundance.
596        dynamic_range : float
597            The dynamic range.
598
599        Yields
600        ------
601        MolecularFormulaIsotopologue
602            The molecular formula isotopologue.
603
604        Notes
605        -----
606        This calculation ignores the hydrogen isotopes.
607        """
608        isotopologues = []
609        for mf in self._cal_isotopologues(
610            self._d_molecular_formula,
611            min_abundance,
612            current_mono_abundance,
613            dynamic_range,
614        ):
615            isotopologues.append(mf)
616
617        # To account for differences in how the isotopologue outputs are sorted between IsoSpec versions.
618        sorted_isotopologues = sorted(isotopologues, key=lambda mf: mf[1], reverse=True)
619
620        for mf in sorted_isotopologues:
621            yield MolecularFormulaIsotopologue(
622                *mf,
623                current_mono_abundance,
624                self.ion_charge,
625                ion_type=self.ion_type,
626                adduct_atom=self.adduct_atom,
627            )
628
629    def atoms_qnt(self, atom):
630        """Get the atom quantity of a specific atom in the molecular formula."""
631        if atom in self._d_molecular_formula:
632            return self._d_molecular_formula.get(atom)
633        else:
634            raise Warning(
635                "Could not find %s in this Molecular Formula object" % str(atom)
636            )
637
638    def atoms_symbol(self, atom):
639        """Get the atom symbol without the mass number."""
640        return "".join([i for i in atom if not i.isdigit()])
641
642    @property
643    def string(self):
644        """Returns the molecular formula as a string."""
645        if self._d_molecular_formula:
646            if self.adduct_atom is None:
647                mol_form_dict = self._d_molecular_formula
648            else:
649                mol_form_dict = self._d_molecular_formula.copy()
650                if self.adduct_atom not in mol_form_dict.keys():
651                    raise Exception("Adduct atom not found in molecular formula dict")
652                mol_form_dict[self.adduct_atom] -= 1
653                mol_form_dict = {
654                    key: val for key, val in mol_form_dict.items() if val != 0
655                }
656            formula_srt = ""
657            for atom in Atoms.atoms_order:
658                if atom in mol_form_dict.keys():
659                    formula_srt += atom + str(int(mol_form_dict.get(atom))) + " "
660            return formula_srt.strip()
661
662        else:
663            raise Exception("Molecular formula identification not performed yet")
664
665    @property
666    def string_formated(self):
667        SUB = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
668        SUP = str.maketrans("0123456789", "⁰¹²³⁴⁵⁶⁷⁸⁹")
669
670        if self._d_molecular_formula:
671            formula_srt = ""
672            for atom in Atoms.atoms_order:
673                if atom in self.to_dict().keys():
674                    formula_srt += atom.translate(SUP) + str(
675                        int(self.to_dict().get(atom))
676                    ).translate(SUB)
677            return formula_srt
678
679        else:
680            raise Exception("Molecular formula identification not performed yet")
681
682    def to_dict(self):
683        """Returns the molecular formula as a dictionary.
684
685        Returns
686        -------
687        dict
688            The molecular formula as a dictionary.
689        """
690        return self._d_molecular_formula
691
692    def to_list(self):
693        """Returns the molecular formula as a list.
694
695        Returns
696        -------
697        list
698            The molecular formula as a list.
699
700        Raises
701        ------
702        Exception
703            If the molecular formula identification was not performed yet.
704        """
705        # TODO ensure self._d_molecular_formula is a orderedDict
706
707        if self._d_molecular_formula:
708            formula_list = []
709
710            for atom, atom_number in self._d_molecular_formula.items():
711                if atom != Labels.ion_type:
712                    formula_list.append(atom)
713                    formula_list.append(atom_number)
714
715            return formula_list
716        else:
717            raise Exception("Molecular formula identification not performed yet")
718
719    @property
720    def class_label(self):
721        if self._d_molecular_formula:
722            formulalist = self.to_list()
723            classstring = ""
724
725            for each in range(0, len(formulalist), 2):
726                if (
727                    formulalist[each] != "C"
728                    and formulalist[each] != "H"
729                    and formulalist[each] != "HC"
730                ):
731                    classstring = (
732                        classstring
733                        + str(formulalist[each])
734                        + str(formulalist[each + 1])
735                        + " "
736                    )
737
738            if classstring == "":
739                classstring = "HC"
740
741            classstring = classstring.strip()
742
743            if self._d_molecular_formula.get(Labels.ion_type) == Labels.radical_ion:
744                return classstring + " -R"
745
746            # elif self._d_molecular_formula.get(Labels.ion_type) == Labels.adduct_ion:
747
748            #    return classstring + ' -A'
749
750            else:
751                return classstring
752
753            #'dict, tuple or string'
754
755        else:
756            raise Exception("Molecular formula identification not performed yet")
757
758    @property
759    def class_dict(self):
760        if self._d_molecular_formula:
761            class_dict = {}
762
763            for atom, qnt in self._d_molecular_formula.items():
764                if atom != Labels.ion_type and atom != "C" and atom != "H":
765                    class_dict[atom] = qnt
766
767            return class_dict
768
769        raise Exception("Molecular formula identification not performed yet")
770
771
772class MolecularFormulaIsotopologue(MolecularFormulaBase):
773    """Class for representing a molecular formula isotopologue.
774
775    Parameters
776    ----------
777    _d_molecular_formula : dict
778        The molecular formula as a dictionary.
779    prob_ratio : float
780        The probability ratio.
781    mono_abundance : float
782        The monoisotopic abundance.
783    ion_charge : int
784        The ion charge.
785    mspeak_parent : object, optional
786        The parent mass spectrum peak object instance. Defaults to None.
787    ion_type : str, optional
788        The ion type. Defaults to None.
789    adduct_atom : str, optional
790        The adduct atom. Defaults to None.
791
792    Attributes
793    ----------
794    prob_ratio : float
795        The probability ratio.
796    abundance_calc : float
797        The calculated abundance.
798    area_error : float
799        The area error.
800    abundance_error : float
801        The abundance error.
802    is_isotopologue : bool
803        The isotopologue flag. Defaults to True.
804    mspeak_index_mono_isotopic : int
805        The index of the monoisotopic peak in the mass spectrum peak list. Defaults to None.
806    mono_isotopic_formula_index : int
807        The index of the monoisotopic formula in the molecular formula list. Defaults to None.
808    """
809
810    def __init__(
811        self,
812        _d_molecular_formula,
813        prob_ratio,
814        mono_abundance,
815        ion_charge,
816        mspeak_parent=None,
817        ion_type=None,
818        adduct_atom=None,
819    ):
820        if ion_type is None:
821            # check if ion type or adduct_atom is in the molecular formula dict
822            if Labels.ion_type in _d_molecular_formula:
823                ion_type = _d_molecular_formula.get(Labels.ion_type)
824            else:
825                ion_type = None
826        else:
827            ion_type = Labels.ion_type_translate.get(ion_type)
828
829        if ion_type == Labels.adduct_ion:
830            adduct_atom_int = None
831            if adduct_atom in _d_molecular_formula.keys():
832                adduct_atom_int = adduct_atom
833            else:
834                # Check to see if adduct_atom should actually be an isotope of the adduct atom
835                for adduct_iso in Atoms.isotopes.get(adduct_atom)[1]:
836                    if adduct_iso in _d_molecular_formula.keys():
837                        adduct_atom_int = adduct_iso
838            adduct_atom = adduct_atom_int
839            if adduct_atom is None:
840                raise Exception("adduct_atom is required for adduct ion")
841            _d_molecular_formula[adduct_atom] -= 1
842            _d_molecular_formula = {
843                key: val for key, val in _d_molecular_formula.items() if val != 0
844            }
845
846        super().__init__(
847            molecular_formula=_d_molecular_formula,
848            ion_charge=ion_charge,
849            ion_type=ion_type,
850            adduct_atom=adduct_atom,
851        )
852        # prob_ratio is relative to the monoisotopic peak p_isotopologue/p_mono_isotopic
853
854        self.prob_ratio = prob_ratio
855
856        self.abundance_calc = mono_abundance * prob_ratio
857
858        self.is_isotopologue = True
859
860        self.mspeak_index_mono_isotopic = None
861
862        self.mono_isotopic_formula_index = None
863        # parent mass spectrum peak obj instance
864        self._mspeak_parent = mspeak_parent
865
866    @property
867    def area_error(self):
868        return self._calc_area_error()
869
870    @property
871    def abundance_error(self):
872        return self._calc_abundance_error()
873
874
875class LCMSLibRefMolecularFormula:
876    """Removed LC-MS library reference molecular formula type.
877
878    This class is no longer supported. Canonical LC-MS MS1 formula assignment
879    uses :class:`MolecularFormula` via combinatorial / SQL search. Construction
880    raises :class:`NotImplementedError`. The name is retained as a stub until
881    the next major release.
882
883    Parameters
884    ----------
885    *args
886        Ignored; retained only for call-site compatibility.
887    **kwargs
888        Ignored; retained only for call-site compatibility.
889    """
890
891    def __init__(self, *args, **kwargs) -> None:
892        raise NotImplementedError(
893            "LCMSLibRefMolecularFormula is no longer supported. "
894            "Use MolecularFormula for formula objects. "
895            "This stub will be removed in the next major release."
896        )
897
898
899class MolecularFormula(MolecularFormulaBase):
900    """General class for representing a molecular formula.
901
902    Parameters
903    ----------
904    molecular_formula : dict, list, str
905        The molecular formula.
906    ion_charge : int
907        The ion charge.
908    ion_type : str, optional
909        The ion type. Defaults to None.
910    adduct_atom : str, optional
911        The adduct atom. Defaults to None.
912    mspeak_parent : object, optional
913        The parent mass spectrum peak object instance. Defaults to None.
914    external_mz : float, optional
915        The external m/z value. Defaults to False.
916    """
917
918    def __init__(
919        self,
920        molecular_formula,
921        ion_charge,
922        ion_type=None,
923        adduct_atom=None,
924        mspeak_parent=None,
925        external_mz=False,
926    ):
927        super().__init__(
928            molecular_formula,
929            ion_charge,
930            ion_type=ion_type,
931            adduct_atom=adduct_atom,
932            mspeak_parent=mspeak_parent,
933            external_mz=external_mz,
934        )
 11class MolecularFormulaBase(MolecularFormulaCalc):
 12    """Base class for representing a molecular formula.
 13
 14    Parameters
 15    ----------
 16    molecular_formula : dict, list, str
 17        The molecular formula.
 18    ion_charge : int
 19        The ion charge.
 20    ion_type : str, optional
 21        The ion type. Defaults to None.
 22    adduct_atom : str, optional
 23        The adduct atom. Defaults to None.
 24    mspeak_parent : _MSPeak, optional
 25        The parent mass spectrum peak object instance. Defaults to None.
 26    external_mz : float, optional
 27        The external m/z value. Defaults to None.
 28
 29    Raises
 30    ------
 31    TypeError
 32        If the ion type is not 'DE_OR_PROTONATED', 'RADICAL' or  'ADDUCT'.
 33
 34    Attributes
 35    ----------
 36    isotopologue_count_percentile : float
 37        The isotopologue count percentile.
 38    O_C : float
 39        The O/C ratio.
 40    H_C : float
 41        The H/C ratio.
 42    dbe : float
 43        The double bond equivalent.
 44    mz_nominal_calc : int
 45        The nominal m/z value.
 46    mz_error : float
 47        The m/z error.
 48    mz_calc : float
 49        The m/z value.
 50    protonated_mz : float
 51        The protonated or deprotonated m/z value.
 52    radical_mz : float
 53        The radical m/z value.
 54    neutral_mass : float
 55        The neutral mass.
 56    ion_type : str
 57        The ion type.
 58    ion_charge : int
 59        The ion charge.
 60    atoms : list
 61        The atoms in the molecular formula.
 62    confidence_score : float
 63        Composite assignment confidence score in about [0, 1] (higher is
 64        better). Weighted sum of ``average_mz_error_score`` and
 65        ``isotopologue_similarity`` using
 66        ``mz_error_score_weight`` / ``isotopologue_score_weight`` from the
 67        parent spectrum's molecular search settings. See the
 68        ``corems.molecular_formula`` package documentation for the full
 69        formulation and literature reference.
 70    isotopologue_similarity : float
 71        Similarity between expected and observed isotopologue abundance
 72        patterns (about [0, 1]). Zero when no isotopologues are expected or
 73        none were computed during search.
 74    average_mz_error_score : float
 75        Mean Gaussian mass-accuracy score over the monoisotopic peak and
 76        expected isotopologue peaks. Missing expected isotopologues
 77        contribute 0.
 78    mz_error_score : float
 79        Gaussian mass-accuracy score for this formula on its parent peak
 80        only (ppm error vs ``predicted_std`` or a 1.66 ppm fallback).
 81    kmd : float
 82        The Kendrick mass defect (KMD).
 83    kendrick_mass : float
 84        The Kendrick mass.
 85    knm : float
 86        The nominal Kendrick mass.
 87    string : str
 88        The molecular formula string.
 89    string_formated : str
 90        The molecular formula string formated with subscripts and superscripts.
 91    class_label : str
 92        The class label.
 93    class_dict : dict
 94        The class dictionary.
 95
 96    Methods
 97    -------
 98    * change_kendrick_base(kendrick_dict_base).
 99        Change the Kendrick base.
100    * isotopologues(min_abundance, current_mono_abundance, dynamic_range).
101        Calculate the isotopologues.
102    * atoms_qnt(atom).
103        Get the atom quantity.
104    * atoms_symbol(atom).
105        Get the atom symbol without the mass number.
106    * to_dict().
107        Get the molecular formula as a dictionary.
108    * to_list().
109        Get the molecular formula as a list.
110    """
111
112    def __init__(
113        self,
114        molecular_formula,
115        ion_charge,
116        ion_type=None,
117        adduct_atom=None,
118        mspeak_parent=None,
119        external_mz=None,
120    ):
121        # clear dictionary of atoms with 0 value
122        if type(molecular_formula) is dict:
123            self._from_dict(molecular_formula, ion_type, adduct_atom)
124
125        elif type(molecular_formula) is list:
126            self._from_list(molecular_formula, ion_type, adduct_atom)
127
128        elif type(molecular_formula) is str:
129            self._from_str(molecular_formula, ion_type, adduct_atom)
130
131        self._ion_charge = ion_charge
132        self._external_mz = external_mz
133        self._confidence_score = None
134        self._isotopologue_similarity = None
135        self._mz_error_score = None
136        self._mass_error_average_score = None
137
138        self.is_isotopologue = False
139
140        # parent mass spectrum peak obj instance
141        self._mspeak_parent = mspeak_parent
142
143        self.expected_isotopologues = []
144        self.mspeak_mf_isotopologues_indexes = []
145
146        if self._mspeak_parent:
147            kendrick_dict_base = (
148                self._mspeak_parent._ms_parent.mspeaks_settings.kendrick_base
149            )
150        else:
151            kendrick_dict_base = {"C": 1, "H": 2}
152        self._kmd, self._kendrick_mass, self._nominal_km = self._calc_kmd(
153            kendrick_dict_base
154        )
155
156    def __repr__(self):
157        return "MolecularFormula({0},{1},ion type = {2}".format(
158            self._d_molecular_formula, self.ion_charge, self.ion_type
159        )
160
161    def __str__(self):
162        return "MolecularFormula {0}, ion_charge:{1}, ion type:{2}, m/z:{3} ".format(
163            self.string, self.ion_charge, self.ion_type, self.mz_calc
164        )
165
166    def __len__(self):
167        # crash if keys are not ordered
168        return len(self._d_molecular_formula.keys())
169
170    def __getitem__(self, atom):
171        # atom = list(self._d_molecular_formula.keys())[position]
172        if atom in self._d_molecular_formula.keys():
173            return self._d_molecular_formula[atom]
174        else:
175            return 0
176
177    def get(self, atom):
178        """Get the atom quantity of a specific atom.
179
180        Parameters
181        ----------
182        atom : str
183            The atom symbol.
184
185        Returns
186        -------
187        int
188            The atom quantity.
189        """
190        # atom = list(self._d_molecular_formula.keys())[position]
191        if atom in self._d_molecular_formula.keys():
192            return self._d_molecular_formula[atom]
193        else:
194            return 0
195
196    def _from_dict(self, molecular_formula, ion_type, adduct_atom):
197        self._d_molecular_formula = {
198            key: val for key, val in molecular_formula.items() if val != 0
199        }
200
201        if ion_type is not None:
202            self._d_molecular_formula[Labels.ion_type] = ion_type
203
204        if adduct_atom:
205            if adduct_atom in self._d_molecular_formula:
206                self._d_molecular_formula[adduct_atom] += 1
207            else:
208                self._d_molecular_formula[adduct_atom] = 1
209        self.adduct_atom = adduct_atom
210
211    def _from_list(self, molecular_formula_list, ion_type, adduct_atom):
212        # list has to be in the format
213        # ['C', 10, 'H', 21, '13C', 1, 'Cl', 1, etc]
214        self._d_molecular_formula = {}
215        for each in range(0, len(molecular_formula_list), 2):
216            atoms_label = molecular_formula_list[each]
217            atoms_count = int(molecular_formula_list[each + 1])
218
219            if atoms_count > 0:
220                self._d_molecular_formula[atoms_label] = int(atoms_count)
221
222        self._d_molecular_formula[Labels.ion_type] = ion_type
223        if adduct_atom:
224            self.adduct_atom = adduct_atom
225            if adduct_atom in self._d_molecular_formula:
226                self._d_molecular_formula[adduct_atom] += 1
227            else:
228                self._d_molecular_formula[adduct_atom] = 1
229        else:
230            self.adduct_atom = None
231
232    def _from_str(self, molecular_formula_str, ion_type, adduct_atom):
233        # string has to be in the format
234        #'C10 H21 13C1 Cl1 37Cl1 etc'
235        # Check if there are spaces in the string
236        if " " not in molecular_formula_str:
237            raise ValueError(
238                "The molecular formula string should have spaces, input: %s"
239                % molecular_formula_str
240            )
241
242        # Split the string by spaces
243        # Grab the text before a digit for each element after splitting on spaces (atoms)
244        elements = [re.sub(r"\d+$", "", x) for x in molecular_formula_str.split()]
245        # Grab the digits at the end of each element after splitting on spaces (counts)
246        counts = [re.findall(r"\d+$", x)[0] for x in molecular_formula_str.split()]
247        # Check that the number of elements and counts are the same
248        if len(elements) != len(counts):
249            raise ValueError(
250                "The number of elements and counts do not match, input: %s"
251                % molecular_formula_str
252            )
253
254        # Create a dictionary from the elements and counts and add it to the molecular formula
255        dict_ = dict(zip(elements, counts))
256        # Cast counts to integers
257        dict_ = {key: int(val) for key, val in dict_.items()}
258        self._from_dict(dict_, ion_type, adduct_atom)
259
260    def split(self, delimiters, string, maxsplit=0):  # pragma: no cover
261        """Splits the molecular formula string.
262
263        Parameters
264        ----------
265        delimiters : list
266            The list of delimiters.
267        string : str
268            The molecular formula string.
269        maxsplit : int, optional
270            The maximum number of splits. Defaults to 0.
271
272        Returns
273        -------
274        list
275            The molecular formula list.
276
277        Notes
278        -----
279        Does not work when formula has atoms with same characters in a row that below to different atoms, i.e. C10H21NNa.
280        """
281        regexPattern = "|".join(map(re.escape, delimiters))  # pragma: no cover
282        isotopes = re.findall(regexPattern, string)  # pragma: no cover
283        counts = re.split(regexPattern, string, maxsplit)  # pragma: no cover
284
285        return [isotopes[0], int(counts[1])]
286
287    @property
288    def isotopologue_count_percentile(
289        self,
290    ):
291        if not len(self.expected_isotopologues) == 0:
292            return (
293                len(self.mspeak_mf_isotopologues_indexes)
294                / len(self.expected_isotopologues)
295            ) * 100
296        else:
297            return 100
298
299    @property
300    def O_C(self):
301        if "O" in self._d_molecular_formula.keys():
302            # gather all the Os and Hs, regardless of the isotopic composition
303            Os = sum(
304                [
305                    self._d_molecular_formula.get(key)
306                    for key in ["O"] + Atoms.isotopes["O"][1]
307                    if key in self._d_molecular_formula.keys()
308                ]
309            )
310            Cs = sum(
311                [
312                    self._d_molecular_formula.get(key)
313                    for key in ["C"] + Atoms.isotopes["C"][1]
314                    if key in self._d_molecular_formula.keys()
315                ]
316            )
317            return Os / Cs
318        else:
319            return 0
320
321    @property
322    def H_C(self):
323        # gather all the Cs and Hs, regardless of the isotopic composition
324        Cs = sum(
325            [
326                self._d_molecular_formula.get(key)
327                for key in ["C"] + Atoms.isotopes["C"][1]
328                if key in self._d_molecular_formula.keys()
329            ]
330        )
331        Hs = sum(
332            [
333                self._d_molecular_formula.get(key)
334                for key in ["H"] + Atoms.isotopes["H"][1]
335                if key in self._d_molecular_formula.keys()
336            ]
337        )
338        return Hs / Cs
339
340    @property
341    def A_I(self):
342        """Aromaticity index"""
343        return self._calc_aromaticity_index()
344
345    @property
346    def A_I_mod(self):
347        """Modified aromaticity index"""
348        return self._calc_aromaticity_index_mod()
349
350    @property
351    def nosc(self):
352        """Nominal oxidation state of carbon"""
353        return self._calc_nosc()
354
355    @property
356    def dbe(self):
357        return self._calc_dbe()
358
359    @property
360    def mz_nominal_calc(self):
361        return int(self._calc_mz())
362
363    @property
364    def mz_error(self):
365        return self._calc_assignment_mass_error()
366
367    @property
368    def mz_calc(self):
369        return self._calc_mz()
370
371    @property
372    def protonated_mz(self):
373        return self._protonated_mz(self.ion_charge)
374
375    @property
376    def radical_mz(self):
377        return self._radical_mz(self.ion_charge)
378
379    @property
380    def neutral_mass(self):
381        return self._neutral_mass()
382
383    def adduct_mz(self, adduct_atom):
384        """Get m/z of an adducted ion version of the molecular formula.
385
386        Parameters
387        ----------
388        adduct_atom : str
389            The adduct atom.
390
391        Returns
392        -------
393        float
394            The m/z value of the adducted ion version of the molecular formula.
395        """
396        return self._adduct_mz(adduct_atom, self.ion_charge)
397
398    @property
399    def ion_type(self):
400        ion_type = self._d_molecular_formula.get(Labels.ion_type)
401        if ion_type == Labels.protonated_de_ion:
402            if self.ion_charge > 0:
403                return Labels.protonated
404            else:
405                return Labels.de_protonated
406        else:
407            return ion_type
408
409    @ion_type.setter
410    def ion_type(self, ion_type):
411        if ion_type in [
412            Labels.protonated_de_ion,
413            Labels.adduct_ion,
414            Labels.radical_ion,
415        ]:
416            self._d_molecular_formula[Labels.ion_type] = ion_type
417        else:
418            raise TypeError(
419                "Ion type can only be: 'DE_OR_PROTONATED', 'RADICAL' or  'ADDUCT', not %s"
420                % ion_type
421            )
422
423    @property
424    def ion_charge(self):
425        return self._ion_charge
426
427    @property
428    def atoms(self):
429        """Get the atoms in the molecular formula."""
430        # if there is an adduct_atom, them reduce it from the atoms list
431        if self.adduct_atom is None:
432            return [
433                key
434                for key in self._d_molecular_formula.keys()
435                if key != Labels.ion_type
436            ]
437        else:
438            temp_dict = self._d_molecular_formula.copy()
439            temp_dict[self.adduct_atom] -= 1
440            return [
441                key
442                for key, val in temp_dict.items()
443                if key != Labels.ion_type and val > 0
444            ]
445
446    @property
447    def confidence_score(self):
448        """Composite molecular formula assignment confidence score.
449
450        Combines formula-level mass accuracy and isotopologue pattern
451        agreement:
452
453        ```
454        CS = (w_err * m_err) + (w_iso * m_iso)
455        ```
456
457        where ``m_err`` is :attr:`average_mz_error_score`, ``m_iso`` is
458        :attr:`isotopologue_similarity`, and the weights are
459        ``molecular_search_settings.mz_error_score_weight`` (default 0.6)
460        and ``isotopologue_score_weight`` (default 0.4).
461
462        Returns
463        -------
464        float
465            Score typically in about [0, 1]; higher values indicate better
466            agreement with mass error and (when available) isotopologue
467            evidence. Used for ranking when ``score_method`` is
468            ``\"prob_score\"``.
469
470        Notes
471        -----
472        Requires an associated parent mass spectral peak. Isotopologue
473        terms are only informative when search was run with isotopologue
474        detection enabled. See the ``corems.molecular_formula`` package
475        documentation and Dewey et al., *Anal. Chem.* 2025, 97, 13031–13039
476        (https://doi.org/10.1021/acs.analchem.4c06826).
477        """
478        if not self._confidence_score:
479            self._confidence_score = self._calc_confidence_score()
480
481        return self._confidence_score
482
483    @property
484    def isotopologue_similarity(self):
485        """Isotopologue abundance pattern similarity for this formula.
486
487        Compares expected natural-abundance isotopologue intensities
488        (from dynamic-range-limited expansion) to abundances of peaks
489        assigned as those partners. Implemented as a normalized Manhattan
490        distance mapped to a similarity in about [0, 1]
491        (higher is better).
492
493        Returns
494        -------
495        float
496            Similarity score. Returns 0.0 when no isotopologues are
497            expected for this monoisotopic formula (or none were computed).
498
499        Notes
500        -----
501        This is the ``m_iso`` term in :attr:`confidence_score`. See the
502        ``corems.molecular_formula`` package documentation for details.
503        """
504        if not self._isotopologue_similarity:
505            self._isotopologue_similarity = self._calc_isotopologue_confidence()
506
507        return self._isotopologue_similarity
508
509    @property
510    def average_mz_error_score(self):
511        """Mean mass-accuracy score over mono and expected isotopologues.
512
513        Averages :attr:`mz_error_score` for the monoisotopic assignment and
514        for each expected isotopologue formula that has a parent peak.
515        Expected isotopologues without a matched peak contribute 0.
516
517        Returns
518        -------
519        float
520            Mean Gaussian mass-error score in about [0, 1].
521
522        Notes
523        -----
524        This is the ``m_err`` term in :attr:`confidence_score`.
525        """
526        # includes the isotopologues
527
528        if not self._mass_error_average_score:
529            self._mass_error_average_score = self._calc_average_mz_score()
530
531        return self._mass_error_average_score
532
533    @property
534    def mz_error_score(self):
535        """Gaussian mass-accuracy score for this formula on its parent peak.
536
537        Uses the assignment mass error ``delta`` (ppm) and a width ``sigma``
538        equal to the parent peak's ``predicted_std`` when set, otherwise a
539        fallback of 1.66 ppm:
540
541        ```
542        m = exp( -((delta - mu) ** 2) / (2 * (sigma ** 2)) )
543        ```
544
545        with mean ``mu = 0`` (calibrated spectrum with near-zero mean error
546        assumed).
547
548        Returns
549        -------
550        float
551            Score typically in about 0 to 1; exact mass match is 1,
552            and larger ppm error yields values closer to 0.
553
554        Notes
555        -----
556        Formula-level ranking uses :attr:`average_mz_error_score`, which
557        folds in expected isotopologue peaks as well.
558        """
559        if not self._mz_error_score:
560            self._mz_error_score = self._calc_mz_confidence()
561
562        return self._mz_error_score
563
564    @property
565    def kmd(self):
566        return self._kmd
567
568    @property
569    def kendrick_mass(self):
570        return self._kendrick_mass
571
572    @property
573    def knm(self):
574        return self._nominal_km
575
576    def change_kendrick_base(self, kendrick_dict_base):
577        """Change the Kendrick base.
578
579        Parameters
580        ----------
581        kendrick_dict_base : dict
582            The Kendrick base dictionary. Ex: {"C": 1, "H": 2}
583        """
584        self._kmd, self._kendrick_mass, self._nominal_km = self._calc_kmd(
585            kendrick_dict_base
586        )
587
588    def isotopologues(self, min_abundance, current_mono_abundance, dynamic_range):
589        """Calculate the isotopologues for a given molecular formula.
590
591        Parameters
592        ----------
593        min_abundance : float
594            The minimum abundance.
595        current_mono_abundance : float
596            The current monoisotopic abundance.
597        dynamic_range : float
598            The dynamic range.
599
600        Yields
601        ------
602        MolecularFormulaIsotopologue
603            The molecular formula isotopologue.
604
605        Notes
606        -----
607        This calculation ignores the hydrogen isotopes.
608        """
609        isotopologues = []
610        for mf in self._cal_isotopologues(
611            self._d_molecular_formula,
612            min_abundance,
613            current_mono_abundance,
614            dynamic_range,
615        ):
616            isotopologues.append(mf)
617
618        # To account for differences in how the isotopologue outputs are sorted between IsoSpec versions.
619        sorted_isotopologues = sorted(isotopologues, key=lambda mf: mf[1], reverse=True)
620
621        for mf in sorted_isotopologues:
622            yield MolecularFormulaIsotopologue(
623                *mf,
624                current_mono_abundance,
625                self.ion_charge,
626                ion_type=self.ion_type,
627                adduct_atom=self.adduct_atom,
628            )
629
630    def atoms_qnt(self, atom):
631        """Get the atom quantity of a specific atom in the molecular formula."""
632        if atom in self._d_molecular_formula:
633            return self._d_molecular_formula.get(atom)
634        else:
635            raise Warning(
636                "Could not find %s in this Molecular Formula object" % str(atom)
637            )
638
639    def atoms_symbol(self, atom):
640        """Get the atom symbol without the mass number."""
641        return "".join([i for i in atom if not i.isdigit()])
642
643    @property
644    def string(self):
645        """Returns the molecular formula as a string."""
646        if self._d_molecular_formula:
647            if self.adduct_atom is None:
648                mol_form_dict = self._d_molecular_formula
649            else:
650                mol_form_dict = self._d_molecular_formula.copy()
651                if self.adduct_atom not in mol_form_dict.keys():
652                    raise Exception("Adduct atom not found in molecular formula dict")
653                mol_form_dict[self.adduct_atom] -= 1
654                mol_form_dict = {
655                    key: val for key, val in mol_form_dict.items() if val != 0
656                }
657            formula_srt = ""
658            for atom in Atoms.atoms_order:
659                if atom in mol_form_dict.keys():
660                    formula_srt += atom + str(int(mol_form_dict.get(atom))) + " "
661            return formula_srt.strip()
662
663        else:
664            raise Exception("Molecular formula identification not performed yet")
665
666    @property
667    def string_formated(self):
668        SUB = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
669        SUP = str.maketrans("0123456789", "⁰¹²³⁴⁵⁶⁷⁸⁹")
670
671        if self._d_molecular_formula:
672            formula_srt = ""
673            for atom in Atoms.atoms_order:
674                if atom in self.to_dict().keys():
675                    formula_srt += atom.translate(SUP) + str(
676                        int(self.to_dict().get(atom))
677                    ).translate(SUB)
678            return formula_srt
679
680        else:
681            raise Exception("Molecular formula identification not performed yet")
682
683    def to_dict(self):
684        """Returns the molecular formula as a dictionary.
685
686        Returns
687        -------
688        dict
689            The molecular formula as a dictionary.
690        """
691        return self._d_molecular_formula
692
693    def to_list(self):
694        """Returns the molecular formula as a list.
695
696        Returns
697        -------
698        list
699            The molecular formula as a list.
700
701        Raises
702        ------
703        Exception
704            If the molecular formula identification was not performed yet.
705        """
706        # TODO ensure self._d_molecular_formula is a orderedDict
707
708        if self._d_molecular_formula:
709            formula_list = []
710
711            for atom, atom_number in self._d_molecular_formula.items():
712                if atom != Labels.ion_type:
713                    formula_list.append(atom)
714                    formula_list.append(atom_number)
715
716            return formula_list
717        else:
718            raise Exception("Molecular formula identification not performed yet")
719
720    @property
721    def class_label(self):
722        if self._d_molecular_formula:
723            formulalist = self.to_list()
724            classstring = ""
725
726            for each in range(0, len(formulalist), 2):
727                if (
728                    formulalist[each] != "C"
729                    and formulalist[each] != "H"
730                    and formulalist[each] != "HC"
731                ):
732                    classstring = (
733                        classstring
734                        + str(formulalist[each])
735                        + str(formulalist[each + 1])
736                        + " "
737                    )
738
739            if classstring == "":
740                classstring = "HC"
741
742            classstring = classstring.strip()
743
744            if self._d_molecular_formula.get(Labels.ion_type) == Labels.radical_ion:
745                return classstring + " -R"
746
747            # elif self._d_molecular_formula.get(Labels.ion_type) == Labels.adduct_ion:
748
749            #    return classstring + ' -A'
750
751            else:
752                return classstring
753
754            #'dict, tuple or string'
755
756        else:
757            raise Exception("Molecular formula identification not performed yet")
758
759    @property
760    def class_dict(self):
761        if self._d_molecular_formula:
762            class_dict = {}
763
764            for atom, qnt in self._d_molecular_formula.items():
765                if atom != Labels.ion_type and atom != "C" and atom != "H":
766                    class_dict[atom] = qnt
767
768            return class_dict
769
770        raise Exception("Molecular formula identification not performed yet")

Base class for representing a molecular formula.

Parameters
  • molecular_formula (dict, list, str): The molecular formula.
  • ion_charge (int): The ion charge.
  • ion_type (str, optional): The ion type. Defaults to None.
  • adduct_atom (str, optional): The adduct atom. Defaults to None.
  • mspeak_parent (_MSPeak, optional): The parent mass spectrum peak object instance. Defaults to None.
  • external_mz (float, optional): The external m/z value. Defaults to None.
Raises
  • TypeError: If the ion type is not 'DE_OR_PROTONATED', 'RADICAL' or 'ADDUCT'.
Attributes
  • isotopologue_count_percentile (float): The isotopologue count percentile.
  • O_C (float): The O/C ratio.
  • H_C (float): The H/C ratio.
  • dbe (float): The double bond equivalent.
  • mz_nominal_calc (int): The nominal m/z value.
  • mz_error (float): The m/z error.
  • mz_calc (float): The m/z value.
  • protonated_mz (float): The protonated or deprotonated m/z value.
  • radical_mz (float): The radical m/z value.
  • neutral_mass (float): The neutral mass.
  • ion_type (str): The ion type.
  • ion_charge (int): The ion charge.
  • atoms (list): The atoms in the molecular formula.
  • confidence_score (float): Composite assignment confidence score in about [0, 1] (higher is better). Weighted sum of average_mz_error_score and isotopologue_similarity using mz_error_score_weight / isotopologue_score_weight from the parent spectrum's molecular search settings. See the corems.molecular_formula package documentation for the full formulation and literature reference.
  • isotopologue_similarity (float): Similarity between expected and observed isotopologue abundance patterns (about [0, 1]). Zero when no isotopologues are expected or none were computed during search.
  • average_mz_error_score (float): Mean Gaussian mass-accuracy score over the monoisotopic peak and expected isotopologue peaks. Missing expected isotopologues contribute 0.
  • mz_error_score (float): Gaussian mass-accuracy score for this formula on its parent peak only (ppm error vs predicted_std or a 1.66 ppm fallback).
  • kmd (float): The Kendrick mass defect (KMD).
  • kendrick_mass (float): The Kendrick mass.
  • knm (float): The nominal Kendrick mass.
  • string (str): The molecular formula string.
  • string_formated (str): The molecular formula string formated with subscripts and superscripts.
  • class_label (str): The class label.
  • class_dict (dict): The class dictionary.
Methods
  • change_kendrick_base(kendrick_dict_base). Change the Kendrick base.
  • isotopologues(min_abundance, current_mono_abundance, dynamic_range). Calculate the isotopologues.
  • atoms_qnt(atom). Get the atom quantity.
  • atoms_symbol(atom). Get the atom symbol without the mass number.
  • to_dict(). Get the molecular formula as a dictionary.
  • to_list(). Get the molecular formula as a list.
MolecularFormulaBase( molecular_formula, ion_charge, ion_type=None, adduct_atom=None, mspeak_parent=None, external_mz=None)
112    def __init__(
113        self,
114        molecular_formula,
115        ion_charge,
116        ion_type=None,
117        adduct_atom=None,
118        mspeak_parent=None,
119        external_mz=None,
120    ):
121        # clear dictionary of atoms with 0 value
122        if type(molecular_formula) is dict:
123            self._from_dict(molecular_formula, ion_type, adduct_atom)
124
125        elif type(molecular_formula) is list:
126            self._from_list(molecular_formula, ion_type, adduct_atom)
127
128        elif type(molecular_formula) is str:
129            self._from_str(molecular_formula, ion_type, adduct_atom)
130
131        self._ion_charge = ion_charge
132        self._external_mz = external_mz
133        self._confidence_score = None
134        self._isotopologue_similarity = None
135        self._mz_error_score = None
136        self._mass_error_average_score = None
137
138        self.is_isotopologue = False
139
140        # parent mass spectrum peak obj instance
141        self._mspeak_parent = mspeak_parent
142
143        self.expected_isotopologues = []
144        self.mspeak_mf_isotopologues_indexes = []
145
146        if self._mspeak_parent:
147            kendrick_dict_base = (
148                self._mspeak_parent._ms_parent.mspeaks_settings.kendrick_base
149            )
150        else:
151            kendrick_dict_base = {"C": 1, "H": 2}
152        self._kmd, self._kendrick_mass, self._nominal_km = self._calc_kmd(
153            kendrick_dict_base
154        )
is_isotopologue
expected_isotopologues
mspeak_mf_isotopologues_indexes
def get(self, atom):
177    def get(self, atom):
178        """Get the atom quantity of a specific atom.
179
180        Parameters
181        ----------
182        atom : str
183            The atom symbol.
184
185        Returns
186        -------
187        int
188            The atom quantity.
189        """
190        # atom = list(self._d_molecular_formula.keys())[position]
191        if atom in self._d_molecular_formula.keys():
192            return self._d_molecular_formula[atom]
193        else:
194            return 0

Get the atom quantity of a specific atom.

Parameters
  • atom (str): The atom symbol.
Returns
  • int: The atom quantity.
def split(self, delimiters, string, maxsplit=0):
260    def split(self, delimiters, string, maxsplit=0):  # pragma: no cover
261        """Splits the molecular formula string.
262
263        Parameters
264        ----------
265        delimiters : list
266            The list of delimiters.
267        string : str
268            The molecular formula string.
269        maxsplit : int, optional
270            The maximum number of splits. Defaults to 0.
271
272        Returns
273        -------
274        list
275            The molecular formula list.
276
277        Notes
278        -----
279        Does not work when formula has atoms with same characters in a row that below to different atoms, i.e. C10H21NNa.
280        """
281        regexPattern = "|".join(map(re.escape, delimiters))  # pragma: no cover
282        isotopes = re.findall(regexPattern, string)  # pragma: no cover
283        counts = re.split(regexPattern, string, maxsplit)  # pragma: no cover
284
285        return [isotopes[0], int(counts[1])]

Splits the molecular formula string.

Parameters
  • delimiters (list): The list of delimiters.
  • string (str): The molecular formula string.
  • maxsplit (int, optional): The maximum number of splits. Defaults to 0.
Returns
  • list: The molecular formula list.
Notes

Does not work when formula has atoms with same characters in a row that below to different atoms, i.e. C10H21NNa.

isotopologue_count_percentile
287    @property
288    def isotopologue_count_percentile(
289        self,
290    ):
291        if not len(self.expected_isotopologues) == 0:
292            return (
293                len(self.mspeak_mf_isotopologues_indexes)
294                / len(self.expected_isotopologues)
295            ) * 100
296        else:
297            return 100
O_C
299    @property
300    def O_C(self):
301        if "O" in self._d_molecular_formula.keys():
302            # gather all the Os and Hs, regardless of the isotopic composition
303            Os = sum(
304                [
305                    self._d_molecular_formula.get(key)
306                    for key in ["O"] + Atoms.isotopes["O"][1]
307                    if key in self._d_molecular_formula.keys()
308                ]
309            )
310            Cs = sum(
311                [
312                    self._d_molecular_formula.get(key)
313                    for key in ["C"] + Atoms.isotopes["C"][1]
314                    if key in self._d_molecular_formula.keys()
315                ]
316            )
317            return Os / Cs
318        else:
319            return 0
H_C
321    @property
322    def H_C(self):
323        # gather all the Cs and Hs, regardless of the isotopic composition
324        Cs = sum(
325            [
326                self._d_molecular_formula.get(key)
327                for key in ["C"] + Atoms.isotopes["C"][1]
328                if key in self._d_molecular_formula.keys()
329            ]
330        )
331        Hs = sum(
332            [
333                self._d_molecular_formula.get(key)
334                for key in ["H"] + Atoms.isotopes["H"][1]
335                if key in self._d_molecular_formula.keys()
336            ]
337        )
338        return Hs / Cs
A_I
340    @property
341    def A_I(self):
342        """Aromaticity index"""
343        return self._calc_aromaticity_index()

Aromaticity index

A_I_mod
345    @property
346    def A_I_mod(self):
347        """Modified aromaticity index"""
348        return self._calc_aromaticity_index_mod()

Modified aromaticity index

nosc
350    @property
351    def nosc(self):
352        """Nominal oxidation state of carbon"""
353        return self._calc_nosc()

Nominal oxidation state of carbon

dbe
355    @property
356    def dbe(self):
357        return self._calc_dbe()
mz_nominal_calc
359    @property
360    def mz_nominal_calc(self):
361        return int(self._calc_mz())
mz_error
363    @property
364    def mz_error(self):
365        return self._calc_assignment_mass_error()
mz_calc
367    @property
368    def mz_calc(self):
369        return self._calc_mz()
protonated_mz
371    @property
372    def protonated_mz(self):
373        return self._protonated_mz(self.ion_charge)
radical_mz
375    @property
376    def radical_mz(self):
377        return self._radical_mz(self.ion_charge)
neutral_mass
379    @property
380    def neutral_mass(self):
381        return self._neutral_mass()
def adduct_mz(self, adduct_atom):
383    def adduct_mz(self, adduct_atom):
384        """Get m/z of an adducted ion version of the molecular formula.
385
386        Parameters
387        ----------
388        adduct_atom : str
389            The adduct atom.
390
391        Returns
392        -------
393        float
394            The m/z value of the adducted ion version of the molecular formula.
395        """
396        return self._adduct_mz(adduct_atom, self.ion_charge)

Get m/z of an adducted ion version of the molecular formula.

Parameters
  • adduct_atom (str): The adduct atom.
Returns
  • float: The m/z value of the adducted ion version of the molecular formula.
ion_type
398    @property
399    def ion_type(self):
400        ion_type = self._d_molecular_formula.get(Labels.ion_type)
401        if ion_type == Labels.protonated_de_ion:
402            if self.ion_charge > 0:
403                return Labels.protonated
404            else:
405                return Labels.de_protonated
406        else:
407            return ion_type
ion_charge
423    @property
424    def ion_charge(self):
425        return self._ion_charge
atoms
427    @property
428    def atoms(self):
429        """Get the atoms in the molecular formula."""
430        # if there is an adduct_atom, them reduce it from the atoms list
431        if self.adduct_atom is None:
432            return [
433                key
434                for key in self._d_molecular_formula.keys()
435                if key != Labels.ion_type
436            ]
437        else:
438            temp_dict = self._d_molecular_formula.copy()
439            temp_dict[self.adduct_atom] -= 1
440            return [
441                key
442                for key, val in temp_dict.items()
443                if key != Labels.ion_type and val > 0
444            ]

Get the atoms in the molecular formula.

confidence_score
446    @property
447    def confidence_score(self):
448        """Composite molecular formula assignment confidence score.
449
450        Combines formula-level mass accuracy and isotopologue pattern
451        agreement:
452
453        ```
454        CS = (w_err * m_err) + (w_iso * m_iso)
455        ```
456
457        where ``m_err`` is :attr:`average_mz_error_score`, ``m_iso`` is
458        :attr:`isotopologue_similarity`, and the weights are
459        ``molecular_search_settings.mz_error_score_weight`` (default 0.6)
460        and ``isotopologue_score_weight`` (default 0.4).
461
462        Returns
463        -------
464        float
465            Score typically in about [0, 1]; higher values indicate better
466            agreement with mass error and (when available) isotopologue
467            evidence. Used for ranking when ``score_method`` is
468            ``\"prob_score\"``.
469
470        Notes
471        -----
472        Requires an associated parent mass spectral peak. Isotopologue
473        terms are only informative when search was run with isotopologue
474        detection enabled. See the ``corems.molecular_formula`` package
475        documentation and Dewey et al., *Anal. Chem.* 2025, 97, 13031–13039
476        (https://doi.org/10.1021/acs.analchem.4c06826).
477        """
478        if not self._confidence_score:
479            self._confidence_score = self._calc_confidence_score()
480
481        return self._confidence_score

Composite molecular formula assignment confidence score.

Combines formula-level mass accuracy and isotopologue pattern agreement:

CS = (w_err * m_err) + (w_iso * m_iso)

where m_err is average_mz_error_score, m_iso is isotopologue_similarity, and the weights are molecular_search_settings.mz_error_score_weight (default 0.6) and isotopologue_score_weight (default 0.4).

Returns
  • float: Score typically in about [0, 1]; higher values indicate better agreement with mass error and (when available) isotopologue evidence. Used for ranking when score_method is "prob_score".
Notes

Requires an associated parent mass spectral peak. Isotopologue terms are only informative when search was run with isotopologue detection enabled. See the corems.molecular_formula package documentation and Dewey et al., Anal. Chem. 2025, 97, 13031–13039 (https://doi.org/10.1021/acs.analchem.4c06826).

isotopologue_similarity
483    @property
484    def isotopologue_similarity(self):
485        """Isotopologue abundance pattern similarity for this formula.
486
487        Compares expected natural-abundance isotopologue intensities
488        (from dynamic-range-limited expansion) to abundances of peaks
489        assigned as those partners. Implemented as a normalized Manhattan
490        distance mapped to a similarity in about [0, 1]
491        (higher is better).
492
493        Returns
494        -------
495        float
496            Similarity score. Returns 0.0 when no isotopologues are
497            expected for this monoisotopic formula (or none were computed).
498
499        Notes
500        -----
501        This is the ``m_iso`` term in :attr:`confidence_score`. See the
502        ``corems.molecular_formula`` package documentation for details.
503        """
504        if not self._isotopologue_similarity:
505            self._isotopologue_similarity = self._calc_isotopologue_confidence()
506
507        return self._isotopologue_similarity

Isotopologue abundance pattern similarity for this formula.

Compares expected natural-abundance isotopologue intensities (from dynamic-range-limited expansion) to abundances of peaks assigned as those partners. Implemented as a normalized Manhattan distance mapped to a similarity in about [0, 1] (higher is better).

Returns
  • float: Similarity score. Returns 0.0 when no isotopologues are expected for this monoisotopic formula (or none were computed).
Notes

This is the m_iso term in confidence_score. See the corems.molecular_formula package documentation for details.

average_mz_error_score
509    @property
510    def average_mz_error_score(self):
511        """Mean mass-accuracy score over mono and expected isotopologues.
512
513        Averages :attr:`mz_error_score` for the monoisotopic assignment and
514        for each expected isotopologue formula that has a parent peak.
515        Expected isotopologues without a matched peak contribute 0.
516
517        Returns
518        -------
519        float
520            Mean Gaussian mass-error score in about [0, 1].
521
522        Notes
523        -----
524        This is the ``m_err`` term in :attr:`confidence_score`.
525        """
526        # includes the isotopologues
527
528        if not self._mass_error_average_score:
529            self._mass_error_average_score = self._calc_average_mz_score()
530
531        return self._mass_error_average_score

Mean mass-accuracy score over mono and expected isotopologues.

Averages mz_error_score for the monoisotopic assignment and for each expected isotopologue formula that has a parent peak. Expected isotopologues without a matched peak contribute 0.

Returns
  • float: Mean Gaussian mass-error score in about [0, 1].
Notes

This is the m_err term in confidence_score.

mz_error_score
533    @property
534    def mz_error_score(self):
535        """Gaussian mass-accuracy score for this formula on its parent peak.
536
537        Uses the assignment mass error ``delta`` (ppm) and a width ``sigma``
538        equal to the parent peak's ``predicted_std`` when set, otherwise a
539        fallback of 1.66 ppm:
540
541        ```
542        m = exp( -((delta - mu) ** 2) / (2 * (sigma ** 2)) )
543        ```
544
545        with mean ``mu = 0`` (calibrated spectrum with near-zero mean error
546        assumed).
547
548        Returns
549        -------
550        float
551            Score typically in about 0 to 1; exact mass match is 1,
552            and larger ppm error yields values closer to 0.
553
554        Notes
555        -----
556        Formula-level ranking uses :attr:`average_mz_error_score`, which
557        folds in expected isotopologue peaks as well.
558        """
559        if not self._mz_error_score:
560            self._mz_error_score = self._calc_mz_confidence()
561
562        return self._mz_error_score

Gaussian mass-accuracy score for this formula on its parent peak.

Uses the assignment mass error delta (ppm) and a width sigma equal to the parent peak's predicted_std when set, otherwise a fallback of 1.66 ppm:

m = exp( -((delta - mu) ** 2) / (2 * (sigma ** 2)) )

with mean mu = 0 (calibrated spectrum with near-zero mean error assumed).

Returns
  • float: Score typically in about 0 to 1; exact mass match is 1, and larger ppm error yields values closer to 0.
Notes

Formula-level ranking uses average_mz_error_score, which folds in expected isotopologue peaks as well.

kmd
564    @property
565    def kmd(self):
566        return self._kmd
kendrick_mass
568    @property
569    def kendrick_mass(self):
570        return self._kendrick_mass
knm
572    @property
573    def knm(self):
574        return self._nominal_km
def change_kendrick_base(self, kendrick_dict_base):
576    def change_kendrick_base(self, kendrick_dict_base):
577        """Change the Kendrick base.
578
579        Parameters
580        ----------
581        kendrick_dict_base : dict
582            The Kendrick base dictionary. Ex: {"C": 1, "H": 2}
583        """
584        self._kmd, self._kendrick_mass, self._nominal_km = self._calc_kmd(
585            kendrick_dict_base
586        )

Change the Kendrick base.

Parameters
  • kendrick_dict_base (dict): The Kendrick base dictionary. Ex: {"C": 1, "H": 2}
def isotopologues(self, min_abundance, current_mono_abundance, dynamic_range):
588    def isotopologues(self, min_abundance, current_mono_abundance, dynamic_range):
589        """Calculate the isotopologues for a given molecular formula.
590
591        Parameters
592        ----------
593        min_abundance : float
594            The minimum abundance.
595        current_mono_abundance : float
596            The current monoisotopic abundance.
597        dynamic_range : float
598            The dynamic range.
599
600        Yields
601        ------
602        MolecularFormulaIsotopologue
603            The molecular formula isotopologue.
604
605        Notes
606        -----
607        This calculation ignores the hydrogen isotopes.
608        """
609        isotopologues = []
610        for mf in self._cal_isotopologues(
611            self._d_molecular_formula,
612            min_abundance,
613            current_mono_abundance,
614            dynamic_range,
615        ):
616            isotopologues.append(mf)
617
618        # To account for differences in how the isotopologue outputs are sorted between IsoSpec versions.
619        sorted_isotopologues = sorted(isotopologues, key=lambda mf: mf[1], reverse=True)
620
621        for mf in sorted_isotopologues:
622            yield MolecularFormulaIsotopologue(
623                *mf,
624                current_mono_abundance,
625                self.ion_charge,
626                ion_type=self.ion_type,
627                adduct_atom=self.adduct_atom,
628            )

Calculate the isotopologues for a given molecular formula.

Parameters
  • min_abundance (float): The minimum abundance.
  • current_mono_abundance (float): The current monoisotopic abundance.
  • dynamic_range (float): The dynamic range.
Yields
  • MolecularFormulaIsotopologue: The molecular formula isotopologue.
Notes

This calculation ignores the hydrogen isotopes.

def atoms_qnt(self, atom):
630    def atoms_qnt(self, atom):
631        """Get the atom quantity of a specific atom in the molecular formula."""
632        if atom in self._d_molecular_formula:
633            return self._d_molecular_formula.get(atom)
634        else:
635            raise Warning(
636                "Could not find %s in this Molecular Formula object" % str(atom)
637            )

Get the atom quantity of a specific atom in the molecular formula.

def atoms_symbol(self, atom):
639    def atoms_symbol(self, atom):
640        """Get the atom symbol without the mass number."""
641        return "".join([i for i in atom if not i.isdigit()])

Get the atom symbol without the mass number.

string
643    @property
644    def string(self):
645        """Returns the molecular formula as a string."""
646        if self._d_molecular_formula:
647            if self.adduct_atom is None:
648                mol_form_dict = self._d_molecular_formula
649            else:
650                mol_form_dict = self._d_molecular_formula.copy()
651                if self.adduct_atom not in mol_form_dict.keys():
652                    raise Exception("Adduct atom not found in molecular formula dict")
653                mol_form_dict[self.adduct_atom] -= 1
654                mol_form_dict = {
655                    key: val for key, val in mol_form_dict.items() if val != 0
656                }
657            formula_srt = ""
658            for atom in Atoms.atoms_order:
659                if atom in mol_form_dict.keys():
660                    formula_srt += atom + str(int(mol_form_dict.get(atom))) + " "
661            return formula_srt.strip()
662
663        else:
664            raise Exception("Molecular formula identification not performed yet")

Returns the molecular formula as a string.

string_formated
666    @property
667    def string_formated(self):
668        SUB = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
669        SUP = str.maketrans("0123456789", "⁰¹²³⁴⁵⁶⁷⁸⁹")
670
671        if self._d_molecular_formula:
672            formula_srt = ""
673            for atom in Atoms.atoms_order:
674                if atom in self.to_dict().keys():
675                    formula_srt += atom.translate(SUP) + str(
676                        int(self.to_dict().get(atom))
677                    ).translate(SUB)
678            return formula_srt
679
680        else:
681            raise Exception("Molecular formula identification not performed yet")
def to_dict(self):
683    def to_dict(self):
684        """Returns the molecular formula as a dictionary.
685
686        Returns
687        -------
688        dict
689            The molecular formula as a dictionary.
690        """
691        return self._d_molecular_formula

Returns the molecular formula as a dictionary.

Returns
  • dict: The molecular formula as a dictionary.
def to_list(self):
693    def to_list(self):
694        """Returns the molecular formula as a list.
695
696        Returns
697        -------
698        list
699            The molecular formula as a list.
700
701        Raises
702        ------
703        Exception
704            If the molecular formula identification was not performed yet.
705        """
706        # TODO ensure self._d_molecular_formula is a orderedDict
707
708        if self._d_molecular_formula:
709            formula_list = []
710
711            for atom, atom_number in self._d_molecular_formula.items():
712                if atom != Labels.ion_type:
713                    formula_list.append(atom)
714                    formula_list.append(atom_number)
715
716            return formula_list
717        else:
718            raise Exception("Molecular formula identification not performed yet")

Returns the molecular formula as a list.

Returns
  • list: The molecular formula as a list.
Raises
  • Exception: If the molecular formula identification was not performed yet.
class_label
720    @property
721    def class_label(self):
722        if self._d_molecular_formula:
723            formulalist = self.to_list()
724            classstring = ""
725
726            for each in range(0, len(formulalist), 2):
727                if (
728                    formulalist[each] != "C"
729                    and formulalist[each] != "H"
730                    and formulalist[each] != "HC"
731                ):
732                    classstring = (
733                        classstring
734                        + str(formulalist[each])
735                        + str(formulalist[each + 1])
736                        + " "
737                    )
738
739            if classstring == "":
740                classstring = "HC"
741
742            classstring = classstring.strip()
743
744            if self._d_molecular_formula.get(Labels.ion_type) == Labels.radical_ion:
745                return classstring + " -R"
746
747            # elif self._d_molecular_formula.get(Labels.ion_type) == Labels.adduct_ion:
748
749            #    return classstring + ' -A'
750
751            else:
752                return classstring
753
754            #'dict, tuple or string'
755
756        else:
757            raise Exception("Molecular formula identification not performed yet")
class_dict
759    @property
760    def class_dict(self):
761        if self._d_molecular_formula:
762            class_dict = {}
763
764            for atom, qnt in self._d_molecular_formula.items():
765                if atom != Labels.ion_type and atom != "C" and atom != "H":
766                    class_dict[atom] = qnt
767
768            return class_dict
769
770        raise Exception("Molecular formula identification not performed yet")
class MolecularFormulaIsotopologue(MolecularFormulaBase):
773class MolecularFormulaIsotopologue(MolecularFormulaBase):
774    """Class for representing a molecular formula isotopologue.
775
776    Parameters
777    ----------
778    _d_molecular_formula : dict
779        The molecular formula as a dictionary.
780    prob_ratio : float
781        The probability ratio.
782    mono_abundance : float
783        The monoisotopic abundance.
784    ion_charge : int
785        The ion charge.
786    mspeak_parent : object, optional
787        The parent mass spectrum peak object instance. Defaults to None.
788    ion_type : str, optional
789        The ion type. Defaults to None.
790    adduct_atom : str, optional
791        The adduct atom. Defaults to None.
792
793    Attributes
794    ----------
795    prob_ratio : float
796        The probability ratio.
797    abundance_calc : float
798        The calculated abundance.
799    area_error : float
800        The area error.
801    abundance_error : float
802        The abundance error.
803    is_isotopologue : bool
804        The isotopologue flag. Defaults to True.
805    mspeak_index_mono_isotopic : int
806        The index of the monoisotopic peak in the mass spectrum peak list. Defaults to None.
807    mono_isotopic_formula_index : int
808        The index of the monoisotopic formula in the molecular formula list. Defaults to None.
809    """
810
811    def __init__(
812        self,
813        _d_molecular_formula,
814        prob_ratio,
815        mono_abundance,
816        ion_charge,
817        mspeak_parent=None,
818        ion_type=None,
819        adduct_atom=None,
820    ):
821        if ion_type is None:
822            # check if ion type or adduct_atom is in the molecular formula dict
823            if Labels.ion_type in _d_molecular_formula:
824                ion_type = _d_molecular_formula.get(Labels.ion_type)
825            else:
826                ion_type = None
827        else:
828            ion_type = Labels.ion_type_translate.get(ion_type)
829
830        if ion_type == Labels.adduct_ion:
831            adduct_atom_int = None
832            if adduct_atom in _d_molecular_formula.keys():
833                adduct_atom_int = adduct_atom
834            else:
835                # Check to see if adduct_atom should actually be an isotope of the adduct atom
836                for adduct_iso in Atoms.isotopes.get(adduct_atom)[1]:
837                    if adduct_iso in _d_molecular_formula.keys():
838                        adduct_atom_int = adduct_iso
839            adduct_atom = adduct_atom_int
840            if adduct_atom is None:
841                raise Exception("adduct_atom is required for adduct ion")
842            _d_molecular_formula[adduct_atom] -= 1
843            _d_molecular_formula = {
844                key: val for key, val in _d_molecular_formula.items() if val != 0
845            }
846
847        super().__init__(
848            molecular_formula=_d_molecular_formula,
849            ion_charge=ion_charge,
850            ion_type=ion_type,
851            adduct_atom=adduct_atom,
852        )
853        # prob_ratio is relative to the monoisotopic peak p_isotopologue/p_mono_isotopic
854
855        self.prob_ratio = prob_ratio
856
857        self.abundance_calc = mono_abundance * prob_ratio
858
859        self.is_isotopologue = True
860
861        self.mspeak_index_mono_isotopic = None
862
863        self.mono_isotopic_formula_index = None
864        # parent mass spectrum peak obj instance
865        self._mspeak_parent = mspeak_parent
866
867    @property
868    def area_error(self):
869        return self._calc_area_error()
870
871    @property
872    def abundance_error(self):
873        return self._calc_abundance_error()

Class for representing a molecular formula isotopologue.

Parameters
  • _d_molecular_formula (dict): The molecular formula as a dictionary.
  • prob_ratio (float): The probability ratio.
  • mono_abundance (float): The monoisotopic abundance.
  • ion_charge (int): The ion charge.
  • mspeak_parent (object, optional): The parent mass spectrum peak object instance. Defaults to None.
  • ion_type (str, optional): The ion type. Defaults to None.
  • adduct_atom (str, optional): The adduct atom. Defaults to None.
Attributes
  • prob_ratio (float): The probability ratio.
  • abundance_calc (float): The calculated abundance.
  • area_error (float): The area error.
  • abundance_error (float): The abundance error.
  • is_isotopologue (bool): The isotopologue flag. Defaults to True.
  • mspeak_index_mono_isotopic (int): The index of the monoisotopic peak in the mass spectrum peak list. Defaults to None.
  • mono_isotopic_formula_index (int): The index of the monoisotopic formula in the molecular formula list. Defaults to None.
MolecularFormulaIsotopologue( _d_molecular_formula, prob_ratio, mono_abundance, ion_charge, mspeak_parent=None, ion_type=None, adduct_atom=None)
811    def __init__(
812        self,
813        _d_molecular_formula,
814        prob_ratio,
815        mono_abundance,
816        ion_charge,
817        mspeak_parent=None,
818        ion_type=None,
819        adduct_atom=None,
820    ):
821        if ion_type is None:
822            # check if ion type or adduct_atom is in the molecular formula dict
823            if Labels.ion_type in _d_molecular_formula:
824                ion_type = _d_molecular_formula.get(Labels.ion_type)
825            else:
826                ion_type = None
827        else:
828            ion_type = Labels.ion_type_translate.get(ion_type)
829
830        if ion_type == Labels.adduct_ion:
831            adduct_atom_int = None
832            if adduct_atom in _d_molecular_formula.keys():
833                adduct_atom_int = adduct_atom
834            else:
835                # Check to see if adduct_atom should actually be an isotope of the adduct atom
836                for adduct_iso in Atoms.isotopes.get(adduct_atom)[1]:
837                    if adduct_iso in _d_molecular_formula.keys():
838                        adduct_atom_int = adduct_iso
839            adduct_atom = adduct_atom_int
840            if adduct_atom is None:
841                raise Exception("adduct_atom is required for adduct ion")
842            _d_molecular_formula[adduct_atom] -= 1
843            _d_molecular_formula = {
844                key: val for key, val in _d_molecular_formula.items() if val != 0
845            }
846
847        super().__init__(
848            molecular_formula=_d_molecular_formula,
849            ion_charge=ion_charge,
850            ion_type=ion_type,
851            adduct_atom=adduct_atom,
852        )
853        # prob_ratio is relative to the monoisotopic peak p_isotopologue/p_mono_isotopic
854
855        self.prob_ratio = prob_ratio
856
857        self.abundance_calc = mono_abundance * prob_ratio
858
859        self.is_isotopologue = True
860
861        self.mspeak_index_mono_isotopic = None
862
863        self.mono_isotopic_formula_index = None
864        # parent mass spectrum peak obj instance
865        self._mspeak_parent = mspeak_parent
prob_ratio
abundance_calc
is_isotopologue
mspeak_index_mono_isotopic
mono_isotopic_formula_index
area_error
867    @property
868    def area_error(self):
869        return self._calc_area_error()
abundance_error
871    @property
872    def abundance_error(self):
873        return self._calc_abundance_error()
class LCMSLibRefMolecularFormula:
876class LCMSLibRefMolecularFormula:
877    """Removed LC-MS library reference molecular formula type.
878
879    This class is no longer supported. Canonical LC-MS MS1 formula assignment
880    uses :class:`MolecularFormula` via combinatorial / SQL search. Construction
881    raises :class:`NotImplementedError`. The name is retained as a stub until
882    the next major release.
883
884    Parameters
885    ----------
886    *args
887        Ignored; retained only for call-site compatibility.
888    **kwargs
889        Ignored; retained only for call-site compatibility.
890    """
891
892    def __init__(self, *args, **kwargs) -> None:
893        raise NotImplementedError(
894            "LCMSLibRefMolecularFormula is no longer supported. "
895            "Use MolecularFormula for formula objects. "
896            "This stub will be removed in the next major release."
897        )

Removed LC-MS library reference molecular formula type.

This class is no longer supported. Canonical LC-MS MS1 formula assignment uses MolecularFormula via combinatorial / SQL search. Construction raises NotImplementedError. The name is retained as a stub until the next major release.

Parameters
  • *args: Ignored; retained only for call-site compatibility.
  • **kwargs: Ignored; retained only for call-site compatibility.
LCMSLibRefMolecularFormula(*args, **kwargs)
892    def __init__(self, *args, **kwargs) -> None:
893        raise NotImplementedError(
894            "LCMSLibRefMolecularFormula is no longer supported. "
895            "Use MolecularFormula for formula objects. "
896            "This stub will be removed in the next major release."
897        )
class MolecularFormula(MolecularFormulaBase):
900class MolecularFormula(MolecularFormulaBase):
901    """General class for representing a molecular formula.
902
903    Parameters
904    ----------
905    molecular_formula : dict, list, str
906        The molecular formula.
907    ion_charge : int
908        The ion charge.
909    ion_type : str, optional
910        The ion type. Defaults to None.
911    adduct_atom : str, optional
912        The adduct atom. Defaults to None.
913    mspeak_parent : object, optional
914        The parent mass spectrum peak object instance. Defaults to None.
915    external_mz : float, optional
916        The external m/z value. Defaults to False.
917    """
918
919    def __init__(
920        self,
921        molecular_formula,
922        ion_charge,
923        ion_type=None,
924        adduct_atom=None,
925        mspeak_parent=None,
926        external_mz=False,
927    ):
928        super().__init__(
929            molecular_formula,
930            ion_charge,
931            ion_type=ion_type,
932            adduct_atom=adduct_atom,
933            mspeak_parent=mspeak_parent,
934            external_mz=external_mz,
935        )

General class for representing a molecular formula.

Parameters
  • molecular_formula (dict, list, str): The molecular formula.
  • ion_charge (int): The ion charge.
  • ion_type (str, optional): The ion type. Defaults to None.
  • adduct_atom (str, optional): The adduct atom. Defaults to None.
  • mspeak_parent (object, optional): The parent mass spectrum peak object instance. Defaults to None.
  • external_mz (float, optional): The external m/z value. Defaults to False.
MolecularFormula( molecular_formula, ion_charge, ion_type=None, adduct_atom=None, mspeak_parent=None, external_mz=False)
919    def __init__(
920        self,
921        molecular_formula,
922        ion_charge,
923        ion_type=None,
924        adduct_atom=None,
925        mspeak_parent=None,
926        external_mz=False,
927    ):
928        super().__init__(
929            molecular_formula,
930            ion_charge,
931            ion_type=ion_type,
932            adduct_atom=adduct_atom,
933            mspeak_parent=mspeak_parent,
934            external_mz=external_mz,
935        )