corems.molecular_id.factory.classification

  1__author__ = "Yuri E. Corilo"
  2__date__ = "Jan 31, 2020"
  3
  4from collections.abc import Mapping
  5
  6from matplotlib import pyplot as plt
  7from numpy import linspace
  8
  9from corems.encapsulation.constant import Atoms, Labels
 10
 11flatten_list = lambda l: [item for sublist in l for item in sublist]
 12
 13
 14class HeteroatomsClassification(Mapping):
 15    """Class for grouping mass spectrum data by heteroatom classes (Nn, Oo, Ss, NnOo, NnSs, etc..)
 16
 17    Parameters
 18    ----------
 19    mass_spectrum : MassSpectrum
 20        The mass spectrum object.
 21    choose_molecular_formula : bool, optional
 22        If True, the molecular formula with the highest abundance is chosen. If False, all molecular formulas are considered. Default is True.
 23
 24    Raises
 25    ------
 26    Exception
 27        If no molecular formula is associated with any mspeak objects.
 28
 29    Attributes
 30    ----------
 31    _ms_grouped_class : dict
 32        A dictionary of classes and a list of ms_peak objects.
 33    choose_mf : bool
 34        If True, the molecular formula with the highest abundance is chosen. If False, all molecular formulas are considered.
 35    total_peaks : int
 36        The total number of peaks.
 37    sum_abundance : float
 38        The sum of the abundance of all peaks.
 39    min_max_mz : tuple
 40        The minimum and maximum mz values.
 41    min_max_abundance : tuple
 42        The minimum and maximum abundance values.
 43    min_ppm_error : float
 44        The minimum ppm error.
 45    max_ppm_error : float
 46        The maximum ppm error.
 47    all_identified_atoms : list
 48        A list of all identified atoms.
 49
 50    Methods
 51    -------
 52    * __len__().
 53        Returns the number of classes.
 54    * __getitem__(classe)
 55        Returns the molecular formula list for specified class.
 56    * __iter__().
 57        Returns an iterator over the keys of the dictionary.
 58    * get_classes(threshold_perc=1, isotopologue=True).
 59        Returns a list of classes with abundance percentile above threshold.
 60    * molecular_formula_string(classe).
 61        Returns a list of molecular formula string for specified class.
 62    * molecular_formula(classe).
 63        Returns a list of molecular formula for specified class.
 64    * carbon_number(classe).
 65        Returns a list of carbon number for specified class.
 66    * atom_count(atom, classe).
 67        Returns a list of atom count for specified class.
 68    * dbe(classe).
 69        Returns a list of DBE for specified class.
 70    * atoms_ratio(classe, numerator, denominator).
 71        Returns a list of atoms ratio for specified class.
 72    * mz_exp(classe).
 73        Returns a list of experimental mz for specified class.
 74    * abundance(classe).
 75        Returns a list of abundance for specified class.
 76    * mz_error(classe).
 77        Returns a list of mz error for specified class.
 78    * mz_calc(classe).
 79        Returns a list of calculated mz for specified class.
 80    * peaks_count_percentile(classe).
 81        Returns the peaks count percentile of a specific class.
 82    * abundance_count_percentile(classe).
 83        Returns the abundance percentile of a specific class.
 84    * mz_exp_assigned().
 85        Returns a list of experimental mz for all assigned classes.
 86    * abundance_assigned().
 87        Returns a list of abundance for all classes.
 88    * mz_exp_all().
 89        Returns a list of mz for all classes.
 90
 91    """
 92
 93    # Group mass spectrum data by heteroatom classes (Nn, Oo, Ss, NnOo, NnSs, etc..)
 94
 95    #   class obj behaves as a dictionary of classes and return a list of ms_peak obj
 96
 97    def __init__(self, mass_spectrum, choose_molecular_formula=True):
 98        def sort_atoms_method(atom):
 99            """Sort atoms by order of appearance in the Atoms class"""
