"""Module for representation and analysis of EC measurements"""
from ..measurement_base import Measurement
from ..exporters import ECExporter
from ..plotters.ec_plotter import ECPlotter
from ..tools import deprecate
from ..calculators.ec_calculators import ECCalibration
[docs]class ECMeasurement(Measurement):
"""Class implementing electrochemistry measurements
TODO: Implement a unit library for current and potential, A_el and RE_vs_RHE
so that e.g. current can be seamlessly normalized to mass OR area.
The main job of this class is making sure that the ValueSeries most essential for
visualizing any normal electrochemistry measurements (i.e. excluding impedance
spec., RRDE, etc, which would need new classes) are always available in the
correct form as the measurement is added with others, reduced to a selection,
calibrated and normalized, etc. These most important ValueSeries are:
- `potential`: The working-electrode potential typically in [V].
If `ec_meas` is an `ECMeasurement`, then `ec_meas["potential"]` always returns a
`ValueSeries` characterized by:
- calibrated and/or corrected, if the measurement has been calibrated with the
reference electrode potential (`RE_vs_RHE`, see `calibrate`) and/or corrected
for ohmic drop (`R_Ohm`, see `correct_ohmic_drop`).
- A name that makes clear any calibration and/or correction
- Data which spans the entire timespan of the measurement - i.e. whenever EC
data is being recorded, `potential` is there, even if the name of the raw
`ValueSeries` (what the acquisition software calls it) changes. Indeed
`ec_meas["potential"].tseries` is the measurement's definitive time variable.
- `current`: The working-electrode current typically in [mA] or [mA/cm^2].
`ec_meas["current"]` always returns a `ValueSeries` characterized by:
- normalized if the measurement has been normalized with the electrode area
(`A_el`, see `normalize`)
- A name that makes clear whether it is normalized
- Data which spans the entire timespan of the measurement
- `selector`: A counter series distinguishing sections of the measurement program.
This is essential for analysis of complex measurements as it allows for
corresponding parts of experiments to be isolated and treated identically.
`selector` in `ECMeasurement` is defined to increment each time one or more of
the following changes:
- `loop_number`: A parameter saved by some potentiostats (e.g. BioLogic) which
allow complex looped electrochemistry programs.
- `file_number`: The id of the component measurement from which each section of
the data (the origin of each `ValueSeries` concatenated to `potential`)
- `cycle_number`: An incrementer within a file saved by a potentiostat.
The names of these ValueSeries, which can also be used to index the measurement, are
conveniently available as properties:
- `ec_meas.t_name` is the name of the definitive time, i.e. that of the potential
- `ec_meas.E_name` is the name of the raw potential
- `ec_meas.U_name` is the name of the calibrated and/or corrected potential
- `ec_meas.I_name` is the name of the raw current
- `ec_meas.J_name` is the name of the normalized current
- `ec_meas.selector_name` is the name of the default selector, i.e. "selector"
Numpy arrays from important `DataSeries` are directly accessible via attributes:
- `ec_meas.t` for `ec_meas["potential"].t`
- `ec_meas.U` for `ec_meas["potential"].data`
- `ec_meas.J` for `ec_meas["current"].data`
`ECMeasurement` comes with an `ECPlotter` which either plots `potential` and
`current` against time (`ec_meas.plot_measurement()`) or plots `current` against
`potential (`ec_meas.plot_vs_potential()`).
It turns out that keeping track of current, potential, and selector when combining
datasets is enough of a job to fill a class. Thus, the more exciting
electrochemistry-related functionality should be implemented in inheriting classes
such as `CyclicVoltammogram`.
"""
extra_column_attrs = {
"ec_meaurements": {
"ec_technique",
}
}
control_series_name = "raw_potential"
essential_series_names = ("t", "raw_potential", "raw_current")
selection_series_names = ("file_number", "loop_number", "cycle number", "Ns")
default_exporter = ECExporter
default_plotter = ECPlotter
default_calibration = ECCalibration
def __init__(
self,
name,
*,
ec_technique=None,
RE_vs_RHE=None,
R_Ohm=None,
A_el=None,
**kwargs,
):
"""initialize an electrochemistry measurement
Args:
name (str): The name of the measurement
ec_technique (str): The electrochemistry sub-technique
RE_vs_RHE (float): The reference electrode potential on the RHE scale in [V]
R_Ohm (float): The ohmic drop resistance in [Ohm]
A_el (float): The electrode area in [cm^2]
Kwargs, passed on to `Measurement.__init__` (see :class:`.Measurement`):
metadata (dict): Free-form measurement metadata. Must be json-compatible.
technique (str): The measurement technique
s_ids (list of int): The id's of the measurement's DataSeries, if
to be loaded (instead of given directly in series_list)
series_list (list of DataSeries): The measurement's DataSeries
m_ids (list of int): The id's of the component measurements, if to be
loaded. None unless this is a combined measurement (typically
corresponding to more than one file).
component_measurements (list of Measurements): The measurements of which
this measurement is a combination
reader (Reader): The file reader (None unless read from a file)
plotter (Plotter): The visualization tool for the measurement
exporter (Exporter): The exporting tool for the measurement
sample (Sample or str): The sample being measured
lablog (LabLog): The log entry with e.g. notes taken during the measurement
tstamp (float): The nominal starting time of the measurement, used for
data selection, visualization, and exporting.
"""
super().__init__(name, **kwargs)
self.ec_technique = ec_technique
if RE_vs_RHE is not None or A_el is not None or R_Ohm is not None:
self.calibrate(RE_vs_RHE, A_el, R_Ohm)
self.plot_vs_potential = self.plotter.plot_vs_potential
if "potential" not in self.aliases:
self._aliases.update({"potential": ["raw_potential"]})
if "current" not in self.aliases:
self._aliases.update({"current": ["raw_current"]})
@property
def E_name(self):
return self["raw_potential"].name
@property
def I_name(self):
return self["raw_current"].name
@property
def U_name(self):
return self.potential.name
@property
def J_name(self):
return self.current.name
@property
@deprecate("0.1", "Use `E_name` instead.", "0.3.1")
def E_str(self):
return self.E_name
@property
@deprecate("0.1", "Use `I_name` instead.", "0.3.1")
def I_str(self):
return self.I_name
@property
@deprecate("0.1", "Use `U_name` instead.", "0.3.1")
def V_str(self):
return self.U_name
@property
@deprecate("0.1", "Use `J_name` instead.", "0.3.1")
def J_str(self):
return self.J_name
@property
def aliases(self):
"""A dictionary with the names of other data series a given name can refer to"""
a = super().aliases.copy()
return a
@property
def ec_calibration(self):
"""A calibration joining the first RE_vs_RHE, A_el, and R_Ohm"""
return self.calculators["EC calibration"]
@property
def RE_vs_RHE(self):
"""The refernce electrode potential on the RHE scale in [V]"""
for calculator in self.calculator_list:
if getattr(calculator, "RE_vs_RHE", None) is not None:
return calculator.RE_vs_RHE
@property
def A_el(self):
"""The electrode area in [cm^2]"""
for calculator in self.calculator_list:
if getattr(calculator, "A_el", None) is not None:
return calculator.A_el
@property
def R_Ohm(self):
"""The ohmic drop resistance in [Ohm]"""
for calculator in self.calculator_list:
if getattr(calculator, "R_Ohm", None) is not None:
return calculator.R_Ohm
[docs] def calibrate_RE(self, RE_vs_RHE):
"""Calibrate the reference electrode by providing `RE_vs_RHE` in [V]."""
new_calibration = ECCalibration(
RE_vs_RHE=RE_vs_RHE,
measurement=self,
)
self.add_calculator(new_calibration)
[docs] def normalize_current(self, A_el):
"""Normalize current to electrode surface area by providing `A_el` in [cm^2]."""
new_calibration = ECCalibration(
A_el=A_el,
measurement=self,
)
self.add_calculator(new_calibration)
[docs] def correct_ohmic_drop(self, R_Ohm):
"""Correct for ohmic drop by providing `R_Ohm` in [Ohm]."""
new_calibration = ECCalibration(
R_Ohm=R_Ohm,
measurement=self,
)
self.add_calculator(new_calibration)
@property
def potential(self):
return self["potential"]
@property
def current(self):
return self["current"]
@property
@deprecate(
"0.1", "Use a look-up, i.e. `ec_meas['raw_potential']`, instead.", "0.3.1"
)
def raw_potential(self):
return self["raw_potential"]
@property
@deprecate("0.1", "Use a look-up, i.e. `ec_meas['raw_current']`, instead.", "0.3.1")
def raw_current(self):
return self["raw_current"]
@property
def U(self):
"""The potential [V] numpy array of the measurement"""
return self.potential.data.copy()
@property
def J(self):
"""The current ([mA] or [mA/cm^2]) numpy array of the measurement"""
return self.current.data.copy()
@property
@deprecate("0.1", "Use `U` instead.", "0.3.1")
def v(self):
"""The potential [V] numpy array of the measurement"""
return self.potential.data.copy()
@property
@deprecate("0.1", "Use `J` instead.", "0.3.1")
def j(self):
"""The current ([mA] or [mA/cm^2]) numpy array of the measurement"""
return self.current.data.copy()
[docs] def as_cv(self):
"""Convert self to a CyclicVoltammogram"""
from .cv import CyclicVoltammogram
cv_as_dict = self.as_dict()
cv_as_dict["technique"] = "CV"
# Note, this works perfectly! All needed information is in self_as_dict :)
return CyclicVoltammogram.from_dict(cv_as_dict)