"""Module for representation and analysis of MS measurements"""
import re
import numpy as np
from ..measurement_base import Measurement
from ..spectra import Spectrum, SpectrumSeries, SpectroMeasurement
from ..plotters import MSPlotter, MSSpectroPlotter
from ..plotters.ms_plotter import STANDARD_COLORS
from ..exporters import MSExporter, MSSpectroExporter
from ..tools import deprecate
from ..plugins import plugins
from ..calculators.ms_calculators import (
MSCalResult,
MSBackgroundSet,
MSCalibration,
MSConstantBackground,
)
# and, for back-compatibility until 0.3.1:
from ..calculators.ms_calculators import MSInlet # noqa: F401
[docs]class MSMeasurement(Measurement):
"""Class implementing raw MS functionality"""
default_plotter = MSPlotter
default_exporter = MSExporter
background_calculator_types = [MSBackgroundSet]
def __init__(self, name, **kwargs):
tspan_bg = kwargs.pop("tspan_bg", None)
super().__init__(name, **kwargs)
self.tspan_bg = tspan_bg
self._siq_quantifier = None # Used with external quantification package
@property
def ms_calibration(self):
return self.calculators["MS calibration"]
@property
def siq_calculator(self):
return self.calculators["siq calculator"]
@property
def signal_bgs(self):
return self.calculators["MS background"]
[docs] def set_bg(self, tspan=None, tspan_bg=None, mass_list=None):
"""Set background values for mass_list to the average signal during tspan_bg."""
tspan = tspan or tspan_bg
background = MSBackgroundSet.from_measurement_point(
measurement=self, tspan=tspan, mass_list=mass_list
)
self.add_calculator(background)
[docs] def reset_bg(self, mass_list=None):
"""Reset background values for all masses or the masses in mass_list"""
if mass_list is None:
new_calculator_list = [
cal
for cal in self.calculator_list
if type(cal) not in self.background_calculator_types
]
self._calculator_list = new_calculator_list
self.consolidate_calculators()
else:
self.add_calculator(
MSBackgroundSet(
bg_list=[MSConstantBackground(mass=mass, bg=0) for mass in mass_list]
)
)
[docs] def grab_signal(self, *args, **kwargs):
"""Alias for grab()"""
return self.grab(*args, **kwargs)
[docs] @deprecate(
"0.1", "Use `remove_background` instead.", "0.3.1", kwarg_name="removebackground"
)
def grab_flux(
self,
mol,
tspan=None,
tspan_bg=None,
remove_background=True,
removebackground=None,
include_endpoints=False,
):
"""Return the flux of mol (calibrated signal) in [mol/s]
Note:
- With native ixdat quantification (use_siq=False),
`grab_flux(mol, ...)` is identical to `grab(f"n_dot_{mol}", ...)` with
remove_background=True by default. An MSCalibration does the maths.
- With an external quantification package (use_siq=True), the maths are done
here with the help of self.quantifier
Args:
mol (str or MSCalResult): Name of the molecule or a ms_calibration thereof
tspan (list): Timespan for which the signal is returned.
tspan_bg (list): Timespan that corresponds to the background signal.
If not given, no background is subtracted.
remove_background (bool): Whether to remove a pre-set background if available
Defaults to True.
removebackground (bool): DEPRECATED. Use `remove_background`.
include_endpoints (bool): Whether to interpolate for tspan[0] and tspan[-1]
"""
if removebackground is not None:
remove_background = removebackground
if isinstance(mol, MSCalResult):
t, signal = self.grab(
mol.mass,
tspan=tspan,
tspan_bg=tspan_bg,
remove_background=remove_background,
include_endpoints=include_endpoints,
)
return t, signal / mol.F
return self.grab(
# grab() invokes __getitem__, which invokes the `Calibration`. Specifically,
# `MSCalibration.calculate_series()` interprets item names starting with
# "n_" as molecule fluxes, and checks itself for a sensitivity factor.
f"n_dot_{mol}",
tspan=tspan,
tspan_bg=tspan_bg,
remove_background=remove_background,
include_endpoints=include_endpoints,
)
[docs] @deprecate(
"0.1", "Use `remove_background` instead.", "0.3.1", kwarg_name="removebackground"
)
def grab_flux_for_t(
self,
mol,
t,
tspan_bg=None,
remove_background=False,
removebackground=None,
):
"""Return the flux of mol (calibrated signal) in [mol/s] for a given time vec
Args:
mol (str): Name of the molecule.
t (np.array): The time vector along which to give the flux
tspan_bg (tspan): Timespan that corresponds to the background signal.
If not given, no background is subtracted.
remove_background (bool): Whether to remove a pre-set background if available
removebackground (bool): DEPRECATED. Use `remove_background`.
"""
if removebackground is not None:
remove_background = removebackground
t_0, y_0 = self.grab_flux(
mol,
tspan_bg=tspan_bg,
remove_background=remove_background,
)
y = np.interp(t, t_0, y_0)
return y
[docs] def get_flux_series(self, mol):
"""Return a ValueSeries with the calibrated flux of mol"""
return self[f"n_dot_{mol}"]
[docs] def integrate_signal(self, mass, tspan, tspan_bg, ax=None):
"""Integrate a ms signal with background subtraction and evt. plotting
TODO: Should this, like grab_signal does now, have the option of using a
background saved in the object rather than calculating a new one?
Args:
mass (str): The mass for which to integrate the signal
tspan (tspan): The timespan over which to integrate
tspan_bg (tspan): Timespan at which the signal is at its background value
ax (Axis): axis to plot on. Defaults to None
"""
t, S = self.grab_signal(mass, tspan=tspan, include_endpoints=True)
if tspan_bg:
t_bg, S_bg_0 = self.grab_signal(mass, tspan=tspan_bg, include_endpoints=True)
S_bg = np.mean(S_bg_0) * np.ones(t.shape)
else:
S_bg = np.zeros(t.shape)
if ax:
if ax == "new":
fig, ax = self.plotter.new_ax()
ax.fill_between(t, S_bg, S, color=STANDARD_COLORS[mass], alpha=0.2)
return np.trapz(S - S_bg, t)
[docs] def integrate_flux(self, mol, tspan, tspan_bg, ax=None):
"""Integrate a calibrated ms signal with background subtraction and evt.
plotting (copy of integrate_signal method)
TODO: Should this, like grab_signal does now, have the option of using a
background saved in the object rather than calculating a new one?
TODO: Ensure fill_between considers the non-standard unit in the figure
Args:
mol (str): The molecule name for which to integrate the signal
tspan (tspan): The timespan over which to integrate
tspan_bg (tspan): Timespan at which the signal is at its background value
ax (Axis): axis to plot on. Defaults to None
"""
t, S = self.grab_flux(mol, tspan=tspan, include_endpoints=True)
if tspan_bg:
t_bg, S_bg_0 = self.grab_flux(mol, tspan=tspan_bg, include_endpoints=True)
S_bg = np.mean(S_bg_0) * np.ones(t.shape)
else:
S_bg = np.zeros(t.shape)
if ax:
if ax == "new":
fig, ax = self.plotter.new_ax()
ax.fill_between(t, S_bg, S, color=STANDARD_COLORS[mol], alpha=0.2)
return np.trapz(S - S_bg, t)
@property
def mass_list(self):
"""List of the masses for which ValueSeries are contained in the measurement"""
return [self.as_mass(col) for col in self.series_names if self.is_mass(col)]
def is_mass(self, item):
if re.search("^M[0-9]+$", item):
return True
if item in self.reverse_aliases and self.is_mass(self.reverse_aliases[item][0]):
return True
return False
def as_mass(self, item):
if re.search("^M[0-9]+$", item):
return item
new_item = self.reverse_aliases[item][0]
if self.is_mass(new_item):
return self.as_mass(new_item)
raise TypeError(f"{self!r} does not recognize '{item}' as a mass.")
# --- METHODS WHICH HAVE BEEN MOVED TO `Calculator` CLASSES ---- #
@deprecate(
"0.2.13",
"Use `MSCalibration.gas_flux_calibration` instead.",
"0.3.1",
)
def gas_flux_calibration(self, *args, **kwargs):
return MSCalibration.gas_flux_calibration(measurement=self, *args, **kwargs)
@deprecate(
"0.2.13",
"Use `MSCalibration.gas_flux_calibration_curve` instead.",
"0.3.1",
)
def gas_flux_calibration_curve(self, *args, **kwargs):
return MSCalibration.gas_flux_calibration_curve(
measurement=self, *args, **kwargs
)
@deprecate(
"0.2.13",
"Use `plugins.siq.Calculator.gas_flux_calibration` instead.",
"0.3.1",
)
def siq_gas_flux_calibration(self, mol, mass, tspan, chip=None):
return plugins.siq.Calculator.gas_flux_calibration(
measurement=self, mol=mol, mass=mass, tspan=tspan, chip=None
)
@deprecate(
"0.2.13",
"Use `plugins.siq.Calculator.gas_flux_calibration_curve` instead.",
"0.3.1",
)
def siq_gas_flux_calibration_curve(
self,
*args,
**kwargs,
):
return plugins.siq.Calculator.gas_flux_calibration_curve(
measurement=self, *args, **kwargs
)
@deprecate(
"0.2.13",
"Use `plugins.siq.Calculator.multicomp_gas_flux_calibration` instead.",
"0.3.1",
)
def siq_multicomp_gas_flux_calibration(self, *args, **kwargs):
return plugins.siq.Calculator.siq_multicomp_gas_flux_calibration(
measurement=self, *args, **kwargs
)
@deprecate(
"0.2.13",
"Use `siq_calculator.grab_fluxes` instead.",
"0.3.1",
)
def grab_siq_fluxes(
self,
tspan=None,
tspan_bg=None,
remove_background=False,
include_endpoints=False,
):
return self.siq_calculator.grab_fluxes(
tspan=None, tspan_bg=None, remove_background=False, include_endpoints=False
)
[docs]class MSSpectrum(Spectrum):
"""Nothing to add to normal Spectrum yet.
TODO: Methods for co-plotting ref spectra from a database
"""
pass
[docs]class MSSpectrumSeries(SpectrumSeries):
"""Nothing to add to normal SpectrumSeries yet."""
pass
[docs]class MSSpectroMeasurement(MSMeasurement, SpectroMeasurement):
extra_column_attrs = SpectroMeasurement.extra_column_attrs
default_plotter = MSSpectroPlotter
default_exporter = MSSpectroExporter
# FIXME: https://github.com/ixdat/ixdat/pull/166#discussion_r1486023530