100            return [Atoms.atoms_order.index(atom)]
101
102        self._ms_grouped_class = dict()
103
104        self.choose_mf = choose_molecular_formula
105
106        # mapping for ms peaks without any molecular formula associated
107        self._ms_grouped_class[Labels.unassigned] = list()
108
109        self.total_peaks = 0
110
111        self.sum_abundance = 0
112
113        self.min_max_mz = (mass_spectrum.min_mz_exp, mass_spectrum.max_mz_exp)
114
115        self.min_max_abundance = (
116            mass_spectrum.min_abundance,
117            mass_spectrum.max_abundance,
118        )
119
120        self.min_ppm_error = mass_spectrum.molecular_search_settings.min_ppm_error
121
122        self.max_ppm_error = mass_spectrum.molecular_search_settings.max_ppm_error
123
124        check_assign = False
125
126        all_used_atoms = set()
127
128        for ms_peak in mass_spectrum:
129            self.total_peaks += 1
130
131            self.sum_abundance += ms_peak.abundance
132
133            if not ms_peak.is_assigned:
134                self._ms_grouped_class.get(Labels.unassigned).append(ms_peak)
135
136            else:
137                check_assign = True
138
139                if choose_molecular_formula:
140                    mf = ms_peak.best_molecular_formula_candidate
141
142                    classes = [mf.class_label]
143
144                    for atom in mf.atoms:
145                        all_used_atoms.add(atom)
146
147                else:
148                    classes = []
149
150                    for mf in ms_peak:
151                        classes.append(mf.class_label)
152
153                        for atom in mf.atoms:
154                            all_used_atoms.add(atom)
155
156                for classe in classes:
157                    if classe in self._ms_grouped_class.keys():
158                        self._ms_grouped_class.get(classe).append(ms_peak)
159
160                    else:
161                        self._ms_grouped_class[classe] = [ms_peak]
162
163        self.all_identified_atoms = sorted(all_used_atoms, key=sort_atoms_method)
164
165        if not check_assign:
166            raise Exception("No molecular formula associated with any mspeak objects")
167
168    def __len__(self):
169        """Return the number of classes"""
170        return len(self._ms_grouped_class)
171
172    def __getitem__(self, classe):
173        """Return the molecular formula list for specified class"""
174        return self._ms_grouped_class.get(classe)
175
176    def __iter__(self):
177        """Return an iterator over the keys of the dictionary."""
178        return iter(self._ms_grouped_class)
179
180    def get_classes(self, threshold_perc=1, isotopologue=True):
181        """Return a list of classes with abundance percentile above threshold"""
182        classes = list()
183        for classe in self.keys():
184            if classe != Labels.unassigned:
185                if self.abundance_count_percentile(classe) > threshold_perc:
186                    if classe != Labels.unassigned:
187                        # access first molecular formula inside the first ms peak and check isotopologue
188                        if not isotopologue and self.get(classe)[0][0].is_isotopologue:
189                            continue
190
191                    classes.append(classe)
192        # TODO sort classes chemically here too
193        return classes
194
195    def molecular_formula_string(
196        self,
197        classe,
198    ):
199        """Return a list of molecular formula string for specified class"""
200        if self.choose_mf:
201            return [mspeak.best_molecular_formula_candidate for mspeak in self[classe]]
202        else:
203            return [
204                mf
205                for mspeak in self[classe]
206                for mf in mspeak
207                if mf.class_label == classe
208            ]
209
210    def molecular_formula(
211        self,
212        classe,
213    ):
214        """Return a list of molecular formula for specified class"""
215        if self.choose_mf:
216            return [mspeak.best_molecular_formula_candidate for mspeak in self[classe]]
217        else:
218            return [
219                mf
220                for mspeak in self[classe]
221                for mf in mspeak
222                if mf.class_label == classe
223            ]
224
225    def carbon_number(self, classe):
226        """Return a list of carbon number for specified class"""
227        if self.choose_mf:
228            return [
229                mspeak.best_molecular_formula_candidate.get("C")
230                for mspeak in self[classe]
231            ]
232        else:
233            return [
234                mf.get("C")
235                for mspeak in self[classe]
236                for mf in mspeak
237                if mf.class_label == classe
238            ]
239
240    def atom_count(self, atom, classe):
241        """Return a list of atom count for specified class"""
242
243        if self.choose_mf:
244            return [
245                mspeak.best_molecular_formula_candidate.get(atom)
246                for mspeak in self[classe]
247            ]
248        else:
249            return [
250                mf.get(atom)
251                for mspeak in self[classe]
252                for mf in mspeak
253                if mf.class_label == classe
254            ]
255
256    def dbe(self, classe):
257        """Return a list of DBE for specified class"""
258        if self.choose_mf:
259            return [
260                mspeak.best_molecular_formula_candidate.dbe for mspeak in self[classe]
261            ]
262        else:
263            return [
264                mf.dbe
265                for mspeak in self[classe]
266                for mf in mspeak
267                if mf.class_label == classe
268            ]
269
270    def atoms_ratio(self, classe, numerator, denominator):
271        """Return a list of atoms ratio for specified class"""
272        return [
273            mf.get(numerator) / mf.get(denominator)
274            for mf in self.molecular_formula(classe)
275        ]
276
277    def mz_exp(self, classe):
278        """Return a list of experimental mz for specified class"""
279        if self.choose_mf or classe == Labels.unassigned:
280            return [mspeak.mz_exp for mspeak in self[classe]]
281
282        else:
283            return [
284                mspeak.mz_exp
285                for mspeak in self[classe]
286                for mf in mspeak
287                if mf.class_label == classe
288            ]
289
290    def abundance(self, classe):
291        """Return a list of abundance for specified class"""
292        if self.choose_mf or classe == Labels.unassigned:
293            return [mspeak.abundance for mspeak in self[classe]]
294
295        else:
296            return [
297                mspeak.abundance
298                for mspeak in self[classe]
299                for mf in mspeak
300                if mf.class_label == classe
301            ]
302
303    def mz_error(self, classe):
304        """Return a list of mz error for specified class"""
305        if classe != Labels.unassigned:
306            if self.choose_mf:
307                return [
308                    mspeak.best_molecular_formula_candidate.mz_error
309                    for mspeak in self[classe]
310                ]
311
312            else:
313                return [
314                    mf.mz_error
315                    for mspeak in self[classe]
316                    for mf in mspeak
317                    if mf.class_label == classe
318                ]
319
320    def mz_calc(self, classe):
321        """Return a list of calculated mz for specified class"""
322        if self.choose_mf:
323            return [
324                mspeak.best_molecular_formula_candidate.mz_calc
325                for mspeak in self[classe]
326            ]
327
328        else:
329            return [
330                mf.mz_calc
331                for mspeak in self[classe]
332                for mf in mspeak
333                if mf.class_label == classe
334            ]
335
336    def peaks_count_percentile(self, classe):
337        """Return the peaks count percentile of a specific class"""
338        return (len(self[classe]) / self.total_peaks) * 100
339
340    def abundance_count_percentile(self, classe):
341        """Return the abundance percentile of a specific class"""
342        return (
343            sum([mspeak.abundance for mspeak in self[classe]]) / self.sum_abundance
344        ) * 100
345
346    def mz_exp_assigned(self):
347        """Return a list of experimental mz for all assigned classes"""
348        classes = self.keys()
349
350        return [
351            mspeak.mz_exp
352            for classe in classes
353            for mspeak in self[classe]
354            if classe != Labels.unassigned
355        ]
356
357    def abundance_assigned(self):
358        """Return a list of abundance for all classes"""
359        classes = self.keys()
360
361        return [
362            mspeak.abundance
363            for classe in classes
364            for mspeak in self[classe]
365            if classe != Labels.unassigned
366        ]
367
368    def mz_exp_all(self):
369        """Return a list of mz for all classes"""
370        classes = self.keys()
371
372        return flatten_list(
373            [self.mz_exp(classe) for classe in classes if classe != Labels.unassigned]
374        )
375
376    def mz_error_all(self):
377        """Return a list of mz error for all classes"""
378        classes = self.keys()
379
380        return flatten_list(
381            [self.mz_error(classe) for classe in classes if classe != Labels.unassigned]
382        )
383
384    def carbon_number_all(self):
385        """Return a list of carbon number for all classes"""
386        classes = self.keys()
387
388        return flatten_list(
389            [
390                self.carbon_number(classe)
391                for classe in classes
392                if classe != Labels.unassigned
393            ]
394        )
395
396    def dbe_all(self):
397        """Return a list of DBE for all classes"""
398        classes = self.keys()
399
400        return flatten_list(
401            [self.dbe(classe) for classe in classes if classe != Labels.unassigned]
402        )
403
404    def atoms_ratio_all(self, numerator, denominator):
405        """Return a list of atoms ratio for all classes"""
406        classes = self.keys()
407
408        return flatten_list(
409            [
410                self.atoms_ratio(classe, numerator, denominator)
411                for classe in classes
412                if classe != Labels.unassigned
413            ]
414        )
415
416    def to_dataframe(
417        self,
418        include_isotopologue=False,
419        abundance_perc_threshold=5,
420        include_unassigned=False,
421    ):
422        """Return a pandas dataframe with all the data from the class
423
424        Parameters
425        ----------
426        include_isotopologue : bool, optional
427            Include isotopologues, by default False
428        abundance_perc_threshold : int, optional
429            Abundance percentile threshold, by default 5
430        include_unassigned : bool, optional
431            Include unassigned peaks, by default False
432
433        Returns
434        -------
435        DataFrame
436            Pandas dataframe with all the data from the class
437        """
438        from pandas import DataFrame
439
440        columns_labels = [
441            "mz",
442            "calibrated_mz",
443            "calculated_m_z",
444            "abundance",
445            "resolving_power",
446            "sn",
447            "ion_charge",
448            "mass_error",
449            "DBE",
450            "class",
451            "HC",
452            "OC",
453            "ion_type",
454            "is_isotopologue",
455            "class_abundance",
456            "class_count",
457        ]
458
459        dict_data_list = []
460
461        for classe, list_mspeaks in self.items():
462            percent_abundance = self.abundance_count_percentile(classe)
463
464            # ignores low abundant classes
465            if abundance_perc_threshold < abundance_perc_threshold:
466                continue
467
468            peaks_count_percentile = self.peaks_count_percentile(classe)
469
470            for ms_peak in list_mspeaks:
471                if ms_peak.is_assigned:
472                    for m_formula in ms_peak:
473                        # ignores isotopologues
474                        if not include_isotopologue and m_formula.is_isotopologue:
475                            continue
476
477                        formula_dict = m_formula.to_dict()
478
479                        dict_result = {
480                            "mz": ms_peak._mz_exp,
481                            "calibrated_mz": ms_peak.mz_exp,
482                            "calculated_mz": m_formula.mz_calc,
483                            "abundance": ms_peak.abundance,
484                            "resolving_power": ms_peak.resolving_power,
485                            "sn": ms_peak.signal_to_noise,
486                            # Assigned: formula charge (may be multi-charge), not peak polarity
487                            "ion_charge": m_formula.ion_charge,
488                            "mass_error": m_formula.mz_error,
489                            "DBE": m_formula.dbe,
490                            "class": classe,
491                            "HC": m_formula.H_C,
492                            "OC": m_formula.O_C,
493                            "ion_type": str(m_formula.ion_type.lower().encode("utf-8")),
494                            "is_isotopologue": int(m_formula.is_isotopologue),
495                            "class_abundance": percent_abundance,
496                            "class_count": peaks_count_percentile,
497                        }
498
499                        for atom in formula_dict.keys():
500                            dict_result[atom] = formula_dict.get(atom)
501
502                    dict_data_list.append(dict_result)
503
504                else:
505                    if not include_unassigned:
506                        continue
507
508                    dict_result = {
509                        "mz": ms_peak._mz_exp,
510                        "calibrated_mz": ms_peak.mz_exp,
511                        "abundance": ms_peak.abundance,
512                        "resolving_power": ms_peak.resolving_power,
513                        "sn": ms_peak.signal_to_noise,
514                        # Peak polarity (±1); no formula charge for unassigned peaks
515                        "ion_charge": ms_peak.polarity,
516                        "class": classe,
517                        "class_abundance": percent_abundance,
518                        "class_count": percent_abundance,
519                    }
520
521                    dict_data_list.append(dict_result)
522
523        columns = columns_labels + self.all_identified_atoms
524
525        return DataFrame(dict_data_list, columns=columns)
526
527    def plot_ms_assigned_unassigned(self, assigned_color="b", unassigned_color="r"):
528        """Plot stick mass spectrum of all classes
529
530        Parameters
531        ----------
532        assigned_color : str, optional
533            Matplotlib color for the assigned peaks, by default "b"
534        unassigned_color : str, optional
535            Matplotlib color for the unassigned peaks, by default "r"
536
537        Returns
538        -------
539        ax : matplotlib.axes
540            Matplotlib axes object
541        """
542        mz_assigned = self.mz_exp_assigned()
543        abundance_assigned = self.abundance_assigned()
544
545        mz_not_assigned = self.mz_exp(Labels.unassigned)
546        abundance_not_assigned = self.abundance(Labels.unassigned)
547
548        ax = plt.gca()
549
550        for plot_obj in ax.stem(
551            mz_assigned,
552            abundance_assigned,
553            linefmt="-",
554            markerfmt=" ",
555            label="Assigned",
556        ):
557            plt.setp(plot_obj, "color", assigned_color, "linewidth", 2)
558
559        for plot_obj in ax.stem(
560            mz_not_assigned,
561            abundance_not_assigned,
562            linefmt="-",
563            markerfmt=" ",
564            label="Unassigned",
565        ):
566            plt.setp(plot_obj, "color", unassigned_color, "linewidth", 2)
567
568        ax.set_xlabel("$\t{m/z}$", fontsize=12)
569        ax.set_ylabel("Abundance", fontsize=12)
570        ax.tick_params(axis="both", which="major", labelsize=12)
571
572        ax.axes.spines["top"].set_visible(False)
573        ax.axes.spines["right"].set_visible(False)
574
575        ax.get_yaxis().set_visible(False)
576        ax.spines["left"].set_visible(False)
577        plt.legend()
578
579        return ax
580
581    def plot_mz_error(self, color="g"):
582        """Plot mz error scatter plot of all classes
583
584        Parameters
585        ----------
586        color : str, optional
587            Matplotlib color, by default "g"
588
589        Returns
590        -------
591        ax : matplotlib.axes
592            Matplotlib axes object
593        """
594        ax = plt.gca()
595
596        mz_assigned = self.mz_exp_all()
597        mz_error = self.mz_error_all()
598
599        ax.scatter(mz_assigned, mz_error, c=color)
600
601        ax.set_xlabel("$\t{m/z}$", fontsize=12)
602        ax.set_ylabel("Error (ppm)", fontsize=12)
603        ax.tick_params(axis="both", which="major", labelsize=12)
604
605        ax.axes.spines["top"].set_visible(True)
606        ax.axes.spines["right"].set_visible(True)
607
608        ax.get_yaxis().set_visible(True)
609        ax.spines["left"].set_visible(True)
610
611        ax.set_xlim(self.min_max_mz)
612        ax.set_ylim(self.min_ppm_error, self.max_ppm_error)
613
614        return ax
615
616    def plot_mz_error_class(self, classe, color="g"):
617        """Plot mz error scatter plot of a specific class
618
619        Parameters
620        ----------
621        classe : str
622            Class name
623        color : str, optional
624            Matplotlib color, by default "g"
625
626        Returns
627        -------
628        ax : matplotlib.axes
629            Matplotlib axes object
630
631        """
632        if classe != Labels.unassigned:
633            ax = plt.gca()
634
635            abun_perc = self.abundance_count_percentile(classe)
636            mz_assigned = self.mz_exp(classe)
637            mz_error = self.mz_error(classe)
638
639            ax.scatter(mz_assigned, mz_error, c=color)
640
641            title = "%s, %.2f %%" % (classe, abun_perc)
642            ax.set_title(title)
643            ax.set_xlabel("$\t{m/z}$", fontsize=12)
644            ax.set_ylabel("Error (ppm)", fontsize=12)
645            ax.tick_params(axis="both", which="major", labelsize=12)
646
647            ax.axes.spines["top"].set_visible(True)
648            ax.axes.spines["right"].set_visible(True)
649
650            ax.get_yaxis().set_visible(True)
651            ax.spines["left"].set_visible(True)
652
653            ax.set_xlim(self.min_max_mz)
654            ax.set_ylim(self.min_ppm_error, self.max_ppm_error)
655
656            return ax
657
658    def plot_ms_class(self, classe, color="g"):
659        """Plot stick mass spectrum of a specific class
660
661        Parameters
662        ----------
663        classe : str
664            Class name
665        color : str, optional
666            Matplotlib color, by default "g"
667
668        Returns
669        -------
670        ax : matplotlib.axes
671            Matplotlib axes object
672
673        """
674        if classe != Labels.unassigned:
675            ax = plt.gca()
676
677            abun_perc = self.abundance_count_percentile(classe)
678            mz_assigned = self.mz_exp(classe)
679            abundance_assigned = self.abundance(classe)
680
681            for plot_obj in ax.stem(
682                mz_assigned, abundance_assigned, linefmt="-", markerfmt=" "
683            ):
684                plt.setp(plot_obj, "color", color, "linewidth", 2)
685
686            title = "%s, %.2f %%" % (classe, abun_perc)
687            ax.set_title(title)
688            ax.set_xlabel("$\t{m/z}$", fontsize=12)
689            ax.set_ylabel("Abundance", fontsize=12)
690            ax.tick_params(axis="both", which="major", labelsize=12)
691
692            ax.axes.spines["top"].set_visible(False)
693            ax.axes.spines["right"].set_visible(False)
694
695            ax.get_yaxis().set_visible(False)
696            ax.spines["left"].set_visible(False)
697
698            ax.set_xlim(self.min_max_mz)
699            ax.set_ylim(self.min_max_abundance)
700
701            return ax
702
703    def plot_van_krevelen(
704        self,
705        classe=None,
706        max_hc=2.5,
707        max_oc=2,
708        ticks_number=5,
709        color="viridis",
710        alpha=0.5,
711        log_abundance=False,
712    ):
713        """Plot Van Krevelen Diagram for a single class or all assigned classes
714
715        Parameters
716        ----------
717        classe : str, optional
718            Class name or None to plot all assigned classes, by default None
719        max_hc : float, optional
720            Max H/C ratio, by default 2.5
721        max_oc : float, optional
722            Max O/C ratio, by default 2
723        ticks_number : int, optional
724            Number of ticks, by default 5
725        color : str, optional
726            Matplotlib color/colormap, by default "viridis"
727        alpha : float, optional
728            Transparency of points, by default 0.5
729        log_abundance : bool, optional
730            If True, use log10 scale for abundance values, by default False
731
732        Returns
733        -------
734        ax : matplotlib.axes
735            Matplotlib axes object
736        abun_perc : float or None
737            Class percentile of the relative abundance (if classe specified)
738        """
739        import numpy as np
740
741        ax = plt.gca()
742
743        if classe is not None and classe != Labels.unassigned:
744            # Single class plot
745            abun_perc = self.abundance_count_percentile(classe)
746            hc = self.atoms_ratio(classe, "H", "C")
747            oc = self.atoms_ratio(classe, "O", "C")
748            abundance = self.abundance(classe)
749
750            if log_abundance:
751                abundance = [np.log10(a + 1e-10) for a in abundance]
752                colorbar_label = "log\u2081\u2080(Abundance)"
753            else:
754                colorbar_label = "Abundance"
755
756            # Sort by abundance so higher values are plotted on top
757            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
758            hc = [hc[i] for i in sorted_indices]
759            oc = [oc[i] for i in sorted_indices]
760            abundance = [abundance[i] for i in sorted_indices]
761
762            scatter = ax.scatter(oc, hc, c=abundance, alpha=alpha, cmap=color)
763            plt.colorbar(scatter, label=colorbar_label)
764
765            title = "%s, %.2f %%" % (classe, abun_perc)
766            ax.set_title(title)
767
768            return_val = ax, abun_perc
769        else:
770            # All assigned classes plot
771            hc = self.atoms_ratio_all("H", "C")
772            oc = self.atoms_ratio_all("O", "C")
773            abundance = self.abundance_assigned()
774
775            if log_abundance:
776                abundance = [np.log10(a + 1e-10) for a in abundance]
777                colorbar_label = "log\u2081\u2080(Abundance)"
778            else:
779                colorbar_label = "Abundance"
780
781            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
782            hc = [hc[i] for i in sorted_indices]
783            oc = [oc[i] for i in sorted_indices]
784            abundance = [abundance[i] for i in sorted_indices]
785
786            scatter = ax.scatter(oc, hc, c=abundance, alpha=alpha, cmap=color)
787            plt.colorbar(scatter, label=colorbar_label)
788
789            ax.set_title("Van Krevelen Diagram - All Assigned Classes")
790
791            return_val = ax
792
793        ax.set_xlabel("O/C", fontsize=16)
794        ax.set_ylabel("H/C", fontsize=16)
795        ax.tick_params(axis="both", which="major", labelsize=18)
796        ax.set_xticks(linspace(0, max_oc, ticks_number, endpoint=True))
797        ax.set_yticks(linspace(0, max_hc, ticks_number, endpoint=True))
798        ax.grid(alpha=0.3, linestyle="--")
799
800        return return_val
801
802    def plot_dbe_vs_carbon_number(
803        self,
804        classe=None,
805        max_c=50,
806        max_dbe=40,
807        dbe_incr=5,
808        c_incr=10,
809        color="viridis",
810        alpha=0.5,
811        log_abundance=False,
812    ):
813        """Plot DBE vs Carbon Number for a single class or all assigned classes
814
815        Parameters
816        ----------
817        classe : str, optional
818            Class name or None to plot all assigned classes, by default None
819        max_c : int, optional
820            Max Carbon Number, by default 50
821        max_dbe : int, optional
822            Max DBE, by default 40
823        dbe_incr : int, optional
824            DBE increment, by default 5
825        c_incr : int, optional
826            Carbon Number increment, by default 10
827        color : str, optional
828            Matplotlib color/colormap, by default "viridis"
829        alpha : float, optional
830            Transparency of points, by default 0.5
831        log_abundance : bool, optional
832            If True, use log10 scale for abundance values, by default False
833
834        Returns
835        -------
836        ax : matplotlib.axes
837            Matplotlib axes object
838        abun_perc : float or None
839            Class percentile of the relative abundance (if classe specified)
840        """
841        import numpy as np
842
843        ax = plt.gca()
844
845        if classe is not None and classe != Labels.unassigned:
846            # Single class plot
847            abun_perc = self.abundance_count_percentile(classe)
848            carbon_number = self.carbon_number(classe)
849            dbe = self.dbe(classe)
850            abundance = self.abundance(classe)
851
852            if log_abundance:
853                abundance = [np.log10(a + 1e-10) for a in abundance]
854                colorbar_label = "log\u2081\u2080(Abundance)"
855            else:
856                colorbar_label = "Abundance"
857
858            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
859            carbon_number = [carbon_number[i] for i in sorted_indices]
860            dbe = [dbe[i] for i in sorted_indices]
861            abundance = [abundance[i] for i in sorted_indices]
862
863            scatter = ax.scatter(
864                carbon_number, dbe, c=abundance, alpha=alpha, cmap=color
865            )
866            plt.colorbar(scatter, label=colorbar_label)
867
868            title = "%s, %.2f %%" % (classe, abun_perc)
869            ax.set_title(title)
870
871            return_val = ax, abun_perc
872        else:
873            # All assigned classes plot
874            carbon_number = self.carbon_number_all()
875            dbe = self.dbe_all()
876            abundance = self.abundance_assigned()
877
878            if log_abundance:
879                abundance = [np.log10(a + 1e-10) for a in abundance]
880                colorbar_label = "log\u2081\u2080(Abundance)"
881            else:
882                colorbar_label = "Abundance"
883
884            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
885            carbon_number = [carbon_number[i] for i in sorted_indices]
886            dbe = [dbe[i] for i in sorted_indices]
887            abundance = [abundance[i] for i in sorted_indices]
888
889            scatter = ax.scatter(
890                carbon_number, dbe, c=abundance, alpha=alpha, cmap=color
891            )
892            plt.colorbar(scatter, label=colorbar_label)
893
894            ax.set_title("DBE vs Carbon Number - All Assigned Classes")
895
896            return_val = ax
897
898        ax.set_xlabel("Carbon number", fontsize=16)
899        ax.set_ylabel("DBE", fontsize=16)
900        ax.tick_params(axis="both", which="major", labelsize=18)
901        ax.set_xticks(range(0, max_c, c_incr))
902        ax.set_yticks(range(0, max_dbe, dbe_incr))
903        ax.grid(alpha=0.3, linestyle="--")
904
905        return return_val
def flatten_list(l):
12flatten_list = lambda l: [item for sublist in l for item in sublist]
class HeteroatomsClassification(collections.abc.Mapping):
 15class HeteroatomsClassification(Mapping):
 16    """Class for grouping mass spectrum data by heteroatom classes (Nn, Oo, Ss, NnOo, NnSs, etc..)
 17
 18    Parameters
 19    ----------
 20    mass_spectrum : MassSpectrum
 21        The mass spectrum object.
 22    choose_molecular_formula : bool, optional
 23        If True, the molecular formula with the highest abundance is chosen. If False, all molecular formulas are considered. Default is True.
 24
 25    Raises
 26    ------
 27    Exception
 28        If no molecular formula is associated with any mspeak objects.
 29
 30    Attributes
 31    ----------
 32    _ms_grouped_class : dict
 33        A dictionary of classes and a list of ms_peak objects.
 34    choose_mf : bool
 35        If True, the molecular formula with the highest abundance is chosen. If False, all molecular formulas are considered.
 36    total_peaks : int
 37        The total number of peaks.
 38    sum_abundance : float
 39        The sum of the abundance of all peaks.
 40    min_max_mz : tuple
 41        The minimum and maximum mz values.
 42    min_max_abundance : tuple
 43        The minimum and maximum abundance values.
 44    min_ppm_error : float
 45        The minimum ppm error.
 46    max_ppm_error : float
 47        The maximum ppm error.
 48    all_identified_atoms : list
 49        A list of all identified atoms.
 50
 51    Methods
 52    -------
 53    * __len__().
 54        Returns the number of classes.
 55    * __getitem__(classe)
 56        Returns the molecular formula list for specified class.
 57    * __iter__().
 58        Returns an iterator over the keys of the dictionary.
 59    * get_classes(threshold_perc=1, isotopologue=True).
 60        Returns a list of classes with abundance percentile above threshold.
 61    * molecular_formula_string(classe).
 62        Returns a list of molecular formula string for specified class.
 63    * molecular_formula(classe).
 64        Returns a list of molecular formula for specified class.
 65    * carbon_number(classe).
 66        Returns a list of carbon number for specified class.
 67    * atom_count(atom, classe).
 68        Returns a list of atom count for specified class.
 69    * dbe(classe).
 70        Returns a list of DBE for specified class.
 71    * atoms_ratio(classe, numerator, denominator).
 72        Returns a list of atoms ratio for specified class.
 73    * mz_exp(classe).
 74        Returns a list of experimental mz for specified class.
 75    * abundance(classe).
 76        Returns a list of abundance for specified class.
 77    * mz_error(classe).
 78        Returns a list of mz error for specified class.
 79    * mz_calc(classe).
 80        Returns a list of calculated mz for specified class.
 81    * peaks_count_percentile(classe).
 82        Returns the peaks count percentile of a specific class.
 83    * abundance_count_percentile(classe).
 84        Returns the abundance percentile of a specific class.
 85    * mz_exp_assigned().
 86        Returns a list of experimental mz for all assigned classes.
 87    * abundance_assigned().
 88        Returns a list of abundance for all classes.
 89    * mz_exp_all().
 90        Returns a list of mz for all classes.
 91
 92    """
 93
 94    # Group mass spectrum data by heteroatom classes (Nn, Oo, Ss, NnOo, NnSs, etc..)
 95
 96    #   class obj behaves as a dictionary of classes and return a list of ms_peak obj
 97
 98    def __init__(self, mass_spectrum, choose_molecular_formula=True):
 99        def sort_atoms_method(atom):
