def test_slit_unit_conversions_spectrum_in_cm(verbose=True, plot=True, close_plots=True, *args, **kwargs): """ Test that slit is consistently applied for different units Assert that: - calculated FWHM is the one that was applied """ from radis.test.utils import getTestFile from radis.tools.database import load_spec if plot: # dont get stuck with Matplotlib if executing through pytest plt.ion() if close_plots: plt.close("all") # %% Get a Spectrum (stored in cm-1) s_cm = load_spec(getTestFile("CO_Tgas1500K_mole_fraction0.01.spec"), binary=True) s_cm.rescale_mole_fraction(1) # just because it makes better units s_cm.update() wstep = s_cm.conditions["wstep"] assert s_cm.get_waveunit() == "cm-1" # ensures it's stored in cm-1 for shape in ["gaussian", "triangular"]: # Apply slit in cm-1 slit_cm = 2 s_cm.name = "Spec in cm-1, slit {0:.2f} cm-1".format(slit_cm) s_cm.apply_slit(slit_cm, unit="cm-1", shape=shape, mode="same") # ... mode=same to keep same output length. It helps compare both Spectra afterwards # in cm-1 as that's s.get_waveunit() fwhm = get_FWHM(*s_cm.get_slit()) assert np.isclose(slit_cm, fwhm, atol=2 * wstep) # Apply slit in nm this time s_nm = s_cm.copy() w_cm = s_nm.get_wavenumber(which="non_convoluted") slit_nm = dcm2dnm(slit_cm, w_cm[len(w_cm) // 2]) s_nm.name = "Spec in cm-1, slit {0:.2f} nm".format(slit_nm) s_nm.apply_slit(slit_nm, unit="nm", shape=shape, mode="same") plotargs = {} if plot: plotargs[ "title"] = "test_slit_unit_conversions: {0} ({1} cm-1)".format( shape, slit_cm) s_cm.compare_with(s_nm, spectra_only="radiance", rtol=1e-3, verbose=verbose, plot=plot, **plotargs)
def test_convert(verbose=True, *args, **kwargs): """ Test conversions """ E = np.linspace(1, 5, 5) # eV assert (J2eV(K2J(J2K(eV2J(E)))) == E).all() E = 2150 # cm-1 assert J2cm(cm2J(E)) == E assert K2cm(cm2K(E)) == E if verbose: print("K -> cm: ~ {0:.2f} K/cm".format(cm2K(1))) E = 1 # eV assert eV2cm(E) == J2cm(eV2J(1)) assert round(eV2K(E), 0) == 11605 assert K2eV(eV2K(E)) == E E = 250 # nm assert isclose(nm2eV(E), cm2eV(nm2cm(E))) assert eV2nm(nm2eV(E)) == E assert hz2nm(nm2hz(E)) == E fwhm = 1.5 # nm lbd_0 = 632.8 # nm assert isclose(fwhm, dcm2dnm(dnm2dcm(fwhm, lbd_0), nm2cm(lbd_0))) fwhm = 2e-3 # 0.002 nm lbd_0 = 632.8 fwhm_hz = dnm2dhz(fwhm, lbd_0) # ~ 1.5 GHz if verbose: print( ( "{0:.2g} nm broadening at {1} nm = {2:.2g} Ghz".format( fwhm, lbd_0, fwhm_hz * 1e-9 ) ) ) assert isclose(fwhm_hz * 1e-9, 1.4973307983125002) assert isclose(dhz2dnm(fwhm_hz, nm2hz(lbd_0)), fwhm) assert atm2bar(1) == 1.01325 assert isclose(torr2bar(atm2torr(bar2atm(1))), 1) assert isclose(torr2atm(bar2torr(atm2bar(1))), 1) # Hz assert isclose(1 / hz2cm(1e9), 30, atol=0.1) # 1 Ghz is about 30 cm assert hz2cm(cm2hz(600)) == 600 return True
def test_convert(verbose=True, *args, **kwargs): ''' Test conversions ''' E = np.linspace(1, 5, 5) # eV assert (J2eV(K2J(J2K(eV2J(E)))) == E).all() E = 2150 # cm-1 assert J2cm(cm2J(E)) E = 1 # eV assert (eV2cm(E) == J2cm(eV2J(1))) assert (round(eV2K(E), 0) == 11605) assert K2eV(eV2K(E)) == E E = 250 # nm assert isclose(nm2eV(E), cm2eV(nm2cm(E))) assert (eV2nm(nm2eV(E)) == E) assert hz2nm(nm2hz(E)) == E fwhm = 1.5 # nm lbd_0 = 632.8 # nm assert (isclose(fwhm, dcm2dnm(dnm2dcm(fwhm, lbd_0), nm2cm(lbd_0)))) fwhm = 2e-3 # 0.002 nm lbd_0 = 632.8 fwhm_hz = dnm2dhz(fwhm, lbd_0) # ~ 1.5 GHz if verbose: print(('{0:.2g} nm broadening at {1} nm = {2:.2g} Ghz'.format( fwhm, lbd_0, fwhm_hz * 1e-9))) assert isclose(fwhm_hz * 1e-9, 1.4973307983125002) assert isclose(dhz2dnm(fwhm_hz, nm2hz(lbd_0)), fwhm) assert atm2bar(1) == 1.01325 assert isclose(torr2bar(atm2torr(bar2atm(1))), 1) assert isclose(torr2atm(bar2torr(atm2bar(1))), 1) return True
def offset(s, offset, unit, name=None, inplace=False): # type: (Spectrum, float, str, str, bool) -> Spectrum """Offset the spectrum by a wavelength or wavenumber Parameters ---------- s: Spectrum Spectrum you want to modify offset: float Constant to add to all quantities in the Spectrum. unit: 'nm' or 'cm-1' unit for ``offset``. name: str name of output spectrum inplace: bool if ``True``, modifies ``s`` directly. Else, returns a copy. Default ``False`` Returns ------- s : Spectrum Spectrum object where cst is added to intensity of s['var'] If ``inplace=True``, ``s`` has been modified directly. See Also -------- call as a Spectrum method directly: :py:meth:`~radis.spectrum.spectrum.Spectrum.offset` """ has_var = len(s._q) > 0 has_conv_var = len(s._q_conv) > 0 stored_waveunit = s.get_waveunit() # Convert offset to correct unit: if stored_waveunit == "cm-1": if unit == "nm": # Note @EP: here we're offsetting by a constant value in 'nm', which is # not a constant value in 'cm-1'. The offset is an array if has_var: offset_q = -dnm_air2dcm( offset, s.get_wavelength( which="non_convoluted")) # this is an array if has_conv_var: offset_qconv = -dnm_air2dcm( offset, s.get_wavelength(which="convoluted")) # this is an array elif unit == "nm_vac": if has_var: offset_q = -dnm2dcm( offset, s.get_wavelength( which="non_convoluted")) # this is an array if has_conv_var: offset_qconv = -dnm2dcm( offset, s.get_wavelength(which="convoluted")) # this is an array elif unit == "cm-1": if has_var: offset_q = offset if has_conv_var: offset_qconv = offset else: raise ValueError(unit) elif stored_waveunit == "nm": # wavelength air if unit == "nm": if has_var: offset_q = offset if has_conv_var: offset_qconv = offset elif unit == "nm_vac": # Note @EP: strictly speaking, the offset should change a little bit # as nm_vac > nm depends on the wavelength. Neglected here. # TODO ? if has_var: offset_q = offset if has_conv_var: offset_qconv = offset elif unit == "cm-1": if has_var: offset_q = -dcm2dnm_air( offset, s.get_wavenumber( which="non_convoluted")) # this is an array if has_conv_var: offset_qconv = -dcm2dnm_air( offset, s.get_wavenumber(which="convoluted")) # this is an array else: raise ValueError(unit) elif stored_waveunit == "nm_vac": # wavelength vacuum if unit == "nm": # Note @EP: strictly speaking, the offset should change a little bit # as nm > nm_vac depends on the wavelength. Neglected here. # TODO ? if has_var: offset_q = offset if has_conv_var: offset_qconv = offset elif unit == "nm_vac": if has_var: offset_q = offset if has_conv_var: offset_qconv = offset elif unit == "cm-1": if has_var: offset_q = -dcm2dnm( offset, s.get_wavenumber( which="non_convoluted")) # this is an array if has_conv_var: offset_qconv = -dcm2dnm( offset, s.get_wavenumber(which="convoluted")) # this is an array else: raise ValueError(unit) else: raise ValueError(stored_waveunit) if not inplace: s = s.copy() # Update all variables if has_var: s._q["wavespace"] += offset_q # @dev: updates the Spectrum directly because of copy=False if has_conv_var: s._q_conv["wavespace"] += offset_qconv if name: s.name = name return s
def get_slit_function(slit_function, unit='nm', norm_by='area', shape='triangular', center_wavespace=None, return_unit='same', wstep=None, plot=False, resfactor=2, *args, **kwargs): ''' Import or generate slit function in correct wavespace Give a file path to import, or a float / tuple to generate arbitrary shapes Warning with units: read about unit and return_unit parameters. See :meth:`~radis.spectrum.spectrum.Spectrum.apply_slit` and :func:`~radis.tools.slit.convolve_with_slit` for more info Parameters ---------- slit_function: tuple, or str If float: generate slit function with FWHM of `slit_function` (in `unit`) If .txt file: import experimental slit function (in `unit`): format must be 2-columns with wavelengths and intensity (doesn't have to be normalized) unit: 'nm' or 'cm-1' unit of slit_function FWHM, or unit of imported file norm_by: 'area', 'max', or None how to normalize. `area` conserves energy. With `max` the slit is normalized so that its maximum is one (that is what is done in Specair: it changes the outptut spectrum unit, e.g. from 'mW/cm2/sr/µm' to 'mW/cm2/sr') None doesnt normalize. Default 'area' shape: 'triangular', 'trapezoidal', 'gaussian' which shape to use when generating a slit. Default 'triangular' center_wavespace: float, or None center of slit when generated (in unit). Not used if slit is imported. return_unit: 'nm', 'cm-1', or 'same' if not 'same', slit is converted to the given wavespace. wstep: float which discretization step to use (in return_unit) when generating a slit function. Not used if importing Other Parameters ---------------- resfactor: int resolution increase when resampling from nm to cm-1, or the other way round. Default 2. energy_threshold: float tolerance fraction. Only used when importing experimental slit as the theoretical slit functions are directly generated in spectrum wavespace Default 1e-3 (0.1%) Returns ------- wslit, Islit: array wslit is in `return_unit` . Islit is normalized according to `norm_by` Examples -------- >>> wslit, Islit = get_slit_function(1, 'nm', shape='triangular', center_wavespace=600, wstep=0.01) Returns a triangular slit function of FWHM = 1 nm, centered on 600 nm, with a step of 0.01 nm >>> wslit, Islit = get_slit_function(1, 'nm', shape='triangular', center_wavespace=600, return_unit='cm-1', wstep=0.01) Returns a triangular slit function expressed in cm-1, with a FWHM = 1 nm (converted in equivalent width in cm-1 at 600 nm), centered on 600 nm, with a step of 0.01 cm-1 (!) Notes ----- In norm_by 'max' mode, slit is normalized by slit max. In RADIS, this is done in the spectrum wavespace (to avoid errors that would be caused by interpolating the spectrum). A problem arise if the spectrum wavespace is different from the slit wavespace: typically, slit is in 'nm' but a spectrum calculated by RADIS is stored in 'cm-1': in that case, the convoluted spectrum is multiplied by /int(Islit*dν) instead of /int(Islit*dλ). The output unit is then [radiance]*[spec_unit] instead of [radiance]*[slit_unit], i.e, typically, [mW/cm2/sr/nm]*[cm-1] instead of [mW/cm2/sr/nm]*[nm]=[mW/cm2/sr] While this remains true if the units are taken into account, this is not the expected behaviour. For instance, Specair users are used to having a FWHM(nm) factor between spectra convolved with slit normalized by max and slit normalized by area. The norm_by='max' behaviour adds a correction factor `/int(Islit*dλ)/int(Islit*dν)` to maintain an output spectrum in [radiance]*[slit_unit] See Also -------- :meth:`~radis.spectrum.spectrum.Spectrum.apply_slit`, :func:`~radis.tools.slit.convolve_with_slit` ''' if 'waveunit' in kwargs: assert return_unit == 'same' # default return_unit = kwargs.pop('waveunit') warn(DeprecationWarning('waveunit renamed return_unit')) if 'slit_unit' in kwargs: assert unit == 'nm' # default unit = kwargs.pop('slit_unit') warn(DeprecationWarning('slit_unit renamed unit')) energy_threshold = kwargs.pop('energy_threshold', 1e-3) # type: float # tolerance fraction # when resampling (only used in experimental slit as the) # theoretical slit functions are directly generated in # spectrum wavespace def check_input_gen(): if center_wavespace is None: raise ValueError('center_wavespace has to be given when generating '+\ 'slit function') if wstep is None: raise ValueError('wstep has to be given when generating '+\ 'slit function') # Cast units if return_unit == 'same': return_unit = unit unit = cast_waveunit(unit) return_unit = cast_waveunit(return_unit) scale_slit = 1 # in norm_by=max mode, used to keep units in [Iunit]*return_unit in [Iunit]*unit # not used in norm_by=area mode # First get the slit in return_unit space if is_float(slit_function ): # Generate slit function (directly in return_unit space) check_input_gen() # ... first get FWHM in return_unit (it is in `unit` right now) FWHM = slit_function if return_unit == 'cm-1' and unit == 'nm': # center_wavespace ~ nm, FWHM ~ nm FWHM = dnm2dcm(FWHM, center_wavespace) # wavelength > wavenumber center_wavespace = nm2cm(center_wavespace) if norm_by == 'max': scale_slit = slit_function / FWHM # [unit/return_unit] elif return_unit == 'nm' and unit == 'cm-1': # center_wavespace ~ cm-1, FWHM ~ cm-1 FWHM = dcm2dnm(FWHM, center_wavespace) # wavenumber > wavelength center_wavespace = cm2nm(center_wavespace) if norm_by == 'max': scale_slit = slit_function / FWHM # [unit/return_unit] else: pass # correct unit already # Now FWHM is in 'return_unit' # ... now, build it (in our wavespace) if __debug__: printdbg( 'get_slit_function: {0} FWHM {1:.2f}{2}, center {3:.2f}{2}, norm_by {4}' .format(shape, FWHM, return_unit, center_wavespace, norm_by)) if shape == 'triangular': wslit, Islit = triangular_slit(FWHM, wstep, center=center_wavespace, bplot=plot, norm_by=norm_by, waveunit=return_unit, scale=scale_slit, *args, **kwargs) # Insert other slit shapes here # ... elif shape == 'gaussian': wslit, Islit = gaussian_slit(FWHM, wstep, center=center_wavespace, bplot=plot, norm_by=norm_by, waveunit=return_unit, scale=scale_slit, *args, **kwargs) elif shape == 'trapezoidal': raise TypeError( 'A (top, base) tuple must be given with a trapezoidal slit') else: raise TypeError( 'Slit function ({0}) not in known slit shapes: {1}'.format( shape, SLIT_SHAPES)) elif isinstance(slit_function, tuple): check_input_gen() try: top, base = slit_function except: raise TypeError( 'Wrong format for slit function: {0}'.format(slit_function)) if shape == 'trapezoidal': pass elif shape == 'triangular': # it's the default warn( 'Triangular slit given with a tuple: we used trapezoidal slit instead' ) shape = 'trapezoidal' else: raise TypeError( 'A (top, base) tuple must be used with a trapezoidal slit') # ... first get FWHM in our wavespace unit if return_unit == 'cm-1' and unit == 'nm': # center_wavespace ~ nm, FWHM ~ nm top = dnm2dcm(top, center_wavespace) # wavelength > wavenumber base = dnm2dcm(base, center_wavespace) # wavelength > wavenumber center_wavespace = nm2cm(center_wavespace) if norm_by == 'max': scale_slit = sum(slit_function) / (top + base ) # [unit/return_unit] elif return_unit == 'nm' and unit == 'cm-1': # center_wavespace ~ cm-1, FWHM ~ cm-1 top = dcm2dnm(top, center_wavespace) # wavenumber > wavelength base = dcm2dnm(base, center_wavespace) # wavenumber > wavelength center_wavespace = cm2nm(center_wavespace) if norm_by == 'max': scale_slit = sum(slit_function) / (top + base ) # [unit/return_unit] else: pass # correct unit already FWHM = (top + base) / 2 # ... now, build it (in our wavespace) if __debug__: printdbg( 'get_slit_function: {0}, FWHM {1:.2f}{2}, center {3:.2f}{2}, norm_by {4}' .format(shape, FWHM, return_unit, center_wavespace, norm_by)) wslit, Islit = trapezoidal_slit(top, base, wstep, center=center_wavespace, bplot=plot, norm_by=norm_by, waveunit=return_unit, scale=scale_slit, *args, **kwargs) elif isinstance(slit_function, string_types): # import it if __debug__: printdbg( 'get_slit_function: {0} in {1}, norm_by {2}, return in {3}'. format(slit_function, unit, norm_by, return_unit)) wslit, Islit = import_experimental_slit( slit_function, norm_by=norm_by, # norm is done later anyway waveunit=unit, bplot=False, # we will plot after resampling *args, **kwargs) # ... get unit # Normalize if norm_by == 'area': # normalize by the area # I_slit /= np.trapz(I_slit, x=w_slit) Iunit = '1/{0}'.format(unit) elif norm_by == 'max': # set maximum to 1 Iunit = '1' elif norm_by is None: Iunit = None else: raise ValueError( 'Unknown normalization type: `norm_by` = {0}'.format(norm_by)) # ... check it looks correct unq, counts = np.unique(wslit, return_counts=True) dup = counts > 1 if dup.sum() > 0: raise ValueError( 'Not all wavespace points are unique: slit function ' + 'format may be wrong. Duplicates for w={0}'.format(unq[dup])) # ... resample if needed if return_unit == 'cm-1' and unit == 'nm': # wavelength > wavenumber wold, Iold = wslit, Islit wslit, Islit = resample_even(nm2cm(wslit), Islit, resfactor=resfactor, energy_threshold=energy_threshold, print_conservation=True) scale_slit = trapz(Iold, wold) / trapz(Islit, wslit) # [unit/return_unit] renormalize = True elif return_unit == 'nm' and unit == 'cm-1': # wavenumber > wavelength wold, Iold = wslit, Islit wslit, Islit = resample_even(cm2nm(wslit), Islit, resfactor=resfactor, energy_threshold=energy_threshold, print_conservation=True) scale_slit = trapz(Iold, wold) / trapz(Islit, wslit) # [unit/return_unit] renormalize = True else: # return_unit == unit renormalize = False # Note: if wstep dont match with quantity it's alright as it gets # interpolated in the `convolve_with_slit` function # re-Normalize if needed (after changing units) if renormalize: if __debug__: printdbg('get_slit_function: renormalize') if norm_by == 'area': # normalize by the area Islit /= abs(np.trapz(Islit, x=wslit)) Iunit = '1/{0}'.format(return_unit) elif norm_by == 'max': # set maximum to 1 Islit /= abs(np.max(Islit)) Islit *= scale_slit Iunit = '1' if scale_slit != 1: Iunit += 'x{0}'.format(scale_slit) # elif norm_by == 'max2': # set maximum to 1 # removed this mode for simplification # Islit /= abs(np.max(Islit)) elif norm_by is None: Iunit = None else: raise ValueError( 'Unknown normalization type: `norm_by` = {0}'.format( norm_by)) if plot: # (plot after resampling / renormalizing) # Plot slit plot_slit(wslit, Islit, waveunit=return_unit, Iunit=Iunit) else: raise TypeError('Unexpected type for slit function: {0}'.format( type(slit_function))) return wslit, Islit
def offset(s, offset, unit, name=None, inplace=False): # type: (Spectrum, float, str, str, bool) -> Spectrum '''Offset the spectrum by a wavelength or wavenumber Parameters ---------- s: Spectrum Spectrum you want to modify offset: float Constant to add to all quantities in the Spectrum. unit: 'nm' or 'cm-1' unit for ``offset``. name: str name of output spectrum inplace: bool if ``True``, modifies ``s`` directly. Else, returns a copy. Default ``False`` Returns ------- s : Spectrum Spectrum object where cst is added to intensity of s['var'] If ``inplace=True``, ``s`` has been modified directly. See Also -------- call as a Spectrum method directly: :py:meth:`~radis.spectrum.spectrum.Spectrum.offset` ''' has_var = len(s._q) > 0 has_conv_var = len(s._q_conv) > 0 # Convert to correct unit: if unit == 'nm' and s.get_waveunit() == 'cm-1': # Note @EP: technically we should check the medium is air or vacuum... TODO # Note @EP: here we're offsetting by a constant value in 'cm-1', which is # not a constant value in 'nm'. We end of with offset as an array if has_var: offset_q = -dnm2dcm(offset, s.get_wavelength( which='non_convoluted')) # this is an array if has_conv_var: offset_qconv = -dnm2dcm(offset, s.get_wavelength( which='convoluted')) # this is an array elif unit == 'cm-1' and s.get_waveunit() == 'nm': if has_var: offset_q = -dcm2dnm(offset, s.get_wavenumber( which='non_convoluted')) # this is an array if has_conv_var: offset_qconv = -dcm2dnm(offset, s.get_wavenumber( which='convoluted')) # this is an array else: assert unit == s.get_waveunit() if has_var: offset_q = offset if has_conv_var: offset_qconv = offset if not inplace: s = s.copy() # Update all variables if has_var: s._q['wavespace'] += offset_q # @dev: updates the Spectrum directly because of copy=False if has_conv_var: s._q_conv['wavespace'] += offset_qconv if name: s.name = name return s