"""Module for representation and analysis of EC-MS measurements"""
import numpy as np
from ..constants import FARADAY_CONSTANT
from .ec import ECMeasurement
from .ms import MSMeasurement, MSCalResult
from .cv import CyclicVoltammogram
from ..exporters.ecms_exporter import ECMSExporter
from ..plotters.ms_plotter import STANDARD_COLORS
from ..db import Saveable # FIXME: doesn't belong here.
import json # FIXME: doesn't belong here.
[docs]class ECMSMeasurement(ECMeasurement, MSMeasurement):
"""Class for raw EC-MS functionality. Parents: ECMeasurement and MSMeasurement"""
extra_column_attrs = {
# FIXME: It would be more elegant if this carried over from both parents
# That might require some custom inheritance definition...
"ecms_meaurements": {
"mass_aliases",
"signal_bgs",
"ec_technique",
"RE_vs_RHE",
"R_Ohm",
"raw_potential_names",
"A_el",
"raw_current_names",
},
}
def __init__(self, **kwargs):
if "calibration" in kwargs and kwargs["calibration"]:
self.calibration = kwargs["calibration"]
else:
# FIXME: This is a slight mess.
# ECMeasurement should also have RE_vs_RHE and A_el in a calibration
self.calibration = ECMSCalibration()
"""FIXME: Passing the right key-word arguments on is a mess"""
ec_kwargs = {
k: v for k, v in kwargs.items() if k in ECMeasurement.get_all_column_attrs()
}
ms_kwargs = {
k: v for k, v in kwargs.items() if k in MSMeasurement.get_all_column_attrs()
}
ms_kwargs["calibration"] = self.calibration # FIXME: This is a mess.
# FIXME: I think the lines below could be avoided with a PlaceHolderObject that
# works together with MemoryBackend
if "series_list" in kwargs:
ec_kwargs.update(series_list=kwargs["series_list"])
ms_kwargs.update(series_list=kwargs["series_list"])
if "component_measurements" in kwargs:
ec_kwargs.update(component_measurements=kwargs["component_measurements"])
ms_kwargs.update(component_measurements=kwargs["component_measurements"])
ECMeasurement.__init__(self, **ec_kwargs)
MSMeasurement.__init__(self, **ms_kwargs)
self._ec_plotter = None
self._ms_plotter = None
@property
def plotter(self):
"""The default plotter for ECMSMeasurement is ECMSPlotter"""
if not self._plotter:
from ..plotters.ecms_plotter import ECMSPlotter
self._plotter = ECMSPlotter(measurement=self)
return self._plotter
@property
def ec_plotter(self):
"""The default plotter for ECMSMeasurement is ECMSPlotter"""
if not self._ec_plotter:
from ..plotters.ec_plotter import ECPlotter
self._ec_plotter = ECPlotter(measurement=self)
return self._ec_plotter
@property
def ms_plotter(self):
"""The default plotter for ECMSMeasurement is ECMSPlotter"""
if not self._ms_plotter:
from ..plotters.ms_plotter import MSPlotter
self._ms_plotter = MSPlotter(measurement=self)
return self._ms_plotter
@property
def exporter(self):
"""The default plotter for ECMSMeasurement is ECMSExporter"""
if not self._exporter:
self._exporter = ECMSExporter(measurement=self)
return self._exporter
[docs] def as_dict(self):
self_as_dict = super().as_dict()
if self.calibration:
self_as_dict["calibration"] = self.calibration.as_dict()
# FIXME: necessary because an ECMSCalibration is not serializeable
# If it it was it would go into extra_column_attrs
return self_as_dict
[docs] @classmethod
def from_dict(cls, obj_as_dict):
"""Unpack the ECMSCalibration when initiating from a dict"""
if "calibration" in obj_as_dict:
if isinstance(obj_as_dict["calibration"], dict):
# FIXME: This is a mess
obj_as_dict["calibration"] = ECMSCalibration.from_dict(
obj_as_dict["calibration"]
)
obj = super(ECMSMeasurement, cls).from_dict(obj_as_dict)
return obj
[docs] def as_cv(self):
self_as_dict = self.as_dict()
# FIXME: The following lines are only necessary because
# PlaceHolderObject.get_object isn't able to find things in the MemoryBackend
del self_as_dict["s_ids"]
self_as_dict["series_list"] = self.series_list
return ECMSCyclicVoltammogram.from_dict(self_as_dict)
[docs] def ecms_calibration(self, mol, mass, n_el, tspan, tspan_bg=None):
"""Calibrate for mol and mass based on one period of steady electrolysis
Args:
mol (str): Name of the molecule to calibrate
mass (str): Name of the mass at which to calibrate
n_el (str): Number of electrons passed per molecule produced (remember the
sign! e.g. +4 for O2 by OER and -2 for H2 by HER)
tspan (tspan): The timespan of steady electrolysis
tspan_bg (tspan): The time to use as a background
Return MSCalResult: The result of the calibration
"""
Y = self.integrate_signal(mass, tspan=tspan, tspan_bg=tspan_bg)
Q = self.integrate("raw current / [mA]", tspan=tspan) * 1e-3
n = Q / (n_el * FARADAY_CONSTANT)
F = Y / n
cal = MSCalResult(
name=f"{mol}_{mass}", mol=mol, mass=mass, cal_type="ecms_calibration", F=F,
)
return cal
[docs] def ecms_calibration_curve(
self,
mol,
mass,
n_el,
tspan_list=None,
tspan_bg=None,
ax="new",
axes_measurement=None,
):
"""Fit mol's sensitivity at mass based on steady periods of EC production
Args:
mol (str): Name of the molecule to calibrate
mass (str): Name of the mass at which to calibrate
n_el (str): Number of electrons passed per molecule produced (remember the
sign! e.g. +4 for O2 by OER and -2 for H2 by HER)
tspan_list (list of tspan): THe timespans of steady electrolysis
tspan_bg (tspan): The time to use as a background
ax (Axis): The axis on which to plot the calibration curve result. Defaults
to a new axis.
axes_measurement (list of Axes): The EC-MS plot axes to highlight the
calibration on. Defaults to None.
Return MSCalResult(, Axis(, Axis)): The result of the calibration
(and requested axes)
"""
axis_ms = axes_measurement[0] if axes_measurement else None
axis_current = axes_measurement[0] if axes_measurement else None
Y_list = []
n_list = []
for tspan in tspan_list:
Y = self.integrate_signal(mass, tspan=tspan, tspan_bg=tspan_bg, ax=axis_ms)
# FIXME: plotting current by giving integrate() an axis doesn't work great.
Q = self.integrate("raw current / [mA]", tspan=tspan) * 1e-3
n = Q / (n_el * FARADAY_CONSTANT)
Y_list.append(Y)
n_list.append(n)
n_vec = np.array(n_list)
Y_vec = np.array(Y_list)
pfit = np.polyfit(n_vec, Y_vec, deg=1)
F = pfit[0]
if ax:
color = STANDARD_COLORS[mass]
if ax == "new":
ax = self.plotter.new_ax()
ax.set_xlabel("amount produced / [nmol]")
ax.set_ylabel("integrated signal / [nC]")
ax.plot(n_vec * 1e9, Y_vec * 1e9, "o", color=color)
n_fit = np.array([0, max(n_vec)])
Y_fit = n_fit * pfit[0] + pfit[1]
ax.plot(n_fit * 1e9, Y_fit * 1e9, "--", color=color)
cal = MSCalResult(
name=f"{mol}_{mass}",
mol=mol,
mass=mass,
cal_type="ecms_calibration_curve",
F=F,
)
if ax:
if axes_measurement:
return cal, ax, axes_measurement
return cal, ax
return cal
[docs]class ECMSCyclicVoltammogram(CyclicVoltammogram, ECMSMeasurement):
"""Class for raw EC-MS functionality. Parents: CyclicVoltammogram, ECMSMeasurement
"""
[docs]class ECMSCalibration(Saveable):
"""Class for calibrations useful for ECMSMeasurements
FIXME: A class in a technique module shouldn't inherit directly from Saveable. We
need to generalize calibration somehow.
Also, ECMSCalibration should inherit from or otherwise use a class MSCalibration
"""
column_attrs = {"name", "date", "setup", "ms_cal_results", "RE_vs_RHE", "A_el", "L"}
# FIXME: Not given a table_name as it can't save to the database without
# MSCalResult's being json-seriealizeable. Exporting and reading works, though :D
def __init__(
self,
name=None,
date=None,
setup=None,
ms_cal_results=None,
RE_vs_RHE=None,
A_el=None,
L=None,
):
"""
Args:
name (str): Name of the calibration
date (str): Date of the calibration
setup (str): Name of the setup where the calibration is made
ms_cal_results (list of MSCalResult): The mass spec calibrations
RE_vs_RHE (float): the RE potential in [V]
A_el (float): the geometric electrode area in [cm^2]
L (float): the working distance in [m]
"""
super().__init__()
self.name = name or f"EC-MS calibration for {setup} on {date}"
self.date = date
self.setup = setup
self.ms_cal_results = ms_cal_results or []
self.RE_vs_RHE = RE_vs_RHE
self.A_el = A_el
self.L = L
[docs] def as_dict(self):
"""Have to dict the MSCalResults to get serializable as_dict (see Saveable)"""
self_as_dict = super().as_dict()
self_as_dict["ms_cal_results"] = [cal.as_dict() for cal in self.ms_cal_results]
return self_as_dict
[docs] @classmethod
def from_dict(cls, obj_as_dict):
"""Unpack the MSCalResults when initiating from a dict"""
obj = super(ECMSCalibration, cls).from_dict(obj_as_dict)
obj.ms_cal_results = [
MSCalResult.from_dict(cal_as_dict) for cal_as_dict in obj.ms_cal_results
]
return obj
[docs] def export(self, path_to_file=None):
"""Export an ECMSCalibration as a json-formatted text file"""
path_to_file = path_to_file or (self.name + ".ix")
self_as_dict = self.as_dict()
with open(path_to_file, "w") as f:
json.dump(self_as_dict, f, indent=4)
[docs] @classmethod
def read(cls, path_to_file):
"""Read an ECMSCalibration from a json-formatted text file"""
with open(path_to_file) as f:
obj_as_dict = json.load(f)
return cls.from_dict(obj_as_dict)
@property
def mol_list(self):
return list({cal.mol for cal in self.ms_cal_results})
@property
def mass_list(self):
return list({cal.mass for cal in self.ms_cal_results})
@property
def name_list(self):
return list({cal.name for cal in self.ms_cal_results})
def __contains__(self, mol):
return mol in self.mol_list or mol in self.name_list
def __iter__(self):
yield from self.ms_cal_results
[docs] def get_mass_and_F(self, mol):
"""Return the mass and sensitivity factor to use for simple quant. of mol"""
cal_list_for_mol = [cal for cal in self if cal.mol == mol or cal.name == mol]
Fs = [cal.F for cal in cal_list_for_mol]
index = np.argmax(np.array(Fs))
the_good_cal = cal_list_for_mol[index]
return the_good_cal.mass, the_good_cal.F
[docs] def get_F(self, mol, mass):
"""Return the sensitivity factor for mol at mass"""
cal_list_for_mol_at_mass = [
cal
for cal in self
if (cal.mol == mol or cal.name == mol) and cal.mass == mass
]
F_list = [cal.F for cal in cal_list_for_mol_at_mass]
return np.mean(np.array(F_list))
[docs] def scaled_to(self, ms_cal_result):
"""Return a new calibration w scaled sensitivity factors to match one given"""
F_0 = self.get_F(ms_cal_result.mol, ms_cal_result.mass)
scale_factor = ms_cal_result.F / F_0
calibration_as_dict = self.as_dict()
new_cal_list = []
for cal in self.ms_cal_results:
cal = MSCalResult(
name=cal.name,
mass=cal.mass,
mol=cal.mol,
F=cal.F * scale_factor,
cal_type=cal.cal_type + " scaled",
)
new_cal_list.append(cal)
calibration_as_dict["ms_cal_results"] = [cal.as_dict() for cal in new_cal_list]
calibration_as_dict["name"] = calibration_as_dict["name"] + " scaled"
return self.__class__.from_dict(calibration_as_dict)