Source code for pyms.Display

"""
Class to Display Ion Chromatograms and TIC.
"""

################################################################################
#                                                                              #
#    PyMassSpec software for processing of mass-spectrometry data              #
#    Copyright (C) 2005-2012 Vladimir Likic                                    #
#    Copyright (C) 2019-2020 Dominic Davis-Foster                              #
#                                                                              #
#    This program is free software; you can redistribute it and/or modify      #
#    it under the terms of the GNU General Public License version 2 as         #
#    published by the Free Software Foundation.                                #
#                                                                              #
#    This program is distributed in the hope that it will be useful,           #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of            #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             #
#    GNU General Public License for more details.                              #
#                                                                              #
#    You should have received a copy of the GNU General Public License         #
#    along with this program; if not, write to the Free Software               #
#    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.                 #
#                                                                              #
################################################################################

# stdlib
import warnings
from typing import Dict, List, Optional, Tuple

# 3rd party
import deprecation  # type: ignore[import]
import matplotlib  # type: ignore[import]
import matplotlib.pyplot as plt  # type: ignore[import]
from matplotlib.axes import Axes  # type: ignore[import]
from matplotlib.container import BarContainer  # type: ignore[import]
from matplotlib.figure import Figure  # type: ignore[import]
from matplotlib.lines import Line2D  # type: ignore[import]

# this package
from pyms import Peak, __version__
from pyms.IonChromatogram import IonChromatogram
from pyms.Peak.List.Function import is_peak_list
from pyms.Spectrum import MassSpectrum, normalize_mass_spec

__all__ = [
		"Display",
		"plot_ic",
		"plot_mass_spec",
		"plot_head2tail",
		"plot_peaks",
		"ClickEventHandler",
		"invert_mass_spec",
		]

default_filetypes = ["png", "pdf", "svg"]

# Ensure that the intersphinx links are correct.
Axes.__module__ = "matplotlib.axes"
Figure.__module__ = "matplotlib.figure"