100            """Sort atoms by order of appearance in the Atoms class"""
101            return [Atoms.atoms_order.index(atom)]
102
103        self._ms_grouped_class = dict()
104
105        self.choose_mf = choose_molecular_formula
106
107        # mapping for ms peaks without any molecular formula associated
108        self._ms_grouped_class[Labels.unassigned] = list()
109
110        self.total_peaks = 0
111
112        self.sum_abundance = 0
113
114        self.min_max_mz = (mass_spectrum.min_mz_exp, mass_spectrum.max_mz_exp)
115
116        self.min_max_abundance = (
117            mass_spectrum.min_abundance,
118            mass_spectrum.max_abundance,
119        )
120
121        self.min_ppm_error = mass_spectrum.molecular_search_settings.min_ppm_error
122
123        self.max_ppm_error = mass_spectrum.molecular_search_settings.max_ppm_error
124
125        check_assign = False
126
127        all_used_atoms = set()
128
129        for ms_peak in mass_spectrum:
130            self.total_peaks += 1
131
132            self.sum_abundance += ms_peak.abundance
133
134            if not ms_peak.is_assigned:
135                self._ms_grouped_class.get(Labels.unassigned).append(ms_peak)
136
137            else:
138                check_assign = True
139
140                if choose_molecular_formula:
141                    mf = ms_peak.best_molecular_formula_candidate
142
143                    classes = [mf.class_label]
144
145                    for atom in mf.atoms:
146                        all_used_atoms.add(atom)
147
148                else:
149                    classes = []
150
151                    for mf in ms_peak:
152                        classes.append(mf.class_label)
153
154                        for atom in mf.atoms:
155                            all_used_atoms.add(atom)
156
157                for classe in classes:
158                    if classe in self._ms_grouped_class.keys():
159                        self._ms_grouped_class.get(classe).append(ms_peak)
160
161                    else:
162                        self._ms_grouped_class[classe] = [ms_peak]
163
164        self.all_identified_atoms = sorted(all_used_atoms, key=sort_atoms_method)
165
166        if not check_assign:
167            raise Exception("No molecular formula associated with any mspeak objects")
168
169    def __len__(self):
170        """Return the number of classes"""
171        return len(self._ms_grouped_class)
172
173    def __getitem__(self, classe):
174        """Return the molecular formula list for specified class"""
175        return self._ms_grouped_class.get(classe)
176
177    def __iter__(self):
178        """Return an iterator over the keys of the dictionary."""
179        return iter(self._ms_grouped_class)
180
181    def get_classes(self, threshold_perc=1, isotopologue=True):
182        """Return a list of classes with abundance percentile above threshold"""
183        classes = list()
184        for classe in self.keys():
185            if classe != Labels.unassigned:
186                if self.abundance_count_percentile(classe) > threshold_perc:
187                    if classe != Labels.unassigned:
188                        # access first molecular formula inside the first ms peak and check isotopologue
189                        if not isotopologue and self.get(classe)[0][0].is_isotopologue:
190                            continue
191
192                    classes.append(classe)
193        # TODO sort classes chemically here too
194        return classes
195
196    def molecular_formula_string(
197        self,
198        classe,
199    ):
200        """Return a list of molecular formula string for specified class"""
201        if self.choose_mf:
202            return [mspeak.best_molecular_formula_candidate for mspeak in self[classe]]
203        else:
204            return [
205                mf
206                for mspeak in self[classe]
207                for mf in mspeak
208                if mf.class_label == classe
209            ]
210
211    def molecular_formula(
212        self,
213        classe,
214    ):
215        """Return a list of molecular formula for specified class"""
216        if self.choose_mf:
217            return [mspeak.best_molecular_formula_candidate for mspeak in self[classe]]
218        else:
219            return [
220                mf
221                for mspeak in self[classe]
222                for mf in mspeak
223                if mf.class_label == classe
224            ]
225
226    def carbon_number(self, classe):
227        """Return a list of carbon number for specified class"""
228        if self.choose_mf:
229            return [
230                mspeak.best_molecular_formula_candidate.get("C")
231                for mspeak in self[classe]
232            ]
233        else:
234            return [
235                mf.get("C")
236                for mspeak in self[classe]
237                for mf in mspeak
238                if mf.class_label == classe
239            ]
240
241    def atom_count(self, atom, classe):
242        """Return a list of atom count for specified class"""
243
244        if self.choose_mf:
245            return [
246                mspeak.best_molecular_formula_candidate.get(atom)
247                for mspeak in self[classe]
248            ]
249        else:
250            return [
251                mf.get(atom)
252                for mspeak in self[classe]
253                for mf in mspeak
254                if mf.class_label == classe
255            ]
256
257    def dbe(self, classe):
258        """Return a list of DBE for specified class"""
259        if self.choose_mf:
260            return [
261                mspeak.best_molecular_formula_candidate.dbe for mspeak in self[classe]
262            ]
263        else:
264            return [
265                mf.dbe
266                for mspeak in self[classe]
267                for mf in mspeak
268                if mf.class_label == classe
269            ]
270
271    def atoms_ratio(self, classe, numerator, denominator):
272        """Return a list of atoms ratio for specified class"""
273        return [
274            mf.get(numerator) / mf.get(denominator)
275            for mf in self.molecular_formula(classe)
276        ]
277
278    def mz_exp(self, classe):
279        """Return a list of experimental mz for specified class"""
280        if self.choose_mf or classe == Labels.unassigned:
281            return [mspeak.mz_exp for mspeak in self[classe]]
282
283        else:
284            return [
285                mspeak.mz_exp
286                for mspeak in self[classe]
287                for mf in mspeak
288                if mf.class_label == classe
289            ]
290
291    def abundance(self, classe):
292        """Return a list of abundance for specified class"""
293        if self.choose_mf or classe == Labels.unassigned:
294            return [mspeak.abundance for mspeak in self[classe]]
295
296        else:
297            return [
298                mspeak.abundance
299                for mspeak in self[classe]
300                for mf in mspeak
301                if mf.class_label == classe
302            ]
303
304    def mz_error(self, classe):
305        """Return a list of mz error for specified class"""
306        if classe != Labels.unassigned:
307            if self.choose_mf:
308                return [
309                    mspeak.best_molecular_formula_candidate.mz_error
310                    for mspeak in self[classe]
311                ]
312
313            else:
314                return [
315                    mf.mz_error
316                    for mspeak in self[classe]
317                    for mf in mspeak
318                    if mf.class_label == classe
319                ]
320
321    def mz_calc(self, classe):
322        """Return a list of calculated mz for specified class"""
323        if self.choose_mf:
324            return [
325                mspeak.best_molecular_formula_candidate.mz_calc
326                for mspeak in self[classe]
327            ]
328
329        else:
330            return [
331                mf.mz_calc
332                for mspeak in self[classe]
333                for mf in mspeak
334                if mf.class_label == classe
335            ]
336
337    def peaks_count_percentile(self, classe):
338        """Return the peaks count percentile of a specific class"""
339        return (len(self[classe]) / self.total_peaks) * 100
340
341    def abundance_count_percentile(self, classe):
342        """Return the abundance percentile of a specific class"""
343        return (
344            sum([mspeak.abundance for mspeak in self[classe]]) / self.sum_abundance
345        ) * 100
346
347    def mz_exp_assigned(self):
348        """Return a list of experimental mz for all assigned classes"""
349        classes = self.keys()
350
351        return [
352            mspeak.mz_exp
353            for classe in classes
354            for mspeak in self[classe]
355            if classe != Labels.unassigned
356        ]
357
358    def abundance_assigned(self):
359        """Return a list of abundance for all classes"""
360        classes = self.keys()
361
362        return [
363            mspeak.abundance
364            for classe in classes
365            for mspeak in self[classe]
366            if classe != Labels.unassigned
367        ]
368
369    def mz_exp_all(self):
370        """Return a list of mz for all classes"""
371        classes = self.keys()
372
373        return flatten_list(
374            [self.mz_exp(classe) for classe in classes if classe != Labels.unassigned]
375        )
376
377    def mz_error_all(self):
378        """Return a list of mz error for all classes"""
379        classes = self.keys()
380
381        return flatten_list(
382            [self.mz_error(classe) for classe in classes if classe != Labels.unassigned]
383        )
384
385    def carbon_number_all(self):
386        """Return a list of carbon number for all classes"""
387        classes = self.keys()
388
389        return flatten_list(
390            [
391                self.carbon_number(classe)
392                for classe in classes
393                if classe != Labels.unassigned
394            ]
395        )
396
397    def dbe_all(self):
398        """Return a list of DBE for all classes"""
399        classes = self.keys()
400
401        return flatten_list(
402            [self.dbe(classe) for classe in classes if classe != Labels.unassigned]
403        )
404
405    def atoms_ratio_all(self, numerator, denominator):
406        """Return a list of atoms ratio for all classes"""
407        classes = self.keys()
408
409        return flatten_list(
410            [
411                self.atoms_ratio(classe, numerator, denominator)
412                for classe in classes
413                if classe != Labels.unassigned
414            ]
415        )
416
417    def to_dataframe(
418        self,
419        include_isotopologue=False,
420        abundance_perc_threshold=5,
421        include_unassigned=False,
422    ):
423        """Return a pandas dataframe with all the data from the class
424
425        Parameters
426        ----------
427        include_isotopologue : bool, optional
428            Include isotopologues, by default False
429        abundance_perc_threshold : int, optional
430            Abundance percentile threshold, by default 5
431        include_unassigned : bool, optional
432            Include unassigned peaks, by default False
433
434        Returns
435        -------
436        DataFrame
437            Pandas dataframe with all the data from the class
438        """
439        from pandas import DataFrame
440
441        columns_labels = [
442            "mz",
443            "calibrated_mz",
444            "calculated_m_z",
445            "abundance",
446            "resolving_power",
447            "sn",
448            "ion_charge",
449            "mass_error",
450            "DBE",
451            "class",
452            "HC",
453            "OC",
454            "ion_type",
455            "is_isotopologue",
456            "class_abundance",
457            "class_count",
458        ]
459
460        dict_data_list = []
461
462        for classe, list_mspeaks in self.items():
463            percent_abundance = self.abundance_count_percentile(classe)
464
465            # ignores low abundant classes
466            if abundance_perc_threshold < abundance_perc_threshold:
467                continue
468
469            peaks_count_percentile = self.peaks_count_percentile(classe)
470
471            for ms_peak in list_mspeaks:
472                if ms_peak.is_assigned:
473                    for m_formula in ms_peak:
474                        # ignores isotopologues
475                        if not include_isotopologue and m_formula.is_isotopologue:
476                            continue
477
478                        formula_dict = m_formula.to_dict()
479
480                        dict_result = {
481                            "mz": ms_peak._mz_exp,
482                            "calibrated_mz": ms_peak.mz_exp,
483                            "calculated_mz": m_formula.mz_calc,
484                            "abundance": ms_peak.abundance,
485                            "resolving_power": ms_peak.resolving_power,
486                            "sn": ms_peak.signal_to_noise,
487                            # Assigned: formula charge (may be multi-charge), not peak polarity
488                            "ion_charge": m_formula.ion_charge,
489                            "mass_error": m_formula.mz_error,
490                            "DBE": m_formula.dbe,
491                            "class": classe,
492                            "HC": m_formula.H_C,
493                            "OC": m_formula.O_C,
494                            "ion_type": str(m_formula.ion_type.lower().encode("utf-8")),
495                            "is_isotopologue": int(m_formula.is_isotopologue),
496                            "class_abundance": percent_abundance,
497                            "class_count": peaks_count_percentile,
498                        }
499
500                        for atom in formula_dict.keys():
501                            dict_result[atom] = formula_dict.get(atom)
502
503                    dict_data_list.append(dict_result)
504
505                else:
506                    if not include_unassigned:
507                        continue
508
509                    dict_result = {
510                        "mz": ms_peak._mz_exp,
511                        "calibrated_mz": ms_peak.mz_exp,
512                        "abundance": ms_peak.abundance,
513                        "resolving_power": ms_peak.resolving_power,
514                        "sn": ms_peak.signal_to_noise,
515                        # Peak polarity (±1); no formula charge for unassigned peaks
516                        "ion_charge": ms_peak.polarity,
517                        "class": classe,
518                        "class_abundance": percent_abundance,
519                        "class_count": percent_abundance,
520                    }
521
522                    dict_data_list.append(dict_result)
523
524        columns = columns_labels + self.all_identified_atoms
525
526        return DataFrame(dict_data_list, columns=columns)
527
528    def plot_ms_assigned_unassigned(self, assigned_color="b", unassigned_color="r"):
529        """Plot stick mass spectrum of all classes
530
531        Parameters
532        ----------
533        assigned_color : str, optional
534            Matplotlib color for the assigned peaks, by default "b"
535        unassigned_color : str, optional
536            Matplotlib color for the unassigned peaks, by default "r"
537
538        Returns
539        -------
540        ax : matplotlib.axes
541            Matplotlib axes object
542        """
543        mz_assigned = self.mz_exp_assigned()
544        abundance_assigned = self.abundance_assigned()
545
546        mz_not_assigned = self.mz_exp(Labels.unassigned)
547        abundance_not_assigned = self.abundance(Labels.unassigned)
548
549        ax = plt.gca()
550
551        for plot_obj in ax.stem(
552            mz_assigned,
553            abundance_assigned,
554            linefmt="-",
555            markerfmt=" ",
556            label="Assigned",
557        ):
558            plt.setp(plot_obj, "color", assigned_color, "linewidth", 2)
559
560        for plot_obj in ax.stem(
561            mz_not_assigned,
562            abundance_not_assigned,
563            linefmt="-",
564            markerfmt=" ",
565            label="Unassigned",
566        ):
567            plt.setp(plot_obj, "color", unassigned_color, "linewidth", 2)
568
569        ax.set_xlabel("$\t{m/z}$", fontsize=12)
570        ax.set_ylabel("Abundance", fontsize=12)
571        ax.tick_params(axis="both", which="major", labelsize=12)
572
573        ax.axes.spines["top"].set_visible(False)
574        ax.axes.spines["right"].set_visible(False)
575
576        ax.get_yaxis().set_visible(False)
577        ax.spines["left"].set_visible(False)
578        plt.legend()
579
580        return ax
581
582    def plot_mz_error(self, color="g"):
583        """Plot mz error scatter plot of all classes
584
585        Parameters
586        ----------
587        color : str, optional
588            Matplotlib color, by default "g"
589
590        Returns
591        -------
592        ax : matplotlib.axes
593            Matplotlib axes object
594        """
595        ax = plt.gca()
596
597        mz_assigned = self.mz_exp_all()
598        mz_error = self.mz_error_all()
599
600        ax.scatter(mz_assigned, mz_error, c=color)
601
602        ax.set_xlabel("$\t{m/z}$", fontsize=12)
603        ax.set_ylabel("Error (ppm)", fontsize=12)
604        ax.tick_params(axis="both", which="major", labelsize=12)
605
606        ax.axes.spines["top"].set_visible(True)
607        ax.axes.spines["right"].set_visible(True)
608
609        ax.get_yaxis().set_visible(True)
610        ax.spines["left"].set_visible(True)
611
612        ax.set_xlim(self.min_max_mz)
613        ax.set_ylim(self.min_ppm_error, self.max_ppm_error)
614
615        return ax
616
617    def plot_mz_error_class(self, classe, color="g"):
618        """Plot mz error scatter plot of a specific class
619
620        Parameters
621        ----------
622        classe : str
623            Class name
624        color : str, optional
625            Matplotlib color, by default "g"
626
627        Returns
628        -------
629        ax : matplotlib.axes
630            Matplotlib axes object
631
632        """
633        if classe != Labels.unassigned:
634            ax = plt.gca()
635
636            abun_perc = self.abundance_count_percentile(classe)
637            mz_assigned = self.mz_exp(classe)
638            mz_error = self.mz_error(classe)
639
640            ax.scatter(mz_assigned, mz_error, c=color)
641
642            title = "%s, %.2f %%" % (classe, abun_perc)
643            ax.set_title(title)
644            ax.set_xlabel("$\t{m/z}$", fontsize=12)
645            ax.set_ylabel("Error (ppm)", fontsize=12)
646            ax.tick_params(axis="both", which="major", labelsize=12)
647
648            ax.axes.spines["top"].set_visible(True)
649            ax.axes.spines["right"].set_visible(True)
650
651            ax.get_yaxis().set_visible(True)
652            ax.spines["left"].set_visible(True)
653
654            ax.set_xlim(self.min_max_mz)
655            ax.set_ylim(self.min_ppm_error, self.max_ppm_error)
656
657            return ax
658
659    def plot_ms_class(self, classe, color="g"):
660        """Plot stick mass spectrum of a specific class
661
662        Parameters
663        ----------
664        classe : str
665            Class name
666        color : str, optional
667            Matplotlib color, by default "g"
668
669        Returns
670        -------
671        ax : matplotlib.axes
672            Matplotlib axes object
673
674        """
675        if classe != Labels.unassigned:
676            ax = plt.gca()
677
678            abun_perc = self.abundance_count_percentile(classe)
679            mz_assigned = self.mz_exp(classe)
680            abundance_assigned = self.abundance(classe)
681
682            for plot_obj in ax.stem(
683                mz_assigned, abundance_assigned, linefmt="-", markerfmt=" "
684            ):
685                plt.setp(plot_obj, "color", color, "linewidth", 2)
686
687            title = "%s, %.2f %%" % (classe, abun_perc)
688            ax.set_title(title)
689            ax.set_xlabel("$\t{m/z}$", fontsize=12)
690            ax.set_ylabel("Abundance", fontsize=12)
691            ax.tick_params(axis="both", which="major", labelsize=12)
692
693            ax.axes.spines["top"].set_visible(False)
694            ax.axes.spines["right"].set_visible(False)
695
696            ax.get_yaxis().set_visible(False)
697            ax.spines["left"].set_visible(False)
698
699            ax.set_xlim(self.min_max_mz)
700            ax.set_ylim(self.min_max_abundance)
701
702            return ax
703
704    def plot_van_krevelen(
705        self,
706        classe=None,
707        max_hc=2.5,
708        max_oc=2,
709        ticks_number=5,
710        color="viridis",
711        alpha=0.5,
712        log_abundance=False,
713    ):
714        """Plot Van Krevelen Diagram for a single class or all assigned classes
715
716        Parameters
717        ----------
718        classe : str, optional
719            Class name or None to plot all assigned classes, by default None
720        max_hc : float, optional
721            Max H/C ratio, by default 2.5
722        max_oc : float, optional
723            Max O/C ratio, by default 2
724        ticks_number : int, optional
725            Number of ticks, by default 5
726        color : str, optional
727            Matplotlib color/colormap, by default "viridis"
728        alpha : float, optional
729            Transparency of points, by default 0.5
730        log_abundance : bool, optional
731            If True, use log10 scale for abundance values, by default False
732
733        Returns
734        -------
735        ax : matplotlib.axes
736            Matplotlib axes object
737        abun_perc : float or None
738            Class percentile of the relative abundance (if classe specified)
739        """
740        import numpy as np
741
742        ax = plt.gca()
743
744        if classe is not None and classe != Labels.unassigned:
745            # Single class plot
746            abun_perc = self.abundance_count_percentile(classe)
747            hc = self.atoms_ratio(classe, "H", "C")
748            oc = self.atoms_ratio(classe, "O", "C")
749            abundance = self.abundance(classe)
750
751            if log_abundance:
752                abundance = [np.log10(a + 1e-10) for a in abundance]
753                colorbar_label = "log\u2081\u2080(Abundance)"
754            else:
755                colorbar_label = "Abundance"
756
757            # Sort by abundance so higher values are plotted on top
758            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
759            hc = [hc[i] for i in sorted_indices]
760            oc = [oc[i] for i in sorted_indices]
761            abundance = [abundance[i] for i in sorted_indices]
762
763            scatter = ax.scatter(oc, hc, c=abundance, alpha=alpha, cmap=color)
764            plt.colorbar(scatter, label=colorbar_label)
765
766            title = "%s, %.2f %%" % (classe, abun_perc)
767            ax.set_title(title)
768
769            return_val = ax, abun_perc
770        else:
771            # All assigned classes plot
772            hc = self.atoms_ratio_all("H", "C")
773            oc = self.atoms_ratio_all("O", "C")
774            abundance = self.abundance_assigned()
775
776            if log_abundance:
777                abundance = [np.log10(a + 1e-10) for a in abundance]
778                colorbar_label = "log\u2081\u2080(Abundance)"
779            else:
780                colorbar_label = "Abundance"
781
782            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
783            hc = [hc[i] for i in sorted_indices]
784            oc = [oc[i] for i in sorted_indices]
785            abundance = [abundance[i] for i in sorted_indices]
786
787            scatter = ax.scatter(oc, hc, c=abundance, alpha=alpha, cmap=color)
788            plt.colorbar(scatter, label=colorbar_label)
789
790            ax.set_title("Van Krevelen Diagram - All Assigned Classes")
791
792            return_val = ax
793
794        ax.set_xlabel("O/C", fontsize=16)
795        ax.set_ylabel("H/C", fontsize=16)
796        ax.tick_params(axis="both", which="major", labelsize=18)
797        ax.set_xticks(linspace(0, max_oc, ticks_number, endpoint=True))
798        ax.set_yticks(linspace(0, max_hc, ticks_number, endpoint=True))
799        ax.grid(alpha=0.3, linestyle="--")
800
801        return return_val
802
803    def plot_dbe_vs_carbon_number(
804        self,
805        classe=None,
806        max_c=50,
807        max_dbe=40,
808        dbe_incr=5,
809        c_incr=10,
810        color="viridis",
811        alpha=0.5,
812        log_abundance=False,
813    ):
814        """Plot DBE vs Carbon Number for a single class or all assigned classes
815
816        Parameters
817        ----------
818        classe : str, optional
819            Class name or None to plot all assigned classes, by default None
820        max_c : int, optional
821            Max Carbon Number, by default 50
822        max_dbe : int, optional
823            Max DBE, by default 40
824        dbe_incr : int, optional
825            DBE increment, by default 5
826        c_incr : int, optional
827            Carbon Number increment, by default 10
828        color : str, optional
829            Matplotlib color/colormap, by default "viridis"
830        alpha : float, optional
831            Transparency of points, by default 0.5
832        log_abundance : bool, optional
833            If True, use log10 scale for abundance values, by default False
834
835        Returns
836        -------
837        ax : matplotlib.axes
838            Matplotlib axes object
839        abun_perc : float or None
840            Class percentile of the relative abundance (if classe specified)
841        """
842        import numpy as np
843
844        ax = plt.gca()
845
846        if classe is not None and classe != Labels.unassigned:
847            # Single class plot
848            abun_perc = self.abundance_count_percentile(classe)
849            carbon_number = self.carbon_number(classe)
850            dbe = self.dbe(classe)
851            abundance = self.abundance(classe)
852
853            if log_abundance:
854                abundance = [np.log10(a + 1e-10) for a in abundance]
855                colorbar_label = "log\u2081\u2080(Abundance)"
856            else:
857                colorbar_label = "Abundance"
858
859            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
860            carbon_number = [carbon_number[i] for i in sorted_indices]
861            dbe = [dbe[i] for i in sorted_indices]
862            abundance = [abundance[i] for i in sorted_indices]
863
864            scatter = ax.scatter(
865                carbon_number, dbe, c=abundance, alpha=alpha, cmap=color
866            )
867            plt.colorbar(scatter, label=colorbar_label)
868
869            title = "%s, %.2f %%" % (classe, abun_perc)
870            ax.set_title(title)
871
872            return_val = ax, abun_perc
873        else:
874            # All assigned classes plot
875            carbon_number = self.carbon_number_all()
876            dbe = self.dbe_all()
877            abundance = self.abundance_assigned()
878
879            if log_abundance:
880                abundance = [np.log10(a + 1e-10) for a in abundance]
881                colorbar_label = "log\u2081\u2080(Abundance)"
882            else:
883                colorbar_label = "Abundance"
884
885            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
886            carbon_number = [carbon_number[i] for i in sorted_indices]
887            dbe = [dbe[i] for i in sorted_indices]
888            abundance = [abundance[i] for i in sorted_indices]
889
890            scatter = ax.scatter(
891                carbon_number, dbe, c=abundance, alpha=alpha, cmap=color
892            )
893            plt.colorbar(scatter, label=colorbar_label)
894
895            ax.set_title("DBE vs Carbon Number - All Assigned Classes")
896
897            return_val = ax
898
899        ax.set_xlabel("Carbon number", fontsize=16)
900        ax.set_ylabel("DBE", fontsize=16)
901        ax.tick_params(axis="both", which="major", labelsize=18)
902        ax.set_xticks(range(0, max_c, c_incr))
903        ax.set_yticks(range(0, max_dbe, dbe_incr))
904        ax.grid(alpha=0.3, linestyle="--")
905
906        return return_val

