"""
"Frequency" [,wavelength, and wavenumber] class and function.
"""
import numpy as _np
import copy as _copy
from crikit.utils.general import find_nearest as _find_nearest
__all__ = ['Frequency', 'calib_pix_wn', 'calib_pix_wl']
[docs]class Frequency:
"""
Frequency [,wavelength, and wavenumber] class
Attributes
----------
data : 1D ndarray, optional (see note)
Frequency vector
calib : object, optional (see note)
Calibration object that is passed to calib_fcn
calib_orig : object, optional (see note)
Calibration object ('original'). Set during initial setting of calib. \
Useful for backing-up calibration)
calib_fcn : fcn, optional (see note)
Function that accepts a calibration object and returns data and \
units
units : str, optional
Units of data (the default is 'Frequency'). Over-written by return \
from calib_fcn
op_list_pix : list or tuple or 1D ndarray
Range of pixels to perform operations over. Must be even lengthed.
op_list_freq : list or tuple or 1D ndarray
Range of frequencies (or wavelength, or wavenumber) to perform \
operations over. Must be even lengthed.
plot_list_pix : list or tuple or 1D ndarray
Range of pixels to plot. Must be even lengthed.
plot_list_freq : list or tuple or 1D ndarray
Range of frequencies (or wavelength, or wavenumber) to plot. \
Must be even lengthed.
size : int, read-only
Length of data
pix_vec : 1D ndarray, read-only
Pixel vector (0-indexed)
op_range_freq : 1D ndarray, read-only
Range of operarational frequencies
plot_range_freq : 1D ndarray, read-only
Range of printing frequencies (not implemented)
Methods
-------
update
Updates data based on contents of calib (or calib_orig) and calib_fcn
get_index_of_closest_freq
Find the closest frequency in freq to a given freq value and RETURN
the pixel value.
get_closest_freq
Find the closest frequency in freq to a given freq value.
Notes
-----
Currently, this implementation does not check whether the
attributes/parameters are contradictory:
- calib
- calib_orig
- data
- The purpose of _list_ is to limit the range over which operation \
or plotting is performed. In some instances, for example, one may collect \
a larger set of frequencies that of interest or there may be blank (i.e., \
no signal) regions. Limiting these regions enables faster computation, may \
minimize "edge effects", and may allow for zoomed-in plotting when there \
are items of interest or a large dynamic range across a spectrum.
- For functions, methods, etc. that take into account _list_ \
parameters, they should default to op_list_* if plot_list_* are set to \
None.
"""
def __init__(self, data=None, calib=None, calib_orig=None,
calib_fcn=None, units=None):
self._data = None
self._calib = None
self._calib_orig = None
self._calib_fcn = None
self._label = None
self._units = None
self._op_list_pix = None
self._op_list_freq = None
self._plot_list_pix = None
self._plot_list_freq = None
if data is not None:
self.data = data
if calib is not None:
self.calib = calib
if calib_orig is not None:
self.calib_orig = calib_orig
if calib_fcn is not None:
self.calib_fcn = calib_fcn
if units is not None:
self.units = units
if (self._data is None) and (self._calib is not None) and \
(self._calib_fcn is not None):
self.update()
@property
def data(self):
return self._data
@data.setter
def data(self, value):
if isinstance(value, _np.ndarray):
if value.ndim == 1:
self._data = value
else:
raise TypeError('data must be a 1D ndarray')
elif value is None:
self._data = None
else:
raise TypeError('data must be a 1D ndarray')
@property
def calib(self):
return self._calib
@calib.setter
def calib(self, value):
if isinstance(value, dict):
self._calib = value
if self._calib_orig is None:
self._calib_orig = value
elif value is None:
self._calib = None
self.update()
else:
raise TypeError('calib must be of type dict')
@calib.deleter
def calib(self):
self._calib = None
self.update()
@property
def calib_orig(self):
return self._calib_orig
@calib_orig.setter
def calib_orig(self, value):
if isinstance(value, dict):
self._calib_orig = value
else:
raise TypeError('calib_orig must be of type dict')
@property
def calib_fcn(self):
return self._calib_fcn
@calib_fcn.setter
def calib_fcn(self, value):
if callable(value):
self._calib_fcn = value
else:
raise TypeError('calib_fcn must be a callable function')
@property
def units(self):
return self._units
@units.setter
def units(self, value):
if isinstance(value, str):
self._units = value
else:
raise TypeError('units must be of type str')
@property
def size(self):
return self.data.size
@property
def pix_vec(self):
return _np.arange(self.data.size)
[docs] def update(self):
"""
Update data with calib and calib_fcn.
"""
if self._calib_fcn is None:
raise TypeError('Calibration function not set')
if self._calib is None:
if self._calib_orig is None:
raise TypeError('Calibration object not set')
else:
self.calib = self._calib_orig
self.data, self.units = self.calib_fcn(self.calib) # pylint: disable=not-callable
@property
def op_list_pix(self):
return self._op_list_pix
@op_list_pix.setter
def op_list_pix(self,value):
if isinstance(value, list) or isinstance(value, tuple) or isinstance(value, _np.ndarray):
if len(value) == 2:
value = list(value)
value.sort()
self._op_list_pix = value
# if self.data is not None: # ! Allow setting without freq or not?
self._op_list_freq = self.get_closest_freq(value)
self._op_list_freq.sort()
elif len(value) != 2 and _np.mod(len(value),2) == 0 and len(value) != 0:
raise NotImplementedError('op_list_pix can only currently handle 2 entries')
else:
raise TypeError('op_list_pix should be a list, tuple, or ndarray with an even number of entries')
else:
raise TypeError('op_list_pix should be a list, tuple, or ndarray')
@op_list_pix.deleter
def op_list_pix(self):
self._op_list_pix = None
self._op_list_freq = None
@property
def op_list_freq(self):
return self._op_list_freq
@op_list_freq.setter
def op_list_freq(self,value):
if isinstance(value, list) or isinstance(value, tuple) or isinstance(value, _np.ndarray):
if len(value) == 2:
value = list(value)
value.sort()
self._op_list_pix = self.get_index_of_closest_freq(value)
self._op_list_pix.sort()
self._op_list_freq = self.get_closest_freq(value)
self._op_list_freq.sort()
elif len(value) != 2 and _np.mod(len(value),2) == 0 and len(value) != 0:
raise NotImplementedError('op_list_freq can only currently handle 2 entries')
elif len(value) == 0:
self._op_list_freq = None
self._op_list_pix = None
else:
raise TypeError('op_list_freq should be a list, tuple, or ndarray with an even number of entries')
else:
raise TypeError('op_list_freq should be a list, tuple, or ndarray')
@op_list_freq.deleter
def op_list_freq(self):
self._op_list_pix = None
self._op_list_freq = None
@property
def op_range_pix(self):
if self._op_list_pix is not None:
return _np.arange(self._op_list_pix[0],self._op_list_pix[1]+1)
else:
return self.pix_vec
@property
def op_range_freq(self):
if self._op_list_pix is not None:
return self.data[self.op_range_pix]
else:
return self.data
@property
def op_size(self):
if self._op_list_pix is None:
return self.data.size
else:
return self._op_list_pix[-1] - self._op_list_pix[0] + 1
@property
def plot_list_pix(self):
raise NotImplementedError('plot_list_pix is not yet implemented')
@property
def plot_list_freq(self):
raise NotImplementedError('plot_list_freq is not yet implemented')
[docs] def get_index_of_closest_freq(self, in_freqs):
"""
Return index(-es) of frequency(-ies) in freq closest to in_freqs
"""
return _find_nearest(self.data, in_freqs)[1]
[docs] def get_closest_freq(self, in_freqs):
"""
Return frequency(-ies) in freq closest to in_freqs
"""
return _find_nearest(self.data, in_freqs)[0]
[docs]def calib_pix_wl(calib_obj):
"""
Return a wavelength (wl) vector based on calibration (calib) object
Parameters
----------
calib_obj : dict or list or 1D ndarray
Calibration object (see below).
calib_obj : dict {'n_pix', 'ctr_wl', 'ctr_wl0', 'units', 'a_vec'}
Calibration dict with 5 key-value pairs (see Notes)
calib_obj : list or 1D ndarray
Calibration array ['n_pix', 'ctr_wl', 'ctr_wl0', 'units',
'a_0', 'a_1', ..., 'a_n']
Returns
-------
wl_vec : 1D ndarray
Wavelength vector
units : str
Units string 'Wavelength (' + calib_obj['units'] + ')'
Notes
-----
calib_obj dict key-value pairs:
* n_pix : int, number of pixels (0-index)
* ctr_wl : float, center wavelength
* ctr_wl0 : float, center calibration wavelength
* units : str, wavelength units, optional (default is 'nm')
* a_vec : list or 1D ndarray, polynomial coefficients, [a_n, a_n-1,..., \
a_1, a_0]. a_2...a_n, optional.
calibration model:
.. math::
wl\_{vec} = a_n*(n\_{pix})^n + a_{n-1}*(n\_{pix})^{n-1} + ~...~ + \
n\_{pix}*a_1 + a_0 + ctr\_{wl} - ctr\_{wl0}
"""
calib = {}
if len(calib_obj) < 4:
raise TypeError('Calibration object does not contain enough entries')
if isinstance(calib_obj, dict):
key_list = ['n_pix','ctr_wl', 'ctr_wl0', 'a_vec']
for k in key_list:
if k not in calib_obj:
raise KeyError('Calibration dict missing: {}'.format(k))
else:
calib[k] = calib_obj[k]
if 'units' in calib_obj:
calib['units'] = calib_obj['units']
else:
calib['units'] = 'nm'
pix = _np.arange(calib['n_pix'])
wl_vec = _np.polyval(calib['a_vec'], pix)
wl_vec += calib['ctr_wl'] - calib['ctr_wl0']
return (wl_vec, calib['units'])
[docs]def calib_pix_wn(calib_obj):
"""
Return a wavenumber (wn) vector based on calibration (calib) object
Parameters
----------
calib_obj : dict or list or 1D ndarray
Calibration object (see below).
calib_obj : dict {'n_pix', 'ctr_wl', 'ctr_wl0', 'probe', 'units', 'a_vec'}
Calibration dict with 6 key-value pairs (see Notes)
calib_obj : list or 1D ndarray
Calibration array ['n_pix', 'ctr_wl', 'ctr_wl0', 'probe', 'units',
'a_0', 'a_1', ..., 'a_n']
Returns
-------
wn_vec : 1D ndarray
Wavenumber vector
units : str
Wavenumber units. Always 'cm$^-1$'
Notes
-----
calib_obj dict key-value pairs:
* n_pix : int, number of pixels (0-index)
* ctr_wl : float, center wavelength
* ctr_wl0 : float, center calibration wavelength
* probe : float, center wavelength of probe source (in units)
* units : {'nm', 'um'}, wavelength units, optional (default is 'nm')
* a_vec : list or 1D ndarray, polynomial coefficients, [a_n, a_n-1,..., \
a_1, a_0]. a_2...a_n, optional.
calibration model :
Wavelength vector:
.. math::
wl\_{vec} = a_n*(n\_{pix})^n + a_{n-1}*(n\_{pix})^{n-1} + ~...~ + \
n\_{pix}*a_1 + a_0 + ctr\_{wl} - ctr\_{wl0}
Wavenumber vector:
.. math::
wl\_{vec} = a_n*(n\_{pix})^n + a_{n-1}*(n\_{pix})^{n-1} + ~...~ + \
n\_{pix}*a_1 + a_0 + ctr\_{wl} - ctr\_{wl0}
"""
calib = _copy.deepcopy(calib_obj)
if 'probe' not in calib:
raise KeyError('\'probe\' key not in calib_obj')
if 'units' not in calib:
calib['units'] = 'nm'
elif isinstance(calib['units'], bytes):
calib['units'] = calib['units'].decode()
if calib['units'] == 'nm':
factor = 1e7
elif calib['units'] == 'um':
factor = 1e4
else:
errstr1 = 'Only nanometer (\'nm\') and micrometer (\'um\') units accepted. '
errstr2 = '{} provided of type {}.'.format(calib['units'], type(calib['units']))
raise ValueError(errstr1 + errstr2)
wl_vec, _ = calib_pix_wl(calib_obj)
wn_vec = factor/wl_vec - factor/calib['probe']
return (wn_vec, 'cm$^{-1}$')
if __name__ == '__main__': # pragma: no cover
import matplotlib as _mpl
_mpl.use('Qt5Agg')
_mpl.rcParams['font.family'] = 'sans-serif'
_mpl.rcParams['font.size'] = 12
import matplotlib.pyplot as _plt
_calib_dict = {}
_calib_dict['n_pix'] = 1600
_calib_dict['ctr_wl'] = 730.0
_calib_dict['ctr_wl0'] = 730.0
_calib_dict['probe'] = 771.461
_calib_dict['units'] = 'nm'
_calib_dict['a_vec'] = (-0.167740721307557, 863.8736708961577) # slope, intercept
_wl_vec, _units_wl = calib_pix_wl(_calib_dict)
_wn_vec, _units_wn = calib_pix_wn(_calib_dict)
_plt.plot(_wl_vec, _wn_vec)
_plt.xlabel('Wavelength ({})'.format(_units_wl))
_plt.ylabel('Wavenumber ({})'.format(_units_wn))
_plt.title('Wavenumber vs Wavelength')
_plt.show()