[docs]class Display: """ Class to display Ion Chromatograms and Total Ion Chromatograms from :class:`pyms.IonChromatogram.IonChromatogram` using :mod:`matplotlib.pyplot`. :param fig: figure object to use :param ax: axes object to use If ``fig`` is not given then ``fig`` and ``ax`` default to: >>> fig = plt.figure() >>> ax = fig.add_subplot(111) If only ``fig`` is given then ``ax`` defaults to: >>> ax = fig.add_subplot(111) :author: Sean O'Callaghan :author: Vladimir Likic :author: Dominic Davis-Foster """ # noqa: D400 @deprecation.deprecated( "2.2.8", "2.4.0", current_version=__version__, details="Functionality has moved to other functions and classes in this module.", ) def __init__(self, fig: Figure = None, ax: Axes = None): if fig is None: fig = plt.figure() ax = fig.add_subplot(111) elif isinstance(fig, matplotlib.figure.Figure) and ax is None: ax = fig.add_subplot(111) if not isinstance(fig, matplotlib.figure.Figure): raise TypeError("'fig' must be a matplotlib.figure.Figure object") if not isinstance(ax, matplotlib.axes.Axes): raise TypeError("'ax' must be a matplotlib.axes.Axes object") self.fig = fig self.ax = ax # Container to store plots self.__tic_ic_plots: List[List[Line2D]] = [] # Peak list container self.__peak_list: List[Peak.Peak] = []
[docs] def do_plotting(self, plot_label: Optional[str] = None) -> None: """ Plots TIC and IC(s) if they have been created by :meth:`~pyms.Display.Display.plot_tic` or :meth:`~pyms.Display.Display.plot_ic`. Also adds detected peaks if they have been added by :meth:`~pyms.Display.Display.plot_peaks` :param plot_label: Label for the plot to show e.g. the data origin """ # noqa: D400 # if no plots have been created advise user if len(self.__tic_ic_plots) == 0: warnings.warn( """No plots have been created. Please call a plotting function before calling 'do_plotting()'""", UserWarning, ) return if plot_label is not None: self.ax.set_title(plot_label) self.ax.legend() self.fig.canvas.draw() # If no peak list plot, no mouse click event if len(self.__peak_list) != 0: self.fig.canvas.mpl_connect("button_press_event", self.onclick)
# plt.show()
[docs] @staticmethod def get_5_largest(intensity_list: List[float]) -> List[int]: """ Returns the indices of the 5 largest ion intensities. :param intensity_list: List of Ion intensities """ largest = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # Find out largest value for idx, intensity in enumerate(intensity_list): if intensity > intensity_list[largest[0]]: largest[0] = idx # Now find next four largest values for j in [1, 2, 3, 4, 5, 6, 7, 8, 9]: for idx, intensity in enumerate(intensity_list): # if intensity_list[i] > intensity_list[largest[j]] and intensity_list[i] < intensity_list[largest[j-1]]: if intensity_list[largest[j]] < intensity < intensity_list[largest[j - 1]]: largest[j] = idx return largest
[docs] def onclick(self, event) -> None: """ Finds the 5 highest intensity m/z channels for the selected peak. The peak is selected by clicking on it. If a button other than the left one is clicked, a new plot of the mass spectrum is displayed. :param event: a mouse click by the user """ intensity_list = [] mass_list = [] for peak in self.__peak_list: # if event.xdata > 0.9999*peak.rt and event.xdata < 1.0001*peak.rt: if 0.9999 * peak.rt < event.xdata < 1.0001 * peak.rt: intensity_list = peak.mass_spectrum.mass_spec mass_list = peak.mass_spectrum.mass_list largest = self.get_5_largest(intensity_list) if len(intensity_list) != 0: print("mass\t intensity") for i in range(10): print(mass_list[largest[i]], '\t', intensity_list[largest[i]]) else: # if the selected point is not close enough to peak print("No Peak at this point") # Check if a button other than left was pressed, if so plot mass spectrum # Also check that a peak was selected, not just whitespace if event.button != 1 and len(intensity_list) != 0: # self.plot_mass_spec(event.xdata, mass_list, intensity_list) self.plot_mass_spec(MassSpectrum(mass_list, intensity_list))
[docs] def plot_ic(self, ic: IonChromatogram, **kwargs) -> List[Line2D]: """ Plots an Ion Chromatogram. :param ic: Ion Chromatograms m/z channels for plotting :Other Parameters: :class:`matplotlib.lines.Line2D` properties. Used to specify properties like a line label (for auto legends), linewidth, antialiasing, marker face color. Example:: >>> plot_ic(im.get_ic_at_index(5), label='IC @ Index 5', linewidth=2) See https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.lines.Line2D.html for the list of possible kwargs """ plot = plot_ic(self.ax, ic, **kwargs) self.__tic_ic_plots.append(plot) return plot
[docs] def plot_mass_spec(self, mass_spec: MassSpectrum, **kwargs) -> BarContainer: """ Plots a Mass Spectrum. :param mass_spec: The mass spectrum at a given time/index :Other Parameters: :class:`matplotlib.lines.Line2D` properties. Used to specify properties like a line label (for auto legends), linewidth, antialiasing, marker face color. Example:: >>> plot_mass_spec(im.get_ms_at_index(5), linewidth=2) >>> ax.set_title(f"Mass spec for peak at time {im.get_time_at_index(5):5.2f}") See https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.lines.Line2D.html for the list of possible kwargs """ plot = plot_mass_spec(self.ax, mass_spec, **kwargs) return plot
[docs] def plot_peaks(self, peak_list: List[Peak.Peak], label: str = "Peaks") -> List[Line2D]: """ Plots the locations of peaks as found by PyMassSpec. :param peak_list: List of peaks to plot :param label: label for plot legend. """ plot = plot_peaks(self.ax, peak_list, label) # Copy to self.__peak_list for onclick event handling self.__peak_list = peak_list return plot
[docs] def plot_tic(self, tic: IonChromatogram, minutes: bool = False, **kwargs) -> List[Line2D]: """ Plots a Total Ion Chromatogram. :param tic: Total Ion Chromatogram. :param minutes: Whether to show the time in minutes. :Other Parameters: :class:`matplotlib.lines.Line2D` properties. Used to specify properties like a line label (for auto legends), linewidth, antialiasing, marker face color. Example:: >>> plot_tic(data.tic, label='TIC', linewidth=2) See https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.lines.Line2D.html for the list of possible kwargs """ if not isinstance(tic, IonChromatogram) or not tic.is_tic(): raise TypeError("'tic' must be an Ion Chromatogram object representing a total ion chromatogram") plot = plot_ic(self.ax, tic, minutes, **kwargs) self.__tic_ic_plots.append(plot) return plot
[docs] def save_chart(self, filepath: str, filetypes: Optional[List[str]] = None) -> None: """ Save the chart to the given path with the given filetypes. :param filepath: Path and filename to save the chart as. Should not include extension. :param filetypes: List of filetypes to use. :author: Dominic Davis-Foster """ # TODO: pathlib and remove extension if given & use that as filetype if filetypes is None: filetypes = default_filetypes # matplotlib.use("Agg") for filetype in filetypes: # plt.savefig(filepath + ".{}".format(filetype)) self.fig.savefig(filepath + f".{filetype}") plt.close()
[docs] def show_chart(self) -> None: """ Show the chart on screen. :author: Dominic Davis-Foster """ # matplotlib.use("TkAgg") self.fig.show() input("Press Enter to close the chart") plt.close()
[docs]def plot_ic(ax: matplotlib.axes.Axes, ic: IonChromatogram, minutes: bool = False, **kwargs) -> List[Line2D]: """ Plots an Ion Chromatogram. :param ax: The axes to plot the IonChromatogram on :param ic: Ion Chromatograms m/z channels for plotting :param minutes: Whether the x-axis should be plotted in minutes. Default False (plotted in seconds) :Other Parameters: :class:`matplotlib.lines.Line2D` properties. Used to specify properties like a line label (for auto legends), linewidth, antialiasing, marker face color. Example:: >>> plot_ic(im.get_ic_at_index(5), label='IC @ Index 5', linewidth=2) See https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.lines.Line2D.html for the list of possible kwargs :return: A list of Line2D objects representing the plotted data. """ if not isinstance(ic, IonChromatogram): raise TypeError("'ic' must be an IonChromatogram") time_list = ic.time_list if minutes: time_list = [time / 60 for time in time_list] plot = ax.plot(time_list, ic.intensity_array, **kwargs) # Set axis ranges ax.set_xlim(min(time_list), max(time_list)) ax.set_ylim(bottom=0) return plot
[docs]def plot_mass_spec(ax: Axes, mass_spec: MassSpectrum, **kwargs) -> BarContainer: """ Plots a Mass Spectrum. :param ax: The axes to plot the MassSpectrum on :param mass_spec: The mass spectrum to plot :Other Parameters: :class:`matplotlib.lines.Line2D` properties. Used to specify properties like a line label (for auto legends), linewidth, antialiasing, marker face color. Example:: >>> plot_mass_spec(im.get_ms_at_index(5), linewidth=2) >>> ax.set_title(f"Mass spec for peak at time {im.get_time_at_index(5):5.2f}") See https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.lines.Line2D.html for the list of possible kwargs :return: Container with all the bars and optionally errorbars. :rtype: :class:`matplotlib.container.BarContainer` """ if not isinstance(mass_spec, MassSpectrum): raise TypeError("'mass_spec' must be a MassSpectrum") mass_list = mass_spec.mass_list intensity_list = mass_spec.mass_spec if "width" not in kwargs: kwargs["width"] = 0.5 # to set x axis range find minimum and maximum m/z channels min_mz = mass_list[0] max_mz = mass_list[-1] for idx, mass in enumerate(mass_list): if mass_list[idx] > max_mz: max_mz = mass_list[idx] for idx, mass in enumerate(mass_list): if mass_list[idx] < min_mz: min_mz = mass_list[idx] plot = ax.bar(mass_list, intensity_list, **kwargs) # Set axis ranges ax.set_xlim(min_mz - 1, max_mz + 1) ax.set_ylim(bottom=0) return plot
[docs]def plot_head2tail( ax: Axes, top_mass_spec: MassSpectrum, bottom_mass_spec: MassSpectrum, top_spec_kwargs: Optional[Dict] = None, bottom_spec_kwargs: Optional[Dict] = None, ) -> Tuple[BarContainer, BarContainer]: """ Plots two mass spectra head to tail. :param ax: The axes to plot the MassSpectra on :param top_mass_spec: The Mass Spectrum to plot on top :param bottom_mass_spec: The Mass Spectrum to plot on the bottom :param top_spec_kwargs: A dictionary of keyword arguments for the top mass spectrum. Defaults to red with a line width of 0.5 :no-default top_spec_kwargs: :param bottom_spec_kwargs: A dictionary of keyword arguments for the bottom mass spectrum. Defaults to blue with a line width of 0.5 :no-default bottom_spec_kwargs: `top_spec_kwargs` and `bottom_spec_kwargs` are used to specify properties like a line label (for auto legends), linewidth, antialiasing, marker face color. See https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.lines.Line2D.html for the list of possible kwargs :return: A tuple of container with all the bars and optionally errorbars for the top and bottom spectra. :rtype: tuple of :class:`matplotlib.container.BarContainer` """ if not isinstance(top_mass_spec, MassSpectrum): raise TypeError("'top_mass_spec' must be a MassSpectrum") if not isinstance(bottom_mass_spec, MassSpectrum): raise TypeError("'bottom_mass_spec' must be a MassSpectrum") if top_spec_kwargs is None: top_spec_kwargs = dict(color="red", width=0.5) elif not isinstance(top_spec_kwargs, dict): raise TypeError("'top_spec_kwargs' must be a dictionary of keyword arguments for the top mass spectrum.") if bottom_spec_kwargs is None: bottom_spec_kwargs = dict(color="blue", width=0.5) elif not isinstance(bottom_spec_kwargs, dict): raise TypeError( "'bottom_spec_kwargs' must be a dictionary of keyword arguments for the bottom mass spectrum." ) # Plot a line at y=0 with same width and colour as Spines ax.axhline(y=0, color=ax.spines["bottom"].get_edgecolor(), linewidth=ax.spines["bottom"].get_linewidth()) # Normalize the mass spectra top_mass_spec = normalize_mass_spec(top_mass_spec) bottom_mass_spec = normalize_mass_spec(bottom_mass_spec) # Invert bottom mass spec invert_mass_spec(bottom_mass_spec, inplace=True) top_plot = plot_mass_spec(ax, top_mass_spec, **top_spec_kwargs) bottom_plot = plot_mass_spec(ax, bottom_mass_spec, **bottom_spec_kwargs) # Set ylim to 1.1 times max/min values ax.set_ylim( bottom=min(bottom_mass_spec.intensity_list) * 1.1, top=max(top_mass_spec.intensity_list) * 1.1, ) # ax.spines['bottom'].set_position('zero') return top_plot, bottom_plot
[docs]def plot_peaks(ax: Axes, peak_list: List[Peak.Peak], label: str = "Peaks", style: str = 'o') -> List[Line2D]: """ Plots the locations of peaks as found by PyMassSpec. :param ax: The axes to plot the peaks on :param peak_list: List of peaks to plot :param label: label for plot legend. :param style: The marker style. See `https://matplotlib.org/3.1.1/api/markers_api.html` for a complete list :return: A list of Line2D objects representing the plotted data. """ if not is_peak_list(peak_list): raise TypeError("'peak_list' must be a list of Peak objects") time_list = [] height_list = [] if "line" in style.lower(): lines = [] for peak in peak_list: lines.append(ax.axvline(x=peak.rt, color="lightgrey", alpha=0.8, linewidth=0.3)) return lines else: for peak in peak_list: time_list.append(peak.rt) height_list.append(sum(peak.mass_spectrum.intensity_list)) # height_list.append(peak.height) # print(peak.height - sum(peak.mass_spectrum.intensity_list)) # print(sum(peak.mass_spectrum.intensity_list)) return ax.plot(time_list, height_list, style, label=label)
# TODO: Change order of arguments and use plt.gca() a la pyplot
[docs]class ClickEventHandler: """ Class to enable clicking of chromatogram to view the intensities top n most intense ions at that peak, and viewing of the mass spectrum with a right click """ # noqa: D400 peak_list: List[Peak.Peak] fig: Figure ax: Axes ms_fig: Optional[Figure] ms_ax: Optional[Axes] n_intensities: int def __init__( self, peak_list: List[Peak.Peak], fig: Optional[Figure] = None, ax: Optional[Axes] = None, tolerance: float = 0.005, n_intensities: int = 5, ): if fig is None: self.fig = plt.gcf() else: self.fig = fig if ax is None: self.ax = plt.gca() else: self.ax = ax self.peak_list = peak_list self.ms_fig: Optional[Figure] = None self.ms_ax: Optional[Axes] = None self._min = 1 - tolerance self._max = 1 + tolerance self.n_intensities = n_intensities # If no peak list plot, no mouse click event if len(self.peak_list) != 0: self.cid = self.fig.canvas.mpl_connect("button_press_event", self.onclick) else: self.cid = None
[docs] def onclick(self, event) -> None: """ Finds the n highest intensity m/z channels for the selected peak. The peak is selected by clicking on it. If a button other than the left one is clicked, a new plot of the mass spectrum is displayed. :param event: a mouse click by the user """ for peak in self.peak_list: # if event.xdata > 0.9999*peak.rt and event.xdata < 1.0001*peak.rt: if self._min * peak.rt < event.xdata < self._max * peak.rt: intensity_list = peak.mass_spectrum.mass_spec mass_list = peak.mass_spectrum.mass_list largest = self.get_n_largest(intensity_list) print(f"RT: {peak.rt}") print("Mass\t Intensity") for i in range(self.n_intensities): print(f"{mass_list[largest[i]]}\t {intensity_list[largest[i]]}") # Check if right mouse button pressed, if so plot mass spectrum # Also check that a peak was selected, not just whitespace if event.button == 3 and len(intensity_list) != 0: # from pyms.Display import plot_mass_spec if self.ms_fig is None or self.ms_ax is None: self.ms_fig, self.ms_ax = plt.subplots(1, 1) else: self.ms_ax.clear() plot_mass_spec(self.ms_ax, peak.mass_spectrum) self.ms_ax.set_title(f"Mass Spectrum at RT {peak.rt}") self.ms_fig.show() # TODO: Add multiple MS to same plot window and add option to close one of them # TODO: Allow more interaction with MS, e.g. adjusting mass range? return # if the selected point is not close enough to peak print("No Peak at this point")
[docs] def get_n_largest(self, intensity_list: List[float]) -> List[int]: """ Computes the indices of the largest n ion intensities for writing to console. :param intensity_list: List of Ion intensities :return: Indices of largest ``n`` ion intensities """ largest = [0] * self.n_intensities # Find out largest value for idx, intensity in enumerate(intensity_list): if intensity > intensity_list[largest[0]]: largest[0] = idx # Now find next four largest values for j in list(range(1, self.n_intensities)): for idx, intensity in enumerate(intensity_list): # if intensity_list[i] > intensity_list[largest[j]] and intensity_list[i] < intensity_list[largest[j-1]]: if intensity_list[largest[j]] < intensity < intensity_list[largest[j - 1]]: largest[j] = idx return largest
[docs]def invert_mass_spec(mass_spec: MassSpectrum, inplace: bool = False) -> MassSpectrum: """ Invert the mass spectrum for display in a head2tail plot. :param mass_spec: The Mass Spectrum to normalize :param inplace: Whether the inversion should be applied to the :class:`~pyms.Spectrum.MassSpectrum` object given, or to a copy (default behaviour). :return: The normalized mass spectrum """ inverted_intensity_list = [-x for x in mass_spec.intensity_list] if inplace: mass_spec.intensity_list = inverted_intensity_list return mass_spec else: return MassSpectrum(mass_spec.mass_list, inverted_intensity_list)