Class for grouping mass spectrum data by heteroatom classes (Nn, Oo, Ss, NnOo, NnSs, etc..)

Parameters
  • mass_spectrum (MassSpectrum): The mass spectrum object.
  • choose_molecular_formula (bool, optional): If True, the molecular formula with the highest abundance is chosen. If False, all molecular formulas are considered. Default is True.
Raises
  • Exception: If no molecular formula is associated with any mspeak objects.
Attributes
  • _ms_grouped_class (dict): A dictionary of classes and a list of ms_peak objects.
  • choose_mf (bool): If True, the molecular formula with the highest abundance is chosen. If False, all molecular formulas are considered.
  • total_peaks (int): The total number of peaks.
  • sum_abundance (float): The sum of the abundance of all peaks.
  • min_max_mz (tuple): The minimum and maximum mz values.
  • min_max_abundance (tuple): The minimum and maximum abundance values.
  • min_ppm_error (float): The minimum ppm error.
  • max_ppm_error (float): The maximum ppm error.
  • all_identified_atoms (list): A list of all identified atoms.
Methods
  • __len__(). Returns the number of classes.
  • __getitem__(classe) Returns the molecular formula list for specified class.
  • __iter__(). Returns an iterator over the keys of the dictionary.
  • get_classes(threshold_perc=1, isotopologue=True). Returns a list of classes with abundance percentile above threshold.
  • molecular_formula_string(classe). Returns a list of molecular formula string for specified class.
  • molecular_formula(classe). Returns a list of molecular formula for specified class.
  • carbon_number(classe). Returns a list of carbon number for specified class.
  • atom_count(atom, classe). Returns a list of atom count for specified class.
  • dbe(classe). Returns a list of DBE for specified class.
  • atoms_ratio(classe, numerator, denominator). Returns a list of atoms ratio for specified class.
  • mz_exp(classe). Returns a list of experimental mz for specified class.
  • abundance(classe). Returns a list of abundance for specified class.
  • mz_error(classe). Returns a list of mz error for specified class.
  • mz_calc(classe). Returns a list of calculated mz for specified class.
  • peaks_count_percentile(classe). Returns the peaks count percentile of a specific class.
  • abundance_count_percentile(classe). Returns the abundance percentile of a specific class.
  • mz_exp_assigned(). Returns a list of experimental mz for all assigned classes.
  • abundance_assigned(). Returns a list of abundance for all classes.
  • mz_exp_all(). Returns a list of mz for all classes.
