from .ec import ECMeasurement
from ..spectra import Spectrum
from ..data_series import Field, ValueSeries
import numpy as np
from scipy.interpolate import interp1d
from ..spectra import SpectrumSeries
from ..exporters.sec_exporter import SECExporter
[docs]class SpectroECMeasurement(ECMeasurement):
def __init__(self, *args, **kwargs):
"""Initialize an SEC measurement. All args and kwargs go to ECMeasurement."""
ECMeasurement.__init__(self, *args, **kwargs)
self._reference_spectrum = None
self.tracked_wavelengths = []
self.plot_waterfall = self.plotter.plot_waterfall
self.plot_wavelengths = self.plotter.plot_wavelengths
self.plot_wavelengths_vs_potential = self.plotter.plot_wavelengths_vs_potential
self.technique = "S-EC"
@property
def reference_spectrum(self):
"""The spectrum which will by default be used to calculate dOD"""
if not self._reference_spectrum or self._reference_spectrum == "reference":
self._reference_spectrum = Spectrum.from_field(self["reference"])
return self._reference_spectrum
[docs] def set_reference_spectrum(
self, spectrum=None, t_ref=None, V_ref=None,
):
"""Set the spectrum used as the reference when calculating dOD.
Args:
spectrum (Spectrum or str): If a Spectrum is given, it becomes the reference
spectrum. The string "reference" can be given to make the reference
spectrum become (via the reference_spectrum property) one that the
measurement was loaded with (evt. for definition of wavelengths).
t_ref (float): The time (with respect to self.tstamp) to use as the
reference spectrum
V_ref (float): The potential to use as the reference spectrum. This will
only work if the potential is monotonically increasing.
"""
if (not spectrum) and t_ref:
spectrum = self.get_spectrum(t=t_ref)
if (not spectrum) and V_ref:
spectrum = self.get_spectrum(V=V_ref)
if not spectrum:
raise ValueError("must provide a spectrum, t_ref, or V_ref!")
self._reference_spectrum = spectrum
@property
def spectra(self):
"""The Field that is the spectra of the SEC Measurement"""
return self["spectra"]
@property
def spectrum_series(self):
"""The SpectrumSeries that is the spectra of the SEC Measurement"""
return SpectrumSeries.from_field(
self.spectra, tstamp=self.tstamp, name=self.name + " spectra",
)
@property
def wavelength(self):
"""A DataSeries with the wavelengths for the SEC spectra"""
return self.spectra.axes_series[1]
@property
def wl(self):
"""A numpy array with the wavelengths in [nm] for the SEC spectra"""
return self.wavelength.data
@property
def plotter(self):
"""The default plotter for SpectroECMeasurement is SECPlotter"""
if not self._plotter:
from ..plotters.sec_plotter import SECPlotter
self._plotter = SECPlotter(measurement=self)
return self._plotter
@property
def exporter(self):
"""The default plotter for SpectroECMeasurement is SECExporter"""
if not self._exporter:
self._exporter = SECExporter(measurement=self)
return self._exporter
[docs] def calc_dOD(self, V_ref=None, t_ref=None, index_ref=None):
"""Calculate the optical density with respect to a reference
Provide at most one of V_ref, t_ref, or index. If none are provided the default
reference spectrum (self.reference_spectrum) will be used.
Args:
V_ref (float): The potential at which to get the reference spectrum
t_ref (float): The time at which to get the reference spectrum
index_ref (int): The index of the reference spectrum
Return Field: the delta optical density spanning time and wavelength
"""
counts = self.spectra.data
if V_ref or t_ref:
ref_spec = self.get_spectrum(V=V_ref, t=t_ref, index=index_ref)
else:
ref_spec = self.reference_spectrum
dOD = -np.log10(counts / ref_spec.y)
dOD_series = Field(
name="$\Delta$ O.D.",
unit_name="",
axes_series=self.spectra.axes_series,
data=dOD,
)
return dOD_series
[docs] def get_spectrum(self, V=None, t=None, index=None, name=None):
"""Return the Spectrum at a given potential V, time t, or index
Exactly one of V, t, and index should be given. If V (t) is out of the range of
self.v (self.t), then first or last spectrum will be returned.
Args:
V (float): The potential at which to get the spectrum. Measurement.v must
be monotonically increasing for this to work.
t (float): The time at which to get the spectrum
index (int): The index of the spectrum
name (str): Optional. name to give the new spectrum if interpolated
Return Spectrum: The spectrum. The data is (spectrum.x, spectrum.y)
"""
if V and V in self.v: # woohoo, can skip interpolation!
index = int(np.argmax(self.v == V))
elif t and t in self.t: # woohoo, can skip interpolation!
index = int(np.argmax(self.t == t))
if index: # then we're done:
return self.spectrum_series[index]
# otherwise, we have to interpolate:
counts = self.spectra.data
end_spectra = (self.spectrum_series[0].y, self.spectrum_series[-1].y)
if V:
counts_interpolater = interp1d(
self.v, counts, axis=0, fill_value=end_spectra, bounds_error=False
)
# FIXME: This requires that potential and spectra have same tseries!
y = counts_interpolater(V)
name = name or f"{self.spectra.name}_{V}V"
elif t:
t_spec = self.spectra.axes_series[0].t
counts_interpolater = interp1d(
t_spec, counts, axis=0, fill_value=end_spectra, bounds_error=False
)
y = counts_interpolater(t)
name = name or f"{self.spectra.name}_{t}s"
else:
raise ValueError(f"Need t or V or index to select a spectrum!")
field = Field(
data=y,
name=name,
unit_name=self.spectra.unit_name,
axes_series=[self.wavelength],
)
return Spectrum.from_field(field, tstamp=self.tstamp)
[docs] def get_dOD_spectrum(
self, V=None, t=None, index=None, V_ref=None, t_ref=None, index_ref=None,
):
"""Return the delta optical density Spectrum given a point and reference point.
Provide exactly one of V, t, and index, and at most one of V_ref, t_ref, and
index_ref. For V and V_ref to work, the potential in the measurement must be
monotonically increasing.
Args:
V (float): The potential at which to get the spectrum.
t (float): The time at which to get the spectrum
index (int): The index of the spectrum
V_ref (float): The potential at which to get the reference spectrum
t_ref (float): The time at which to get the reference spectrum
index_ref (int): The index of the reference spectrum
Return Spectrum: The dOD spectrum. The data is (spectrum.x, spectrum.y)
"""
if V_ref or t_ref or index_ref:
spectrum_ref = self.get_spectrum(V=V_ref, t=t_ref, index=index_ref)
else:
spectrum_ref = self.reference_spectrum
spectrum = self.get_spectrum(V=V, t=t, index=index)
field = Field(
data=-np.log10(spectrum.y / spectrum_ref.y),
name="$\Delta$ OD",
unit_name="",
axes_series=[self.wavelength],
)
return Spectrum.from_field(field)
[docs] def track_wavelength(self, wl, width=10, V_ref=None, t_ref=None, index_ref=None):
"""Return and cache a ValueSeries for the dOD for a specific wavelength.
The cacheing adds wl_str to the SECMeasurement's data series, where
wl_str = "w" + int(wl)
This is dOD. The raw is also added as wl_str + "_raw".
So, to get the raw counts for a specific wavelength, call this function and
then use __getitem__, as in: sec_meas[wl_str + "_raw"]
If V_ref, t_ref, or index_ref are provided, they specify what to reference dOD
to. Otherwise, dOD is referenced to the SECMeasurement's reference_spectrum.
Args:
wl (float): The wavelength to track in [nm]
width (float): The width around wl to average. For example, if wl=400 and
width = 20, the spectra will be averaged between 390 and 410 nm to get
the values. Defaults to 10. To interpolate at the exact wavelength
rather than averaging, specify `width=0`.
V_ref (float): The potential at which to get the reference spectrum
t_ref (float): The time at which to get the reference spectrum
index_ref (int): The index of the reference spectrum
Returns ValueSeries: The dOD value of the spectrum at wl.
"""
if V_ref or t_ref or index_ref:
spectrum_ref = self.get_spectrum(V=V_ref, t=t_ref, index=index_ref)
else:
spectrum_ref = self.reference_spectrum
x = self.wl
if width: # averaging
wl_mask = np.logical_and(wl - width / 2 < x, x < wl + width / 2)
counts_ref = np.mean(spectrum_ref.y[wl_mask])
counts_wl = np.mean(self.spectra.data[:, wl_mask], axis=1)
else: # interpolation
counts_ref = np.interp(wl, spectrum_ref.x, spectrum_ref.y)
counts_wl = []
for counts_i in self.spectra.data:
c = np.interp(wl, x, counts_i)
counts_wl.append(c)
counts_wl = np.array(counts_wl)
dOD_wl = -np.log10(counts_wl / counts_ref)
raw_name = f"w{int(wl)} raw"
dOD_name = f"w{int(wl)}"
tseries = self.spectra.axes_series[0]
raw_vseries = ValueSeries(
name=raw_name, unit_name="counts", data=counts_wl, tseries=tseries
)
dOD_vseries = ValueSeries(
name=dOD_name, unit_name="", data=dOD_wl, tseries=tseries
)
self[raw_name] = raw_vseries
# FIXME: better caching. See https://github.com/ixdat/ixdat/pull/11
self[dOD_name] = dOD_vseries
# FIXME: better caching. See https://github.com/ixdat/ixdat/pull/11
self.tracked_wavelengths.append(dOD_name) # For the exporter.
return dOD_vseries