HeteroatomsClassification(mass_spectrum, choose_molecular_formula=True)
 98    def __init__(self, mass_spectrum, choose_molecular_formula=True):
 99        def sort_atoms_method(atom):
100            """Sort atoms by order of appearance in the Atoms class"""
101            return [Atoms.atoms_order.index(atom)]
102
103        self._ms_grouped_class = dict()
104
105        self.choose_mf = choose_molecular_formula
106
107        # mapping for ms peaks without any molecular formula associated
108        self._ms_grouped_class[Labels.unassigned] = list()
109
110        self.total_peaks = 0
111
112        self.sum_abundance = 0
113
114        self.min_max_mz = (mass_spectrum.min_mz_exp, mass_spectrum.max_mz_exp)
115
116        self.min_max_abundance = (
117            mass_spectrum.min_abundance,
118            mass_spectrum.max_abundance,
119        )
120
121        self.min_ppm_error = mass_spectrum.molecular_search_settings.min_ppm_error
122
123        self.max_ppm_error = mass_spectrum.molecular_search_settings.max_ppm_error
124
125        check_assign = False
126
127        all_used_atoms = set()
128
129        for ms_peak in mass_spectrum:
130            self.total_peaks += 1
131
132            self.sum_abundance += ms_peak.abundance
133
134            if not ms_peak.is_assigned:
135                self._ms_grouped_class.get(Labels.unassigned).append(ms_peak)
136
137            else:
138                check_assign = True
139
140                if choose_molecular_formula:
141                    mf = ms_peak.best_molecular_formula_candidate
142
143                    classes = [mf.class_label]
144
145                    for atom in mf.atoms:
146                        all_used_atoms.add(atom)
147
148                else:
149                    classes = []
150
151                    for mf in ms_peak:
152                        classes.append(mf.class_label)
153
154                        for atom in mf.atoms:
155                            all_used_atoms.add(atom)
156
157                for classe in classes:
158                    if classe in self._ms_grouped_class.keys():
159                        self._ms_grouped_class.get(classe).append(ms_peak)
160
161                    else:
162                        self._ms_grouped_class[classe] = [ms_peak]
163
164        self.all_identified_atoms = sorted(all_used_atoms, key=sort_atoms_method)
165
166        if not check_assign:
167            raise Exception("No molecular formula associated with any mspeak objects")
choose_mf
total_peaks
sum_abundance
min_max_mz
min_max_abundance
min_ppm_error
max_ppm_error
all_identified_atoms
def get_classes(self, threshold_perc=1, isotopologue=True):
181    def get_classes(self, threshold_perc=1, isotopologue=True):
182        """Return a list of classes with abundance percentile above threshold"""
183        classes = list()
184        for classe in self.keys():
185            if classe != Labels.unassigned:
186                if self.abundance_count_percentile(classe) > threshold_perc:
187                    if classe != Labels.unassigned:
188                        # access first molecular formula inside the first ms peak and check isotopologue
189                        if not isotopologue and self.get(classe)[0][0].is_isotopologue:
190                            continue
191
192                    classes.append(classe)
193        # TODO sort classes chemically here too
194        return classes

Return a list of classes with abundance percentile above threshold

def molecular_formula_string(self, classe):
196    def molecular_formula_string(
197        self,
198        classe,
199    ):
200        """Return a list of molecular formula string for specified class"""
201        if self.choose_mf:
202            return [mspeak.best_molecular_formula_candidate for mspeak in self[classe]]
203        else:
204            return [
205                mf
206                for mspeak in self[classe]
207                for mf in mspeak
208                if mf.class_label == classe
209            ]

Return a list of molecular formula string for specified class

def molecular_formula(self, classe):
211    def molecular_formula(
212        self,
213        classe,
214    ):
215        """Return a list of molecular formula for specified class"""
216        if self.choose_mf:
217            return [mspeak.best_molecular_formula_candidate for mspeak in self[classe]]
218        else:
219            return [
220                mf
221                for mspeak in self[classe]
222                for mf in mspeak
223                if mf.class_label == classe
224            ]

Return a list of molecular formula for specified class

def carbon_number(self, classe):
226    def carbon_number(self, classe):
227        """Return a list of carbon number for specified class"""
228        if self.choose_mf:
229            return [
230                mspeak.best_molecular_formula_candidate.get("C")
231                for mspeak in self[classe]
232            ]
233        else:
234            return [
235                mf.get("C")
236                for mspeak in self[classe]
237                for mf in mspeak
238                if mf.class_label == classe
239            ]

Return a list of carbon number for specified class

def atom_count(self, atom, classe):
241    def atom_count(self, atom, classe):
242        """Return a list of atom count for specified class"""
243
244        if self.choose_mf:
245            return [
246                mspeak.best_molecular_formula_candidate.get(atom)
247                for mspeak in self[classe]
248            ]
249        else:
250            return [
251                mf.get(atom)
252                for mspeak in self[classe]
253                for mf in mspeak
254                if mf.class_label == classe
255            ]

Return a list of atom count for specified class

def dbe(self, classe):
257    def dbe(self, classe):
258        """Return a list of DBE for specified class"""
259        if self.choose_mf:
260            return [
261                mspeak.best_molecular_formula_candidate.dbe for mspeak in self[classe]
262            ]
263        else:
264            return [
265                mf.dbe
266                for mspeak in self[classe]
267                for mf in mspeak
268                if mf.class_label == classe
269            ]

Return a list of DBE for specified class

def atoms_ratio(self, classe, numerator, denominator):
271    def atoms_ratio(self, classe, numerator, denominator):
272        """Return a list of atoms ratio for specified class"""
273        return [
274            mf.get(numerator) / mf.get(denominator)
275            for mf in self.molecular_formula(classe)
276        ]

Return a list of atoms ratio for specified class

def mz_exp(self, classe):
278    def mz_exp(self, classe):
279        """Return a list of experimental mz for specified class"""
280        if self.choose_mf or classe == Labels.unassigned:
281            return [mspeak.mz_exp for mspeak in self[classe]]
282
283        else:
284            return [
285                mspeak.mz_exp
286                for mspeak in self[classe]
287                for mf in mspeak
288                if mf.class_label == classe
289            ]

Return a list of experimental mz for specified class

def abundance(self, classe):
291    def abundance(self, classe):
292        """Return a list of abundance for specified class"""
293        if self.choose_mf or classe == Labels.unassigned:
294            return [mspeak.abundance for mspeak in self[classe]]
295
296        else:
297            return [
298                mspeak.abundance
299                for mspeak in self[classe]
300                for mf in mspeak
301                if mf.class_label == classe
302            ]

Return a list of abundance for specified class

def mz_error(self, classe):
304    def mz_error(self, classe):
305        """Return a list of mz error for specified class"""
306        if classe != Labels.unassigned:
307            if self.choose_mf:
308                return [
309                    mspeak.best_molecular_formula_candidate.mz_error
310                    for mspeak in self[classe]
311                ]
312
313            else:
314                return [
315                    mf.mz_error
316                    for mspeak in self[classe]
317                    for mf in mspeak
318                    if mf.class_label == classe
319                ]

Return a list of mz error for specified class

def mz_calc(self, classe):
321    def mz_calc(self, classe):
322        """Return a list of calculated mz for specified class"""
323        if self.choose_mf:
324            return [
325                mspeak.best_molecular_formula_candidate.mz_calc
326                for mspeak in self[classe]
327            ]
328
329        else:
330            return [
331                mf.mz_calc
332                for mspeak in self[classe]
333                for mf in mspeak
334                if mf.class_label == classe
335            ]

Return a list of calculated mz for specified class

def peaks_count_percentile(self, classe):
337    def peaks_count_percentile(self, classe):
338        """Return the peaks count percentile of a specific class"""
339        return (len(self[classe]) / self.total_peaks) * 100

Return the peaks count percentile of a specific class

def abundance_count_percentile(self, classe):
341    def abundance_count_percentile(self, classe):
342        """Return the abundance percentile of a specific class"""
343        return (
344            sum([mspeak.abundance for mspeak in self[classe]]) / self.sum_abundance
345        ) * 100

Return the abundance percentile of a specific class

def mz_exp_assigned(self):
347    def mz_exp_assigned(self):
348        """Return a list of experimental mz for all assigned classes"""
349        classes = self.keys()
350
351        return [
352            mspeak.mz_exp
353            for classe in classes
354            for mspeak in self[classe]
355            if classe != Labels.unassigned
356        ]

Return a list of experimental mz for all assigned classes

def abundance_assigned(self):
358    def abundance_assigned(self):
359        """Return a list of abundance for all classes"""
360        classes = self.keys()
361
362        return [
363            mspeak.abundance
364            for classe in classes
365            for mspeak in self[classe]
366            if classe != Labels.unassigned
367        ]

Return a list of abundance for all classes

def mz_exp_all(self):
369    def mz_exp_all(self):
370        """Return a list of mz for all classes"""
371        classes = self.keys()
372
373        return flatten_list(
374            [self.mz_exp(classe) for classe in classes if classe != Labels.unassigned]
375        )

Return a list of mz for all classes

def mz_error_all(self):
377    def mz_error_all(self):
378        """Return a list of mz error for all classes"""
379        classes = self.keys()
380
381        return flatten_list(
382            [self.mz_error(classe) for classe in classes if classe != Labels.unassigned]
383        )

Return a list of mz error for all classes

def carbon_number_all(self):
385    def carbon_number_all(self):
386        """Return a list of carbon number for all classes"""
387        classes = self.keys()
388
389        return flatten_list(
390            [
391                self.carbon_number(classe)
392                for classe in classes
393                if classe != Labels.unassigned
394            ]
395        )

Return a list of carbon number for all classes

def dbe_all(self):
397    def dbe_all(self):
398        """Return a list of DBE for all classes"""
399        classes = self.keys()
400
401        return flatten_list(
402            [self.dbe(classe) for classe in classes if classe != Labels.unassigned]
403        )

Return a list of DBE for all classes

def atoms_ratio_all(self, numerator, denominator):
405    def atoms_ratio_all(self, numerator, denominator):
406        """Return a list of atoms ratio for all classes"""
407        classes = self.keys()
408
409        return flatten_list(
410            [
411                self.atoms_ratio(classe, numerator, denominator)
412                for classe in classes
413                if classe != Labels.unassigned
414            ]
415        )

Return a list of atoms ratio for all classes

def to_dataframe( self, include_isotopologue=False, abundance_perc_threshold=5, include_unassigned=False):
417    def to_dataframe(
418        self,
419        include_isotopologue=False,
420        abundance_perc_threshold=5,
421        include_unassigned=False,
422    ):
423        """Return a pandas dataframe with all the data from the class
424
425        Parameters
426        ----------
427        include_isotopologue : bool, optional
428            Include isotopologues, by default False
429        abundance_perc_threshold : int, optional
430            Abundance percentile threshold, by default 5
431        include_unassigned : bool, optional
432            Include unassigned peaks, by default False
433
434        Returns
435        -------
436        DataFrame
437            Pandas dataframe with all the data from the class
438        """
439        from pandas import DataFrame
440
441        columns_labels = [
442            "mz",
443            "calibrated_mz",
444            "calculated_m_z",
445            "abundance",
446            "resolving_power",
447            "sn",
448            "ion_charge",
449            "mass_error",
450            "DBE",
451            "class",
452            "HC",
453            "OC",
454            "ion_type",
455            "is_isotopologue",
456            "class_abundance",
457            "class_count",
458        ]
459
460        dict_data_list = []
461
462        for classe, list_mspeaks in self.items():
463            percent_abundance = self.abundance_count_percentile(classe)
464
465            # ignores low abundant classes
466            if abundance_perc_threshold < abundance_perc_threshold:
467                continue
468
469            peaks_count_percentile = self.peaks_count_percentile(classe)
470
471            for ms_peak in list_mspeaks:
472                if ms_peak.is_assigned:
473                    for m_formula in ms_peak:
474                        # ignores isotopologues
475                        if not include_isotopologue and m_formula.is_isotopologue:
476                            continue
477
478                        formula_dict = m_formula.to_dict()
479
480                        dict_result = {
481                            "mz": ms_peak._mz_exp,
482                            "calibrated_mz": ms_peak.mz_exp,
483                            "calculated_mz": m_formula.mz_calc,
484                            "abundance": ms_peak.abundance,
485                            "resolving_power": ms_peak.resolving_power,
486                            "sn": ms_peak.signal_to_noise,
487                            # Assigned: formula charge (may be multi-charge), not peak polarity
488                            "ion_charge": m_formula.ion_charge,
489                            "mass_error": m_formula.mz_error,
490                            "DBE": m_formula.dbe,
491                            "class": classe,
492                            "HC": m_formula.H_C,
493                            "OC": m_formula.O_C,
494                            "ion_type": str(m_formula.ion_type.lower().encode("utf-8")),
495                            "is_isotopologue": int(m_formula.is_isotopologue),
496                            "class_abundance": percent_abundance,
497                            "class_count": peaks_count_percentile,
498                        }
499
500                        for atom in formula_dict.keys():
501                            dict_result[atom] = formula_dict.get(atom)
502
503                    dict_data_list.append(dict_result)
504
505                else:
506                    if not include_unassigned:
507                        continue
508
509                    dict_result = {
510                        "mz": ms_peak._mz_exp,
511                        "calibrated_mz": ms_peak.mz_exp,
512                        "abundance": ms_peak.abundance,
513                        "resolving_power": ms_peak.resolving_power,
514                        "sn": ms_peak.signal_to_noise,
515                        # Peak polarity (±1); no formula charge for unassigned peaks
516                        "ion_charge": ms_peak.polarity,
517                        "class": classe,
518                        "class_abundance": percent_abundance,
519                        "class_count": percent_abundance,
520                    }
521
522                    dict_data_list.append(dict_result)
523
524        columns = columns_labels + self.all_identified_atoms
525
526        return DataFrame(dict_data_list, columns=columns)

Return a pandas dataframe with all the data from the class

Parameters
  • include_isotopologue (bool, optional): Include isotopologues, by default False
  • abundance_perc_threshold (int, optional): Abundance percentile threshold, by default 5
  • include_unassigned (bool, optional): Include unassigned peaks, by default False
Returns
  • DataFrame: Pandas dataframe with all the data from the class
def plot_ms_assigned_unassigned(self, assigned_color='b', unassigned_color='r'):
528    def plot_ms_assigned_unassigned(self, assigned_color="b", unassigned_color="r"):
529        """Plot stick mass spectrum of all classes
530
531        Parameters
532        ----------
533        assigned_color : str, optional
534            Matplotlib color for the assigned peaks, by default "b"
535        unassigned_color : str, optional
536            Matplotlib color for the unassigned peaks, by default "r"
537
538        Returns
539        -------
540        ax : matplotlib.axes
541            Matplotlib axes object
542        """
543        mz_assigned = self.mz_exp_assigned()
544        abundance_assigned = self.abundance_assigned()
545
546        mz_not_assigned = self.mz_exp(Labels.unassigned)
547        abundance_not_assigned = self.abundance(Labels.unassigned)
548
549        ax = plt.gca()
550
551        for plot_obj in ax.stem(
552            mz_assigned,
553            abundance_assigned,
554            linefmt="-",
555            markerfmt=" ",
556            label="Assigned",
557        ):
558            plt.setp(plot_obj, "color", assigned_color, "linewidth", 2)
559
560        for plot_obj in ax.stem(
561            mz_not_assigned,
562            abundance_not_assigned,
563            linefmt="-",
564            markerfmt=" ",
565            label="Unassigned",
566        ):
567            plt.setp(plot_obj, "color", unassigned_color, "linewidth", 2)
568
569        ax.set_xlabel("$\t{m/z}$", fontsize=12)
570        ax.set_ylabel("Abundance", fontsize=12)
571        ax.tick_params(axis="both", which="major", labelsize=12)
572
573        ax.axes.spines["top"].set_visible(False)
574        ax.axes.spines["right"].set_visible(False)
575
576        ax.get_yaxis().set_visible(False)
577        ax.spines["left"].set_visible(False)
578        plt.legend()
579
580        return ax

Plot stick mass spectrum of all classes

Parameters
  • assigned_color (str, optional): Matplotlib color for the assigned peaks, by default "b"
  • unassigned_color (str, optional): Matplotlib color for the unassigned peaks, by default "r"
Returns
  • ax (matplotlib.axes): Matplotlib axes object
def plot_mz_error(self, color='g'):
582    def plot_mz_error(self, color="g"):
583        """Plot mz error scatter plot of all classes
584
585        Parameters
586        ----------
587        color : str, optional
588            Matplotlib color, by default "g"
589
590        Returns
591        -------
592        ax : matplotlib.axes
593            Matplotlib axes object
594        """
595        ax = plt.gca()
596
597        mz_assigned = self.mz_exp_all()
598        mz_error = self.mz_error_all()
599
600        ax.scatter(mz_assigned, mz_error, c=color)
601
602        ax.set_xlabel("$\t{m/z}$", fontsize=12)
603        ax.set_ylabel("Error (ppm)", fontsize=12)
604        ax.tick_params(axis="both", which="major", labelsize=12)
605
606        ax.axes.spines["top"].set_visible(True)
607        ax.axes.spines["right"].set_visible(True)
608
609        ax.get_yaxis().set_visible(True)
610        ax.spines["left"].set_visible(True)
611
612        ax.set_xlim(self.min_max_mz)
613        ax.set_ylim(self.min_ppm_error, self.max_ppm_error)
614
615        return ax

Plot mz error scatter plot of all classes

Parameters
  • color (str, optional): Matplotlib color, by default "g"
Returns
  • ax (matplotlib.axes): Matplotlib axes object
def plot_mz_error_class(self, classe, color='g'):
617    def plot_mz_error_class(self, classe, color="g"):
618        """Plot mz error scatter plot of a specific class
619
620        Parameters
621        ----------
622        classe : str
623            Class name
624        color : str, optional
625            Matplotlib color, by default "g"
626
627        Returns
628        -------
629        ax : matplotlib.axes
630            Matplotlib axes object
631
632        """
633        if classe != Labels.unassigned:
634            ax = plt.gca()
635
636            abun_perc = self.abundance_count_percentile(classe)
637            mz_assigned = self.mz_exp(classe)
638            mz_error = self.mz_error(classe)
639
640            ax.scatter(mz_assigned, mz_error, c=color)
641
642            title = "%s, %.2f %%" % (classe, abun_perc)
643            ax.set_title(title)
644            ax.set_xlabel("$\t{m/z}$", fontsize=12)
645            ax.set_ylabel("Error (ppm)", fontsize=12)
646            ax.tick_params(axis="both", which="major", labelsize=12)
647
648            ax.axes.spines["top"].set_visible(True)
649            ax.axes.spines["right"].set_visible(True)
650
651            ax.get_yaxis().set_visible(True)
652            ax.spines["left"].set_visible(True)
653
654            ax.set_xlim(self.min_max_mz)
655            ax.set_ylim(self.min_ppm_error, self.max_ppm_error)
656
657            return ax

Plot mz error scatter plot of a specific class

Parameters
  • classe (str): Class name
  • color (str, optional): Matplotlib color, by default "g"
Returns
  • ax (matplotlib.axes): Matplotlib axes object
def plot_ms_class(self, classe, color='g'):
659    def plot_ms_class(self, classe, color="g"):
660        """Plot stick mass spectrum of a specific class
661
662        Parameters
663        ----------
664        classe : str
665            Class name
666        color : str, optional
667            Matplotlib color, by default "g"
668
669        Returns
670        -------
671        ax : matplotlib.axes
672            Matplotlib axes object
673
674        """
675        if classe != Labels.unassigned:
676            ax = plt.gca()
677
678            abun_perc = self.abundance_count_percentile(classe)
679            mz_assigned = self.mz_exp(classe)
680            abundance_assigned = self.abundance(classe)
681
682            for plot_obj in ax.stem(
683                mz_assigned, abundance_assigned, linefmt="-", markerfmt=" "
684            ):
685                plt.setp(plot_obj, "color", color, "linewidth", 2)
686
687            title = "%s, %.2f %%" % (classe, abun_perc)
688            ax.set_title(title)
689            ax.set_xlabel("$\t{m/z}$", fontsize=12)
690            ax.set_ylabel("Abundance", fontsize=12)
691            ax.tick_params(axis="both", which="major", labelsize=12)
692
693            ax.axes.spines["top"].set_visible(False)
694            ax.axes.spines["right"].set_visible(False)
695
696            ax.get_yaxis().set_visible(False)
697            ax.spines["left"].set_visible(False)
698
699            ax.set_xlim(self.min_max_mz)
700            ax.set_ylim(self.min_max_abundance)
701
702            return ax

Plot stick mass spectrum of a specific class

Parameters
  • classe (str): Class name
  • color (str, optional): Matplotlib color, by default "g"
Returns
  • ax (matplotlib.axes): Matplotlib axes object
def plot_van_krevelen( self, classe=None, max_hc=2.5, max_oc=2, ticks_number=5, color='viridis', alpha=0.5, log_abundance=False):
704    def plot_van_krevelen(
705        self,
706        classe=None,
707        max_hc=2.5,
708        max_oc=2,
709        ticks_number=5,
710        color="viridis",
711        alpha=0.5,
712        log_abundance=False,
713    ):
714        """Plot Van Krevelen Diagram for a single class or all assigned classes
715
716        Parameters
717        ----------
718        classe : str, optional
719            Class name or None to plot all assigned classes, by default None
720        max_hc : float, optional
721            Max H/C ratio, by default 2.5
722        max_oc : float, optional
723            Max O/C ratio, by default 2
724        ticks_number : int, optional
725            Number of ticks, by default 5
726        color : str, optional
727            Matplotlib color/colormap, by default "viridis"
728        alpha : float, optional
729            Transparency of points, by default 0.5
730        log_abundance : bool, optional
731            If True, use log10 scale for abundance values, by default False
732
733        Returns
734        -------
735        ax : matplotlib.axes
736            Matplotlib axes object
737        abun_perc : float or None
738            Class percentile of the relative abundance (if classe specified)
739        """
740        import numpy as np
741
742        ax = plt.gca()
743
744        if classe is not None and classe != Labels.unassigned:
745            # Single class plot
746            abun_perc = self.abundance_count_percentile(classe)
747            hc = self.atoms_ratio(classe, "H", "C")
748            oc = self.atoms_ratio(classe, "O", "C")
749            abundance = self.abundance(classe)
750
751            if log_abundance:
752                abundance = [np.log10(a + 1e-10) for a in abundance]
753                colorbar_label = "log\u2081\u2080(Abundance)"
754            else:
755                colorbar_label = "Abundance"
756
757            # Sort by abundance so higher values are plotted on top
758            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
759            hc = [hc[i] for i in sorted_indices]
760            oc = [oc[i] for i in sorted_indices]
761            abundance = [abundance[i] for i in sorted_indices]
762
763            scatter = ax.scatter(oc, hc, c=abundance, alpha=alpha, cmap=color)
764            plt.colorbar(scatter, label=colorbar_label)
765
766            title = "%s, %.2f %%" % (classe, abun_perc)
767            ax.set_title(title)
768
769            return_val = ax, abun_perc
770        else:
771            # All assigned classes plot
772            hc = self.atoms_ratio_all("H", "C")
773            oc = self.atoms_ratio_all("O", "C")
774            abundance = self.abundance_assigned()
775
776            if log_abundance:
777                abundance = [np.log10(a + 1e-10) for a in abundance]
778                colorbar_label = "log\u2081\u2080(Abundance)"
779            else:
780                colorbar_label = "Abundance"
781
782            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
783            hc = [hc[i] for i in sorted_indices]
784            oc = [oc[i] for i in sorted_indices]
785            abundance = [abundance[i] for i in sorted_indices]
786
787            scatter = ax.scatter(oc, hc, c=abundance, alpha=alpha, cmap=color)
788            plt.colorbar(scatter, label=colorbar_label)
789
790            ax.set_title("Van Krevelen Diagram - All Assigned Classes")
791
792            return_val = ax
793
794        ax.set_xlabel("O/C", fontsize=16)
795        ax.set_ylabel("H/C", fontsize=16)
796        ax.tick_params(axis="both", which="major", labelsize=18)
797        ax.set_xticks(linspace(0, max_oc, ticks_number, endpoint=True))
798        ax.set_yticks(linspace(0, max_hc, ticks_number, endpoint=True))
799        ax.grid(alpha=0.3, linestyle="--")
800
801        return return_val

Plot Van Krevelen Diagram for a single class or all assigned classes

Parameters
  • classe (str, optional): Class name or None to plot all assigned classes, by default None
  • max_hc (float, optional): Max H/C ratio, by default 2.5
  • max_oc (float, optional): Max O/C ratio, by default 2
  • ticks_number (int, optional): Number of ticks, by default 5
  • color (str, optional): Matplotlib color/colormap, by default "viridis"
  • alpha (float, optional): Transparency of points, by default 0.5
  • log_abundance (bool, optional): If True, use log10 scale for abundance values, by default False
Returns
  • ax (matplotlib.axes): Matplotlib axes object
  • abun_perc (float or None): Class percentile of the relative abundance (if classe specified)
def plot_dbe_vs_carbon_number( self, classe=None, max_c=50, max_dbe=40, dbe_incr=5, c_incr=10, color='viridis', alpha=0.5, log_abundance=False):
803    def plot_dbe_vs_carbon_number(
804        self,
805        classe=None,
806        max_c=50,
807        max_dbe=40,
808        dbe_incr=5,
809        c_incr=10,
810        color="viridis",
811        alpha=0.5,
812        log_abundance=False,
813    ):
814        """Plot DBE vs Carbon Number for a single class or all assigned classes
815
816        Parameters
817        ----------
818        classe : str, optional
819            Class name or None to plot all assigned classes, by default None
820        max_c : int, optional
821            Max Carbon Number, by default 50
822        max_dbe : int, optional
823            Max DBE, by default 40
824        dbe_incr : int, optional
825            DBE increment, by default 5
826        c_incr : int, optional
827            Carbon Number increment, by default 10
828        color : str, optional
829            Matplotlib color/colormap, by default "viridis"
830        alpha : float, optional
831            Transparency of points, by default 0.5
832        log_abundance : bool, optional
833            If True, use log10 scale for abundance values, by default False
834
835        Returns
836        -------
837        ax : matplotlib.axes
838            Matplotlib axes object
839        abun_perc : float or None
840            Class percentile of the relative abundance (if classe specified)
841        """
842        import numpy as np
843
844        ax = plt.gca()
845
846        if classe is not None and classe != Labels.unassigned:
847            # Single class plot
848            abun_perc = self.abundance_count_percentile(classe)
849            carbon_number = self.carbon_number(classe)
850            dbe = self.dbe(classe)
851            abundance = self.abundance(classe)
852
853            if log_abundance:
854                abundance = [np.log10(a + 1e-10) for a in abundance]
855                colorbar_label = "log\u2081\u2080(Abundance)"
856            else:
857                colorbar_label = "Abundance"
858
859            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
860            carbon_number = [carbon_number[i] for i in sorted_indices]
861            dbe = [dbe[i] for i in sorted_indices]
862            abundance = [abundance[i] for i in sorted_indices]
863
864            scatter = ax.scatter(
865                carbon_number, dbe, c=abundance, alpha=alpha, cmap=color
866            )
867            plt.colorbar(scatter, label=colorbar_label)
868
869            title = "%s, %.2f %%" % (classe, abun_perc)
870            ax.set_title(title)
871
872            return_val = ax, abun_perc
873        else:
874            # All assigned classes plot
875            carbon_number = self.carbon_number_all()
876            dbe = self.dbe_all()
877            abundance = self.abundance_assigned()
878
879            if log_abundance:
880                abundance = [np.log10(a + 1e-10) for a in abundance]
881                colorbar_label = "log\u2081\u2080(Abundance)"
882            else:
883                colorbar_label = "Abundance"
884
885            sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
886            carbon_number = [carbon_number[i] for i in sorted_indices]
887            dbe = [dbe[i] for i in sorted_indices]
888            abundance = [abundance[i] for i in sorted_indices]
889
890            scatter = ax.scatter(
891                carbon_number, dbe, c=abundance, alpha=alpha, cmap=color
892            )
893            plt.colorbar(scatter, label=colorbar_label)
894
895            ax.set_title("DBE vs Carbon Number - All Assigned Classes")
896
897            return_val = ax
898
899        ax.set_xlabel("Carbon number", fontsize=16)
900        ax.set_ylabel("DBE", fontsize=16)
901        ax.tick_params(axis="both", which="major", labelsize=18)
902        ax.set_xticks(range(0, max_c, c_incr))
903        ax.set_yticks(range(0, max_dbe, dbe_incr))
904        ax.grid(alpha=0.3, linestyle="--")
905
906        return return_val

Plot DBE vs Carbon Number for a single class or all assigned classes

Parameters
  • classe (str, optional): Class name or None to plot all assigned classes, by default None
  • max_c (int, optional): Max Carbon Number, by default 50
  • max_dbe (int, optional): Max DBE, by default 40
  • dbe_incr (int, optional): DBE increment, by default 5
  • c_incr (int, optional): Carbon Number increment, by default 10
  • color (str, optional): Matplotlib color/colormap, by default "viridis"
  • alpha (float, optional): Transparency of points, by default 0.5
  • log_abundance (bool, optional): If True, use log10 scale for abundance values, by default False
Returns
  • ax (matplotlib.axes): Matplotlib axes object
  • abun_perc (float or None): Class percentile of the relative abundance (if classe specified)