def get_baseline(s, var="radiance", Iunit=None): """Calculate and returns a baseline Parameters ---------- s: Spectrum Spectrum which needs a baseline var: str on which spectral quantity to read the baseline. Default ``'radiance'``. See :py:data:`~radis.spectrum.utils.SPECTRAL_QUANTITIES` Returns ------- baseline: Spectrum Spectrum object where intenisity is the baseline of s is computed by peakutils See Also -------- :py:func:`~radis.spectrum.operations.sub_baseline` """ import peakutils w1, I1 = s.get(var=var, Iunit=Iunit) baseline = peakutils.baseline(I1, deg=1, max_it=500) baselineSpectrum = Spectrum.from_array(w1, baseline, var, unit=Iunit, name=s.get_name() + "_baseline") return baselineSpectrum
def test_all_slit_shapes(FWHM=0.4, verbose=True, plot=True, close_plots=True, *args, **kwargs): """ Test all slit generation functions and make sure we get the expected FWHM""" if plot: plt.ion() # dont get stuck with Matplotlib if executing through pytest if close_plots: plt.close("all") # get spectrum from radis.test.utils import getTestFile from radis.spectrum.spectrum import Spectrum s = Spectrum.from_txt( getTestFile("calc_N2C_spectrum_Trot1200_Tvib3000.txt"), quantity="radiance_noslit", waveunit="nm", unit="mW/cm2/sr/µm", ) wstep = np.diff(s.get_wavelength())[0] # Plot all slits # ... gaussian s.apply_slit(FWHM, unit="nm", shape="gaussian", plot_slit=plot) assert np.isclose(get_FWHM(*s.get_slit()), FWHM, atol=2 * wstep) # ... triangular s.apply_slit(FWHM, unit="nm", shape="triangular", plot_slit=plot) assert np.isclose(get_FWHM(*s.get_slit()), FWHM, atol=2 * wstep) # ... trapezoidal s.apply_slit((FWHM * 0.9, FWHM * 1.1), unit="nm", shape="trapezoidal", plot_slit=plot) assert np.isclose(get_FWHM(*s.get_slit()), FWHM, atol=2 * wstep) # # ... trapezoidal # s.apply_slit(FWHM, unit='nm', shape='trapezoidal', plot_slit=plot, norm='max') # assert np.isclose(get_FWHM(*s.get_slit()), FWHM, atol=1.1*wstep) # ... experimental s.apply_slit(getTestFile("slitfunction.txt"), unit="nm", plot_slit=plot) assert np.isclose(get_effective_FWHM(*s.get_slit()), FWHM, atol=0.01) # note that we're applying a slit function measured at 632.5 nm to a Spectrum # at 4.7 µm. It's just good for testing the functions # # ... experimental, convolve with max # s.apply_slit(getTestFile('slitfunction.txt'), unit='nm', norm_by='max', plot_slit=plot) # assert np.isclose(get_FWHM(*s.get_slit()), FWHM, atol=1.1*wstep) if verbose: print("\n>>> _test_all_slits yield correct FWHM (+- wstep) : OK\n") return True # nothing defined yet
def experimental_spectrum(w, I, wunit='nm', Iunit='counts', conditions=None, cond_units=None, name=None): # -> Spectrum: ''' Convert (w, I) into a Spectrum object that has unit conversion and plotting capabilities. Convolution is not available as the spectrum is assumed to be measured experimentally (hence deconvolution of the slit function would be required) Parameters ---------- w, I: np.array wavelength and intensity wunit: 'nm', 'cm-1' wavespace unit Iunit: str intensity unit (can be 'counts', 'mW/cm2/sr/nm', etc...). Default 'counts' (default Winspec output) Other Parameters ---------------- conditions: dict (optional) calculation conditions to be stored with Spectrum cond_units: dict (optional) calculation conditions units name: str (optional) give a name See Also -------- :func:`~radis.spectrum.spectrum.calculated_spectrum`, :func:`~radis.spectrum.spectrum.transmittance_spectrum`, :meth:`~radis.spectrum.spectrum.Spectrum.from_array`, :meth:`~radis.spectrum.spectrum.Spectrum.from_txt`, :func:`~radis.tools.database.load_spec` ''' return Spectrum.from_array(np.array(w), np.array(I), 'radiance', waveunit=wunit, unit=Iunit, conditions=conditions, cond_units=cond_units, name=name)
def test_slit_unit_conversions_spectrum_in_nm(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.spectrum.spectrum import Spectrum if plot: # dont get stuck with Matplotlib if executing through pytest plt.ion() if close_plots: plt.close('all') # %% Get a Spectrum (stored in nm) s_nm = Spectrum.from_txt(getTestFile('calc_N2C_spectrum_Trot1200_Tvib3000.txt'), quantity='radiance_noslit', waveunit='nm', unit='mW/cm2/sr/µm', conditions={'self_absorption': False}) with catch_warnings(): filterwarnings( 'ignore', 'Condition missing to know if spectrum is at equilibrium:') # just because it makes better units s_nm.rescale_path_length(1, 0.001) wstep = np.diff(s_nm.get_wavelength())[0] assert s_nm.get_waveunit() == 'nm' # ensures it's stored in cm-1 for shape in ['gaussian', 'triangular']: # Apply slit in nm slit_nm = 0.5 s_nm.name = 'Spec in nm, slit {0:.2f} nm'.format(slit_nm) s_nm.apply_slit(slit_nm, unit='nm', 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_nm.get_slit()) assert np.isclose(slit_nm, fwhm, atol=2*wstep) # Apply slit in nm this time s_cm = s_nm.copy() w_nm = s_nm.get_wavelength(which='non_convoluted') slit_cm = dnm2dcm(slit_nm, w_nm[len(w_nm)//2]) s_cm.name = 'Spec in nm, slit {0:.2f} cm-1'.format(slit_cm) s_cm.apply_slit(slit_cm, unit='cm-1', shape=shape, mode='same') plotargs = {} if plot: plotargs['title'] = 'test_slit_unit_conversions: {0} ({1} nm)'.format( shape, slit_nm) s_nm.compare_with(s_cm, spectra_only='radiance', rtol=1e-3, verbose=verbose, plot=plot, **plotargs)
def transmittance_spectrum(w, T, wunit='nm', Tunit='I/I0', conditions=None, cond_units=None, name=None): # -> Spectrum: ''' Convert (w, I) into a Spectrum object that has unit conversion, plotting and slit convolution capabilities Parameters ---------- w, I: np.array wavelength and transmittance (no slit) wunit: 'nm', 'cm-1' wavespace unit Iunit: str intensity unit. Default 'I/I0' Other Parameters ---------------- conditions: dict (optional) calculation conditions to be stored with Spectrum cond_units: dict (optional) calculation conditions units name: str (optional) give a name See Also -------- :func:`~radis.spectrum.models.calculated_spectrum`, :func:`~radis.spectrum.models.experimental_spectrum`, :meth:`~radis.spectrum.spectrum.Spectrum.from_array`, :meth:`~radis.spectrum.spectrum.Spectrum.from_txt`, :func:`~radis.tools.database.load_spec` ''' return Spectrum.from_array(np.array(w), np.array(T), 'transmittance_noslit', waveunit=wunit, unit=Tunit, conditions=conditions, cond_units=cond_units, name=name)
def MergeSlabs(*slabs, **kwargs): # type: (*Spectrum, **dict) -> Spectrum r"""Combines several slabs into one. Useful to calculate multi-gas slabs. Linear absorption coefficient is calculated as the sum of all linear absorption coefficients, and the RTE is recalculated to get the total radiance. You can also simply use:: s1//s2 Merged spectrum ``1+2`` is calculated with Eqn (4.3) of the [RADIS-2018]_ article, generalized to N slabs : .. math:: j_{\lambda, 1+2} = j_{\lambda, 1} + j_{\lambda, 2} k_{\lambda, 1+2} = k_{\lambda, 1} + k_{\lambda, 2} where .. math:: j_{\lambda}, k_{\lambda} are the emission coefficient and absorption coefficient of the two slabs ``1`` and ``2``. Emission and absorption coefficients are calculated if not given in the initial slabs (if possible). Parameters ---------- slabs: list of Spectra, each representing a slab ``path_length`` must be given in Spectrum conditions, and equal for all spectra. line-of-sight:: slabs [0] \==== light [1] -> )=== observer [n] /==== Other Parameters ---------------- kwargs input: resample: ``'never'``, ``'intersect'``, ``'full'`` what to do when spectra have different wavespaces: - If ``'never'``, raises an error - If ``'intersect'``, uses the intersection of all ranges, and resample spectra on the most resolved wavespace. - If ``'full'``, uses the overlap of all ranges, resample spectra on the most resolved wavespace, and fill missing data with 0 emission and 0 absorption Default ``'never'`` out: ``'transparent'``, ``'nan'``, ``'error'`` what to do if resampling is out of bounds: - ``'transparent'``: fills with transparent medium. - ``'nan'``: fills with nan. - ``'error'``: raises an error. Default ``'nan'`` optically_thin: boolean if ``True``, merge slabs in optically thin mode. Default ``False`` verbose: boolean if ``True``, print messages and warnings. Default ``False`` modify_inputs: False if ``True``, slabs are modified directly when they are resampled. This avoids making a copy so is slightly faster. Default ``False``. Returns ------- Spectrum object representing total emission and total transmittance as observed at the output. Conditions and units are transported too, unless there is a mismatch then conditions are dropped (and units mismatch raises an error because it doesnt make sense) Examples -------- Merge two spectra calculated with different species (physically correct only if broadening coefficients dont change much):: from radis import calc_spectrum, MergeSlabs s1 = calc_spectrum(...) s2 = calc_spectrum(...) s3 = MergeSlabs(s1, s2) The last line is equivalent to:: s3 = s1//s2 Load a spectrum precalculated on several partial spectral ranges, for a same molecule (i.e, partial spectra are optically thin on the rest of the spectral range):: from radis import load_spec, MergeSlabs spectra = [] for f in ['spec1.spec', 'spec2.spec', ...]: spectra.append(load_spec(f)) s = MergeSlabs(*spectra, resample='full', out='transparent') s.update() # Generate missing spectral quantities s.plot() See Also -------- :func:`~radis.los.slabs.SerialSlabs` See more examples in :ref:`Line-of-Sight module <label_los_index>` """ # Deprecation warnings if "resample_wavespace" in kwargs: warn( DeprecationWarning( "'resample_wavespace' replaced with 'resample'")) kwargs["resample"] = kwargs.pop("resample_wavespace") if "out_of_bounds" in kwargs: warn(DeprecationWarning("'out_of_bounds' replaced with 'out'")) kwargs["out"] = kwargs.pop("out_of_bounds") # Check inputs, get defaults # inputs (Python 2 compatible) resample_wavespace = kwargs.pop("resample", "never") # default 'never' out_of_bounds = kwargs.pop("out", "nan") # default 'nan' optically_thin = kwargs.pop("optically_thin", False) # default False verbose = kwargs.pop("verbose", False) # type: bool kwargs.pop("debug", False) # type: bool modify_inputs = kwargs.pop("modify_inputs", False) # type: bool if len(kwargs) > 0: raise ValueError("Unexpected input: {0}".format(list(kwargs.keys()))) # Check inputs if resample_wavespace not in ["never", "intersect", "full"]: raise ValueError("'resample' should be one of: {0}".format(", ".join( ["never", "intersect", "full"]))) if len(slabs) == 0: raise ValueError("Empty list of slabs") elif len(slabs) == 1: if not is_spectrum(slabs[0]): raise TypeError( "MergeSlabs takes an unfolded list of Spectrum as " + "argument: (got {0})".format(type(slabs[0]))) return slabs[0] else: # calculate serial slabs slabs = list(slabs) # # Check all items are valid Spectrum objects for s in slabs: _check_valid(s) # Check all path_lengths are defined and they exist try: path_lengths = [s.conditions["path_length"] for s in slabs] except KeyError: raise ValueError( "path_length must be defined for all slabs in MergeSlabs. " + "Set it with `s.conditions['path_length']=`. ") if not all([L == path_lengths[0] for L in path_lengths[1:]]): raise ValueError( "path_length must be equal for all MergeSlabs inputs" + " (got {0})".format(path_lengths)) # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1') waveunit = slabs[0].get_waveunit() # Make all our slabs copies with the same wavespace range # (note: wavespace range may be different for different quantities, but # equal for all slabs) slabs = resample_slabs(waveunit, resample_wavespace, out_of_bounds, modify_inputs, *slabs) w_noconv = slabs[0]._get_wavespace() # %% # Get conditions of the Merged spectrum conditions = slabs[0].conditions conditions["waveunit"] = waveunit cond_units = slabs[0].cond_units units0 = slabs[0].units # Define conditions as intersection of everything (N/A if unknown) # ... this will only keep intensive parameters (same for all) for s in slabs[1:]: conditions = intersect(conditions, s.conditions) cond_units = intersect(cond_units, s.cond_units) # units = intersect(units0, s.units) # we're actually using [slabs0].units insteads # ... Add extensive parameters for cond in ["molecule"]: if in_all(cond, [s.conditions for s in slabs]): conditions[cond] = set([s.conditions[cond] for s in slabs]) # %% Get quantities that should be calculated # Try to keep all the quantities of the initial slabs: requested = merge_lists([s.get_vars() for s in slabs]) recompute = requested[:] # copy if "radiance_noslit" in requested and not optically_thin: recompute.append("emisscoeff") recompute.append("abscoeff") if "abscoeff" in recompute and "path_length" in conditions: recompute.append("absorbance") recompute.append("transmittance_noslit") # To make it easier, we start from abscoeff and emisscoeff of all slabs # Let's recompute them all # TODO: if that changes the initial Spectra, maybe we should just work on copies for s in slabs: if "abscoeff" in recompute and not "abscoeff" in list(s._q.keys()): s.update("abscoeff", verbose=False) # that may crash if Spectrum doesnt have the correct inputs. # let update() handle that if "emisscoeff" in recompute and not "emisscoeff" in list( s._q.keys()): s.update("emisscoeff", verbose=False) # same # %% Calculate total emisscoeff and abscoeff added = {} # ... absorption coefficient (cm-1) if "abscoeff" in recompute: # TODO: deal with all cases if __debug__: printdbg("... merge: calculating abscoeff k=sum(k_i)") abscoeff_eq = np.sum( [ s.get("abscoeff", wunit=waveunit, Iunit=units0["abscoeff"])[1] for s in slabs ], axis=0, ) assert len(w_noconv) == len(abscoeff_eq) added["abscoeff"] = (w_noconv, abscoeff_eq) # ... emission coefficient if "emisscoeff" in recompute: if __debug__: printdbg("... merge: calculating emisscoeff j=sum(j_i)") emisscoeff_eq = np.sum( [ s.get("emisscoeff", wunit=waveunit, Iunit=units0["emisscoeff"])[1] for s in slabs ], axis=0, ) assert len(w_noconv) == len(emisscoeff_eq) added["emisscoeff"] = (w_noconv, emisscoeff_eq) # name name = "//".join([s.get_name() for s in slabs]) # TODO: check units are consistent in all slabs inputs s = Spectrum( quantities=added, conditions=conditions, cond_units=cond_units, units=units0, name=name, ) # %% Calculate all quantities from emisscoeff and abscoeff if "emissivity_noslit" in requested and ( "thermal_equilibrium" not in s.conditions or s.is_at_equilibrium() != True): requested.remove("emissivity_noslit") if __debug__: printdbg( "... merge: all slabs are not proven to be at equilibrium. " + "Emissivity was not calculated") # Add the rest of the spectral quantities afterwards: s.update( [k for k in requested if k not in ["emisscoeff", "abscoeff"]], optically_thin=optically_thin, verbose=verbose, ) return s
def SerialSlabs(*slabs, **kwargs): # type: (*Spectrum, **dict) -> Spectrum r"""Adds several slabs along the line-of-sight. If adding two slabs only, you can also use:: s1>s2 Serial spectrum ``1>2`` is calculated with Eqn (4.2) of the [RADIS-2018]_ article, generalized to N slabs : .. math:: I_{\lambda, 1>2} = I_{\lambda, 1} \tau_{\lambda, 2} + I_{\lambda, 2} \tau_{\lambda, 1+2} = \tau_{\lambda, 1} \cdot \tau_{\lambda, 2} where .. math:: I_{\lambda}, \tau_{\lambda} are the radiance and transmittance of the two slabs ``1`` and ``2``. Radiance and transmittance are calculated if not given in the initial slabs (if possible). Parameters ---------- slabs: list of Spectra, each representing a slab line-of-sight:: slabs [0] [1] ............... [n] : : : \==== light * -> * -> * -> )=== observer /==== resample_wavespace: ``'never'``, ``'intersect'``, ``'full'`` what to do when spectra have different wavespaces: - If ``'never'``, raises an error - If ``'intersect'``, uses the intersection of all ranges, and resample spectra on the most resolved wavespace. - If ``'full``', uses the overlap of all ranges, resample spectra on the most resolved wavespace, and fill missing data with 0 emission and 0 absorption Default ``'never'`` out: ``'transparent'``, ``'nan'``, ``'error'`` what to do if resampling is out of bounds: - ``'transparent'``: fills with transparent medium. - ``'nan'``: fills with nan. - ``'error'``: raises an error. Default ``'nan'`` Other Parameters ---------------- verbose: bool if ``True``, more blabla. Default ``False`` modify_inputs: False if ``True``, slabs wavelengths/wavenumbers are modified directly when they are resampled. This avoids making a copy so it is slightly faster. Default ``False``. ..note:: for large number of slabs (in radiative transfer calculations) you surely want to use this option ! Returns ------- Spectrum object representing total emission and total transmittance as observed at the output (slab[n+1]). Conditions and units are transported too, unless there is a mismatch then conditions are dropped (and units mismatch raises an error because it doesnt make sense) Examples -------- Add s1 and s2 along the line of sight: s1 --> s2 :: s1 = calc_spectrum(...) s2 = calc_spectrum(...) s3 = SerialSlabs(s1, s2) The last line is equivalent to:: s3 = s1>s2 See Also -------- :func:`~radis.los.slabs.MergeSlabs` See more examples in the :ref:`Line-of-Sight module <label_los_index>` """ # TODO: rewrite with 'recompute' list like in MergeSlabs ? if "resample_wavespace" in kwargs: warn( DeprecationWarning( "'resample_wavespace' replaced with 'resample'")) kwargs["resample"] = kwargs.pop("resample_wavespace") if "out_of_bounds" in kwargs: warn(DeprecationWarning("'out_of_bounds' replaced with 'out'")) kwargs["out"] = kwargs.pop("out_of_bounds") # Check inputs, get defaults resample_wavespace = kwargs.pop("resample", "never") # default 'never' out_of_bounds = kwargs.pop("out", "nan") # default 'nan' verbose = kwargs.pop("verbose", False) # type: bool modify_inputs = kwargs.pop("modify_inputs", False) # type: bool if len(kwargs) > 0: raise ValueError("Unexpected input: {0}".format(list(kwargs.keys()))) if resample_wavespace not in ["never", "intersect", "full"]: raise ValueError("resample should be one of: {0}".format(", ".join( ["never", "intersect", "full"]))) if len(slabs) == 0: raise ValueError("Empty list of slabs") elif len(slabs) == 1: if not is_spectrum(slabs[0]): raise TypeError( "SerialSlabs takes an unfolded list of Spectrum as " + "argument: *list (got {0})".format(type(slabs[0]))) return slabs[0] else: # recursively calculate serial slabs slabs = list(slabs) # Recursively deal with the rest of Spectra --> call it s sn = slabs.pop(-1) # type: Spectrum _check_valid(sn) # check it is a spectrum s = SerialSlabs(*slabs, resample=resample_wavespace, out=out_of_bounds, modify_inputs=modify_inputs) # Now calculate sn and s in Serial quantities = {} unitsn = sn.units # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1') # also make sure we use the same units waveunit = s.get_waveunit() # Make all our slabs copies with the same wavespace range # (note: wavespace range may be different for different quantities, but # equal for all slabs) s, sn = resample_slabs(waveunit, resample_wavespace, out_of_bounds, modify_inputs, s, sn) try: w = s._q["wavespace"] except KeyError: raise KeyError( "Cannot calculate the RTE if non convoluted quantities " + "are not defined. Got: {0}".format(s.get_vars())) # Get all data # ------------- I, In, T, Tn = None, None, None, None # To make it easier, the radiative transfer equation is solved with 'radiance_noslit' and # 'transmittance_noslit' only. Here we first try to get these quantities: # ... get sn quantities try: sn.update("transmittance_noslit", verbose=verbose) except ValueError: pass else: Tn = sn.get( "transmittance_noslit", wunit=waveunit, Iunit=unitsn["transmittance_noslit"], copy=False, )[1] try: sn.update("radiance_noslit", verbose=verbose) except ValueError: pass else: In = sn.get( "radiance_noslit", wunit=waveunit, Iunit=unitsn["radiance_noslit"], copy=False, )[1] # ... get s quantities try: s.update("transmittance_noslit", verbose=verbose) except ValueError: pass else: T = s.get( "transmittance_noslit", wunit=waveunit, Iunit=unitsn["transmittance_noslit"], copy=False, )[1] try: s.update("radiance_noslit", verbose=verbose) except ValueError: pass else: I = s.get( "radiance_noslit", wunit=waveunit, Iunit=unitsn["radiance_noslit"], copy=False, )[1] # Solve radiative transfer equation # --------------------------------- if I is not None and In is not None: # case where we may use SerialSlabs just to compute the products of all transmittances quantities["radiance_noslit"] = (w, I * Tn + In) if T is not None: # note that we dont need the transmittance in the inner # slabs to calculate the total radiance quantities["transmittance_noslit"] = (w, Tn * T) # Get conditions (if they're different, fill with 'N/A') conditions = intersect(s.conditions, sn.conditions) conditions["waveunit"] = waveunit # sum path lengths if "path_length" in s.conditions and "path_length" in sn.conditions: conditions["path_length"] = (s.conditions["path_length"] + sn.conditions["path_length"]) cond_units = intersect(s.cond_units, sn.cond_units) # name name = _serial_slab_names(s, sn) return Spectrum( quantities=quantities, conditions=conditions, cond_units=cond_units, units=unitsn, name=name, warnings= False, # we already know waveranges are properly spaced, etc. )
def concat_spectra(s1, s2, var=None): """Concatenate two spectra ``s1`` and ``s2`` side by side. Note: their spectral range should not overlap Returns ------- s: Spectrum Spectrum object with the same units and waveunits as ``s1`` Parameters ---------- s1, s2: Spectrum objects Spectrum you want to concatenate var: str quantity to manipulate: 'radiance', 'transmittance', ... If ``None``, get the unique spectral quantity of ``s1``, or the unique spectral quantity of ``s2``, or raises an error if there is any ambiguity Notes ----- .. warning:: the output Spectrum has the sum of the spectral ranges of s1 and s2. It won't be evenly spaced. This means that you cannot apply a slit without side effects. Typically, you want to use this function for convolved quantities only, such as experimental spectra. Else, use :func:`~radis.los.slabs.MergeSlabs` with the options ``resample='full', out='transparent'`` See Also -------- :func:`~radis.spectrum.operations.add_spectra`, :func:`~radis.los.slabs.MergeSlabs` """ # Get variable if var is None: try: var = _get_unique_var( s2, var, inplace=False) # unique variable of 2nd spectrum except KeyError: var = _get_unique_var( s1, var, inplace=False ) # if doesnt exist, unique variable of 1st spectrum # if it fails, let it fail # Make sure it is in both Spectra if var not in s1.get_vars(): raise KeyError("Variable {0} not in Spectrum {1}".format( var, s1.get_name())) if var not in s2.get_vars(): raise KeyError("Variable {0} not in Spectrum {1}".format( var, s1.get_name())) if var in ["transmittance_noslit", "transmittance"]: warn( "It does not make much physical sense to sum transmittances. Are " + "you sure of what you are doing? See also // (MergeSlabs) and > " + "(SerialSlabs)") # Use same units Iunit1 = s1.units[var] wunit1 = s1.get_waveunit() # Get the value, on the same wunit) w1, I1 = s1.get(var=var, copy=False) # @dev: faster to just get the stored value. # it's copied in hstack() below anyway). w2, I2 = s2.get(var=var, Iunit=Iunit1, wunit=wunit1) if not (w1.max() < w2.min() or w2.max() > w1.min()): raise ValueError( "You cannot use concat_spectra for overlapping spectral ranges. " + "Got: {0:.2f}-{1:.2f} and {2:.2f}-{3:.2f} {4}. ".format( w1.min(), w1.max(), w2.min(), w2.max(), wunit1) + "Use MergeSlabs instead, with the correct `out=` parameter " + "for your case") w_tot = hstack((w1, w2)) I_tot = hstack((I1, I2)) name = s1.get_name() + "&" + s2.get_name() # use "&" instead of "+" concat = Spectrum.from_array(w_tot, I_tot, var, waveunit=wunit1, unit=Iunit1, name=name) return concat
def substract_spectra(s1, s2, var=None): """Return a new spectrum with ``s2`` substracted from ``s1``. Equivalent to:: s1 - s2 Parameters ---------- s1, s2: Spectrum objects Spectrum you want to substract var: str quantity to manipulate: 'radiance', 'transmittance', ... If ``None``, get the unique spectral quantity of ``s1``, or the unique spectral quantity of ``s2``, or raises an error if there is any ambiguity Returns ------- s: Spectrum Spectrum object with the same units and waveunits as ``s1`` See Also -------- :func:`~radis.spectrum.operations.add_spectra` """ # Get variable if var is None: try: var = _get_unique_var( s2, var, inplace=False) # unique variable of 2nd spectrum except KeyError: var = _get_unique_var( s1, var, inplace=False ) # if doesnt exist, unique variable of 1st spectrum # if it fails, let it fail # Make sure it is in both Spectra if var not in s1.get_vars(): raise KeyError("Variable {0} not in Spectrum {1}".format( var, s1.get_name())) if var not in s2.get_vars(): raise KeyError("Variable {0} not in Spectrum {1}".format( var, s1.get_name())) # Use same units Iunit1 = s1.units[var] wunit1 = s1.get_waveunit() # Resample s2 on s1 s2 = s2.resample(s1, inplace=False) # Substract w1, I1 = s1.get(var=var, Iunit=Iunit1, wunit=wunit1) w2, I2 = s2.get(var=var, Iunit=Iunit1, wunit=wunit1) name = s1.get_name() + "-" + s2.get_name() sub = Spectrum.from_array(w1, I1 - I2, var, waveunit=wunit1, unit=Iunit1, name=name) # warn("Conditions of the left spectrum were copied in the substraction.", Warning) return sub
def SerialSlabs(*slabs, **kwargs): # type: (*Spectrum, **dict) -> Spectrum ''' Adds several slabs along the line-of-sight. You can also use:: s1>s2>s3 Parameters ---------- slabs: list of Spectra, each representing a slab line-of-sight:: slabs [0] [1] ............... [n] : : : \==== light * -> * -> * -> )=== observer /==== resample_wavespace: ``'never'``, ``'intersect'``, ``'full'`` what to do when spectra have different wavespaces: - If ``'never'``, raises an error - If ``'intersect'``, uses the intersection of all ranges, and resample spectra on the most resolved wavespace. - If ``'full``', uses the overlap of all ranges, resample spectra on the most resolved wavespace, and fill missing data with 0 emission and 0 absorption Default ``'never'`` out: ``'transparent'``, ``'nan'``, ``'error'`` what to do if resampling is out of bounds: - ``'transparent'``: fills with transparent medium. - ``'nan'``: fills with nan. - ``'error'``: raises an error. Default ``'nan'`` Returns ------- Spectrum object representing total emission and total transmittance as observed at the output (slab[n+1]). Conditions and units are transported too, unless there is a mismatch then conditions are dropped (and units mismatch raises an error because it doesnt make sense) Examples -------- Add s1 and s2 along the line of sight: s1 --> s2 :: s1 = calc_spectrum(...) s2 = calc_spectrum(...) s3 = SerialSlabs(s1, s2) The last line is equivalent to:: s3 = s1>s2 Notes ----- Todo: - rewrite with 'recompute' list like in MergeSlabs See Also -------- :func:`~radis.los.slabs.MergeSlabs` See more examples in :ref:`Line-of-Sight module <label_los_index>` ''' if 'resample_wavespace' in kwargs: warn( DeprecationWarning( "'resample_wavespace' replaced with 'resample'")) kwargs['resample'] = kwargs.pop('resample_wavespace') if 'out_of_bounds' in kwargs: warn(DeprecationWarning("'out_of_bounds' replaced with 'out'")) kwargs['out'] = kwargs.pop('out_of_bounds') # Check inputs, get defaults resample_wavespace = kwargs.pop('resample', 'never') # default 'never' out_of_bounds = kwargs.pop('out', 'nan') # default 'nan' if len(kwargs) > 0: raise ValueError('Unexpected input: {0}'.format(list(kwargs.keys()))) if resample_wavespace not in ['never', 'intersect', 'full']: raise ValueError("resample should be one of: {0}".format(', '.join( ['never', 'intersect', 'full']))) if len(slabs) == 0: raise ValueError('Empty list of slabs') elif len(slabs) == 1: if not is_spectrum(slabs[0]): raise TypeError( 'SerialSlabs takes an unfolded list of Spectrum as ' + 'argument: *list (got {0})'.format(type(slabs[0]))) return slabs[0] else: # recursively calculate serial slabs slabs = list(slabs) # # Check all items are Spectrum for s in slabs: _check_valid(s) # Recursively deal with the rest of Spectra --> call it s sn = slabs.pop(-1) # type: Spectrum s = SerialSlabs(*slabs, resample=resample_wavespace, out=out_of_bounds) # Now calculate sn and s in Serial quantities = {} unitsn = sn.units # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1') # also make sure we use the same units waveunit = s.get_waveunit() # Make all our slabs copies with the same wavespace range # (note: wavespace range may be different for different quantities, but # equal for all slabs) s, sn = resample_slabs(waveunit, resample_wavespace, out_of_bounds, s, sn) try: w = s._q['wavespace'] except KeyError: raise KeyError('Cannot calculate the RTE if non convoluted quantities '+\ 'are not defined. Got: {0}'.format(s.get_vars())) # Get all data # ------------- I, In, T, Tn = None, None, None, None # To make it easier, the radiative transfer equation is solved with 'radiance_noslit' and # 'transmittance_noslit' only. Here we first try to get these quantities: # ... get sn quantities try: sn.update('transmittance_noslit', verbose=False) except ValueError: pass else: Tn = sn.get('transmittance_noslit', wunit=waveunit, Iunit=unitsn['transmittance_noslit'])[1] try: sn.update('radiance_noslit', verbose=False) except ValueError: pass else: In = sn.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit'])[1] # ... get s quantities try: s.update('transmittance_noslit', verbose=False) except ValueError: pass else: T = s.get('transmittance_noslit', wunit=waveunit, Iunit=unitsn['transmittance_noslit'])[1] try: s.update('radiance_noslit', verbose=False) except ValueError: pass else: I = s.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit'])[1] # Solve radiative transfer equation # --------------------------------- if I is not None and In is not None: # case where we may use SerialSlabs just to compute the products of all transmittances quantities['radiance_noslit'] = (w, I * Tn + In) if T is not None: # note that we dont need the transmittance in the inner # slabs to calculate the total radiance quantities['transmittance_noslit'] = (w, Tn * T) # Get conditions (if they're different, fill with 'N/A') conditions = intersect(s.conditions, sn.conditions) conditions['waveunit'] = waveunit # sum path lengths if 'path_length' in s.conditions and 'path_length' in sn.conditions: conditions['path_length'] = s.conditions[ 'path_length'] + sn.conditions['path_length'] cond_units = intersect(s.cond_units, sn.cond_units) # name name = _serial_slab_names(s, sn) return Spectrum(quantities=quantities, conditions=conditions, cond_units=cond_units, units=unitsn, name=name)
def test_broadening(rtol=1e-2, verbose=True, plot=False, *args, **kwargs): ''' Test broadening against HAPI and tabulated data We're looking at CO(0->1) line 'R1' at 2150.86 cm-1 ''' from radis.io.hapi import db_begin, fetch, tableList, absorptionCoefficient_Voigt if plot: # Make sure matplotlib is interactive so that test are not stuck in pytest plt.ion() setup_test_line_databases() # add HITRAN-CO-TEST in ~/.radis if not there # Conditions T = 3000 p = 0.0001 wstep = 0.001 wmin = 2150 # cm-1 wmax = 2152 # cm-1 broadening_max_width = 10 # cm-1 # %% HITRAN calculation # ----------- # Generate HAPI database locally db_begin(join(dirname(__file__), __file__.replace('.py', '_HAPIdata'))) if not 'CO' in tableList(): # only if data not downloaded already fetch('CO', 5, 1, wmin - broadening_max_width / 2, wmax + broadening_max_width / 2) # HAPI doesnt correct for side effects # Calculate with HAPI nu, coef = absorptionCoefficient_Voigt( SourceTables='CO', Environment={ 'T': T, # K 'p': p / 1.01325, # atm }, WavenumberStep=wstep, HITRAN_units=False) s_hapi = Spectrum.from_array(nu, coef, 'abscoeff', 'cm-1', 'cm_1', conditions={'Tgas': T}, name='HAPI') # %% Calculate with RADIS # ---------- sf = SpectrumFactory( wavenum_min=wmin, wavenum_max=wmax, mole_fraction=1, path_length=1, # doesnt change anything wstep=wstep, pressure=p, broadening_max_width=broadening_max_width, isotope=[1], warnings={ 'MissingSelfBroadeningWarning': 'ignore', 'NegativeEnergiesWarning': 'ignore', 'HighTemperatureWarning': 'ignore', 'GaussianBroadeningWarning': 'ignore' }) # 0.2) sf.load_databank('HITRAN-CO-TEST') # s = pl.non_eq_spectrum(Tvib=T, Trot=T, Ttrans=T) s = sf.eq_spectrum(Tgas=T, name='RADIS') if plot: # plot broadening of line of largest linestrength sf.plot_broadening(i=sf.df1.S.idxmax()) # Plot and compare res = abs(get_residual_integral(s, s_hapi, 'abscoeff')) if plot: plot_diff(s, s_hapi, var='abscoeff', title='{0} bar, {1} K (residual {2:.2g}%)'.format( p, T, res * 100), show_points=False) plt.xlim((wmin, wmax)) if verbose: printm('residual:', res) assert res < rtol
def non_eq_bands(self, Tvib, Trot, Ttrans=None, mole_fraction=None, path_length=None, pressure=None, vib_distribution='boltzmann', rot_distribution='boltzmann', levels='all', return_lines=None): ''' Calculate vibrational bands in non-equilibrium case. Calculates absorption with broadened linestrength and emission with broadened Einstein coefficient. Parameters ---------- Tvib: float vibrational temperature [K] can be a tuple of float for the special case of more-than-diatomic molecules (e.g: CO2) Trot: float rotational temperature [K] Ttrans: float translational temperature [K]. If None, translational temperature is taken as rotational temperature (valid at 1 atm for times above ~ 2ns which is the RT characteristic time) mole_fraction: float database species mole fraction. If None, Factory mole fraction is used. path_length: float slab size (cm). If None, Factory mole fraction is used. pressure: float pressure (bar). If None, the default Factory pressure is used. Other Parameters ---------------- levels: ``'all'``, int, list of str calculate only bands that feature certain levels. If ``'all'``, all bands are returned. If N (int), return bands for the first N levels (sorted by energy). If list of str, return for all levels in the list. The remaining levels are also calculated and returned merged together in the ``'others'`` key. Default ``'all'`` return_lines: boolean if ``True`` returns each band with its line database. Can produce big spectra! Default ``True`` DEPRECATED. Now use export_lines attribute in Factory Returns ------- Returns :class:`~radis.spectrum.spectrum.Spectrum` object Use .get(something) to get something among ['radiance', 'radiance_noslit', 'absorbance', etc...] Or directly .plot(something) to plot it ''' try: # check inputs, update defaults if path_length is not None: self.input.path_length = path_length if mole_fraction is not None: self.input.mole_fraction = mole_fraction if pressure is not None: self.input.pressure_mbar = pressure * 1e3 if not (is_float(Tvib) or isinstance(Tvib, tuple)): raise TypeError( 'Tvib should be float, or tuple (got {0})'.format( type(Tvib)) + 'For parallel processing use ParallelFactory with a ' + 'list of float or a list of tuple') singleTvibmode = is_float(Tvib) if not is_float(Trot): raise ValueError( 'Trot should be float. Use ParallelFactory for multiple cases' ) assert type(levels) in [str, list, int] if type(levels) == str: assert levels == 'all' else: if len(levels) != len(set(levels)): raise ValueError('levels list has duplicates') if not vib_distribution in ['boltzmann']: raise ValueError( 'calculate per band not meaningful if not Boltzmann') # Temporary: if type(levels) == int: raise NotImplementedError if return_lines is not None: warn( DeprecationWarning( 'return_lines replaced with export_lines attribute in Factory' )) self.misc.export_lines = return_lines # Get translational temperature Tgas = Ttrans if Tgas is None: Tgas = Trot # assuming Ttrans = Trot self.input.Tgas = Tgas self.input.Tvib = Tvib self.input.Trot = Trot # Init variables path_length = self.input.path_length mole_fraction = self.input.mole_fraction pressure_mbar = self.input.pressure_mbar verbose = self.verbose # %% Retrieve from database if exists if self.autoretrievedatabase: s = self._retrieve_bands_from_database() if s is not None: return s # Print conditions if verbose: print('Calculating Non-Equilibrium bands') self.print_conditions() # %% Make sure database is loaded self._check_line_databank() self._check_noneq_parameters(vib_distribution, singleTvibmode) if self.df0 is None: raise AttributeError('Load databank first (.load_databank())') # Make sure database has pre-computed non equilibrium quantities # (Evib, Erot, etc.) if not 'Evib' in self.df0: self._calc_noneq_parameters() if not 'Aul' in self.df0: self._calc_weighted_trans_moment() self._calc_einstein_coefficients() if not 'band' in self.df0: self._add_bands() # %% Calculate the spectrum # --------------------------------------------------- t0 = time() self._reinitialize() # ---------------------------------------------------------------------- # Calculate Populations, Linestrength and Emission Integral # (Note: Emission Integral is non canonical quantity, equivalent to # Linestrength for absorption) self._calc_populations_noneq(Tvib, Trot) self._calc_linestrength_noneq() self._calc_emission_integral() # ---------------------------------------------------------------------- # Cutoff linestrength self._cutoff_linestrength() # ---------------------------------------------------------------------- # Calculate lineshift self._calc_lineshift() # ---------------------------------------------------------------------- # Line broadening # ... calculate broadening FWHM self._calc_broadening_FWHM() # ... find weak lines and calculate semi-continuum (optional) I_continuum = self._calculate_pseudo_continuum() if I_continuum: raise NotImplementedError( 'pseudo continuum not implemented for bands') # ... apply lineshape and get absorption coefficient # ... (this is the performance bottleneck) wavenumber, abscoeff_v_bands, emisscoeff_v_bands = self._calc_broadening_noneq_bands( ) # : : : # cm-1 1/(#.cm-2) mW/sr/cm_1 # # ... add semi-continuum (optional) # abscoeff_v_bands = self._add_pseudo_continuum(abscoeff_v_bands, I_continuum) # ---------------------------------------------------------------------- # Remove bands if levels != 'all': # Filter levels that feature the given energy levels. The rest # is stored in 'others' lines = self.df1 # We need levels to be explicitely stated for given molecule assert hasattr(lines, 'viblvl_u') assert hasattr(lines, 'viblvl_l') # Get bands to remove merge_bands = [] for band in abscoeff_v_bands: # note: could be vectorized with pandas str split. # TODO viblvl_l, viblvl_u = band.split('->') if not viblvl_l in levels and not viblvl_u in levels: merge_bands.append(band) # Remove bands from bandlist and add them to `others` abscoeff_others = np.zeros_like(wavenumber) emisscoeff_others = np.zeros_like(wavenumber) for band in merge_bands: abscoeff = abscoeff_v_bands.pop(band) emisscoeff = emisscoeff_v_bands.pop(band) abscoeff_others += abscoeff emisscoeff_others += emisscoeff abscoeff_v_bands['others'] = abscoeff_others emisscoeff_v_bands['others'] = emisscoeff_others if verbose: print('{0} bands grouped under `others`'.format( len(merge_bands))) # ---------------------------------------------------------------------- # Generate spectra # Progress bar for spectra generation Nbands = len(abscoeff_v_bands) if self.verbose: print('Generating bands ({0})'.format(Nbands)) pb = ProgressBar(Nbands, active=self.verbose) if Nbands < 100: pb.set_active(False) # hide for low line number # Create spectra s_bands = {} for i, band in enumerate(abscoeff_v_bands): abscoeff_v = abscoeff_v_bands[band] emisscoeff_v = emisscoeff_v_bands[band] # incorporate density of molecules (see equation (A.16) ) density = mole_fraction * ((pressure_mbar * 100) / (k_b * Tgas)) * 1e-6 # : # (#/cm3) abscoeff = abscoeff_v * density # cm-1 emisscoeff = emisscoeff_v * density # m/sr/cm3/cm_1 # ============================================================================== # Warning # --------- # if the code is extended to multi-species, then density has to be added # before lineshape broadening (as it would not be constant for all species) # ============================================================================== # get absorbance (technically it's the optical depth `tau`, # absorbance `A` being `A = tau/ln(10)` ) # Generate output quantities absorbance = abscoeff * path_length # (adim) transmittance_noslit = exp(-absorbance) if self.input.self_absorption: # Analytical output of computing RTE over a single slab of constant # emissivity and absorption coefficient b = abscoeff == 0 # optically thin mask radiance_noslit = np.zeros_like(emisscoeff) radiance_noslit[~b] = emisscoeff[~b] / \ abscoeff[~b]*(1-transmittance_noslit[~b]) radiance_noslit[b] = emisscoeff[b] * path_length else: # Note that for k -> 0, radiance_noslit = emisscoeff * \ path_length # (mW/sr/cm2/cm_1) # Convert `radiance_noslit` from (mW/sr/cm2/cm_1) to (mW/sr/cm2/nm) radiance_noslit = convert_rad2nm(radiance_noslit, wavenumber, 'mW/sr/cm2/cm_1', 'mW/sr/cm2/nm') # Convert 'emisscoeff' from (mW/sr/cm3/cm_1) to (mW/sr/cm3/nm) emisscoeff = convert_emi2nm(emisscoeff, wavenumber, 'mW/sr/cm3/cm_1', 'mW/sr/cm3/nm') # Note: emissivity not defined under non equilibrium # ----------------------------- Export: lines = self.df1[self.df1.band == band] # Note: if band == 'others': # for others: all will be None. # TODO. FIXME populations = self.get_populations( self.misc.export_populations) if not self.misc.export_lines: lines = None # Store results in Spectrum class if self.save_memory: try: # saves some memory (note: only once 'lines' is discarded) del self.df1 except AttributeError: # already deleted pass conditions = self.get_conditions() conditions.update({'thermal_equilibrium': False}) # Add band name and hitran band name in conditions def add_attr(attr): ''' # TODO: implement properly''' if attr in lines: if band == 'others': val = 'N/A' else: # all have to be the same val = lines[attr].iloc[0] conditions.update({attr: val}) add_attr('band_htrn') add_attr('viblvl_l') add_attr('viblvl_u') s = Spectrum( quantities={ 'abscoeff': (wavenumber, abscoeff), 'absorbance': (wavenumber, absorbance), # (mW/cm3/sr/nm) 'emisscoeff': (wavenumber, emisscoeff), 'transmittance_noslit': (wavenumber, transmittance_noslit), # (mW/cm2/sr/nm) 'radiance_noslit': (wavenumber, radiance_noslit), }, conditions=conditions, populations=populations, lines=lines, units=self.units, cond_units=self.cond_units, waveunit=self.params.waveunit, # cm-1 name=band, # dont check input (much faster, and Spectrum warnings=False, # is freshly baken so probably in a good format ) # # update database if asked so # if self.autoupdatedatabase: # self.SpecDatabase.add(s, add_info=['Tvib', 'Trot'], add_date='%Y%m%d') s_bands[band] = s pb.update(i) # progress bar pb.done() if verbose: print(('... process done in {0:.1f}s'.format(time() - t0))) return s_bands except: # An error occured: clean before crashing self._clean_temp_file() raise
def SerialSlabs(*slabs, **kwargs): ''' Compute the result of several slabs Parameters ---------- slabs: list of Spectra, each representing a slab slabs [0] [1] ............... [n] : : : \==== light * -> * -> * -> )=== observer /==== resample_wavespace: 'never', 'intersect', 'full' what to do when spectra have different wavespaces. - If 'never', raises an error - If 'intersect', uses the intersection of all ranges, and resample spectra on the most resolved wavespace. - If 'full', uses the overlap of all ranges, resample spectra on the most resolved wavespace, and fill missing data with 0 emission and 0 absorption Default 'never' out_of_bounds: 'transparent', 'nan', 'error' what to do if resampling is out of bounds. 'transparent': fills with transparent medium. 'nan': fills with nan. 'error': raises an error. Default 'nan' Returns ------- Spectrum object representing total emission and total transmittance as observed at the output (slab[n+1]). Conditions and units are transported too, unless there is a mismatch then conditions are dropped (and units mismatch raises an error because it doesnt make sense) Examples -------- Add s1 and s2 along the line of sight: s1 --> s2 :: s1 = calc_spectrum(...) s2 = calc_spectrum(...) s3 = SerialSlabs(s1, s2) Notes ----- Todo: - rewrite with 'recompute' list like in MergeSlabs See Also -------- :func:`~radis.los.slabs.MergeSlabs` ''' # Check inputs, get defaults resample_wavespace = kwargs.pop('resample_wavespace', 'never') # default 'never' out_of_bounds = kwargs.pop('out_of_bounds', 'nan') # default 'nan' if len(kwargs) > 0: raise ValueError('Unexpected input: {0}'.format(list(kwargs.keys()))) if resample_wavespace not in ['never', 'intersect', 'full']: raise ValueError("resample_wavespace should be one of: {0}".format( ', '.join(['never', 'intersect', 'full']))) if len(slabs) == 0: raise ValueError('Empty list of slabs') elif len(slabs) == 1: if not is_spectrum(slabs[0]): raise TypeError('SerialSlabs takes an unfolded list of Spectrum as '+\ 'argument: *list (got {0})'.format(type(slabs[0]))) return slabs[0] else: # recursively calculate serial slabs slabs = list(slabs) # # Check all items are Spectrum for s in slabs: _check_valid(s) sn = slabs.pop(-1) # type: Spectrum s = SerialSlabs(*slabs, resample_wavespace=resample_wavespace, out_of_bounds=out_of_bounds) quantities = {} unitsn = sn.units # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1') # also make sure we use the same units waveunit = s.get_waveunit() w = s.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit'])[0] wn = sn.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit'])[0] # Make all our slabs copies with the same wavespace range # (note: wavespace range may be different for different quantities, but # equal for all slabs) s, sn = resample_slabs(waveunit, resample_wavespace, out_of_bounds, s, sn) w, I = s.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit']) wn, In = sn.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit']) _, Tn = sn.get('transmittance_noslit', wunit=waveunit, Iunit=unitsn['transmittance_noslit']) if 'radiance_noslit' in s._q and 'radiance_noslit' in sn._q: # case where we may use SerialSlabs just to compute the products of all transmittances quantities['radiance_noslit'] = (w, I * Tn + In) if 'transmittance_noslit' in s._q: # note that we dont need the transmittance in the inner # slabs to calculate the total radiance _, T = s.get('transmittance_noslit', wunit=waveunit, Iunit=unitsn['transmittance_noslit']) quantities['transmittance_noslit'] = (w, Tn * T) # Get conditions (if they're different, fill with 'N/A') conditions = intersect(s.conditions, sn.conditions) conditions['waveunit'] = waveunit cond_units = intersect(s.cond_units, sn.cond_units) # name name = _serial_slab_names(s, sn) return Spectrum(quantities=quantities, conditions=conditions, cond_units=cond_units, units=unitsn, name=name)
def MergeSlabs(*slabs, **kwargs): ''' Combines several slabs into one. Useful to calculate multi-gas slabs. Linear absorption coefficient is calculated as the sum of all linear absorption coefficients, and the RTE is recalculated to get the total radiance Parameters ---------- slabs: list of Spectra, each representing a slab If given in conditions, all path_length have to be same Other Parameters ---------------- kwargs input: resample_wavespace: 'never', 'intersect', 'full' what to do when spectra have different wavespaces. - If 'never', raises an error - If 'intersect', uses the intersection of all ranges, and resample spectra on the most resolved wavespace. - If 'full', uses the overlap of all ranges, resample spectra on the most resolved wavespace, and fill missing data with 0 emission and 0 absorption Default 'never' out_of_bounds: 'transparent', 'nan', 'error' what to do if resampling is out of bounds. 'transparent': fills with transparent medium. 'nan': fills with nan. 'error': raises an error. Default 'nan' optically_thin: boolean if True, merge slabs in optically thin mode. Default False verbose: boolean if True, print messages and warnings. Default True Returns ------- Spectrum object representing total emission and total transmittance as observed at the output. Conditions and units are transported too, unless there is a mismatch then conditions are dropped (and units mismatch raises an error because it doesnt make sense) Examples -------- Merge two spectra calculated with different species (true only if broadening coefficient dont change much): >>> from radis import calc_spectrum, MergeSlabs >>> s1 = calc_spectrum(...) >>> s2 = calc_spectrum(...) >>> s3 = MergeSlabs(s1, s2) Load a spectrum precalculated on several partial spectral ranges, for a same molecule (i.e, partial spectra are optically thin on the rest of the spectral range) >>> from radis import load_spec, MergeSlabs >>> spectra = [] >>> for f in ['spec1.spec', 'spec2.spec', ...]: >>> spectra.append(load_spec(f)) >>> s = MergeSlabs(*spectra, resample_wavespace='full', out_of_bounds='transparent') >>> s.update() # Generate missing spectral quantities >>> s.plot() See Also -------- :func:`~radis.los.slabs.SerialSlabs` ''' # inputs (Python 2 compatible) resample_wavespace = kwargs.pop('resample_wavespace', 'never') # default 'never' out_of_bounds = kwargs.pop('out_of_bounds', 'nan') # default 'nan' optically_thin = kwargs.pop('optically_thin', False) # default False verbose = kwargs.pop('verbose', True) # type: bool debug = kwargs.pop('debug', False) # type: bool if len(kwargs) > 0: raise ValueError('Unexpected input: {0}'.format(list(kwargs.keys()))) # Check inputs if resample_wavespace not in ['never', 'intersect', 'full']: raise ValueError("resample_wavespace should be one of: {0}".format( ', '.join(['never', 'intersect', 'full']))) if len(slabs) == 0: raise ValueError('Empty list of slabs') elif len(slabs) == 1: if not is_spectrum(slabs[0]): raise TypeError('MergeSlabs takes an unfolded list of Spectrum as '+\ 'argument: (got {0})'.format(type(slabs[0]))) return slabs[0] else: # calculate serial slabs slabs = list(slabs) # # Check all items are valid Spectrum objects for s in slabs: _check_valid(s) # Just check all path_lengths are the same if they exist path_lengths = [ s.conditions['path_length'] for s in slabs if 'path_length' in s.conditions ] if not all([L == path_lengths[0] for L in path_lengths[1:]]): raise ValueError('path_length must be equal for all MergeSlabs inputs'+\ ' (got {0})'.format(path_lengths)) # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1') waveunit = slabs[0].get_waveunit() # Make all our slabs copies with the same wavespace range # (note: wavespace range may be different for different quantities, but # equal for all slabs) slabs = resample_slabs(waveunit, resample_wavespace, out_of_bounds, *slabs) w_noconv = slabs[0]._get_wavespace() # %% # Get conditions conditions = slabs[0].conditions conditions['waveunit'] = waveunit cond_units = slabs[0].cond_units units0 = slabs[0].units for s in slabs[1:]: conditions = intersect(conditions, s.conditions) cond_units = intersect(cond_units, s.cond_units) #units = intersect(units0, s.units) # we're actually using [slabs0].units insteads # %% Get quantities that should be calculated requested = merge_lists([s.get_vars() for s in slabs]) recompute = requested[:] # copy if ('radiance_noslit' in requested and not optically_thin): recompute.append('emisscoeff') recompute.append('abscoeff') if 'abscoeff' in recompute and 'path_length' in conditions: recompute.append('absorbance') recompute.append('transmittance_noslit') # To make it easier, we start from abscoeff and emisscoeff of all slabs # Let's recompute them all # TODO: if that changes the initial Spectra, maybe we should just work on copies for s in slabs: if 'abscoeff' in recompute and not 'abscoeff' in list(s._q.keys()): s.update('abscoeff') # that may crash if Spectrum doesnt have the correct inputs. # let update() handle that if 'emisscoeff' in recompute and not 'emisscoeff' in list( s._q.keys()): s.update('emisscoeff') # same path_length = conditions['path_length'] # %% Calculate new quantites from emisscoeff and abscoeff # TODO: rewrite all of the above with simple calls to .update() added = {} # ... absorption coefficient (cm-1) if 'abscoeff' in recompute: #TODO: deal with all cases if __debug__: printdbg('... merge: calculating abscoeff k=sum(k_i)') abscoeff_eq = np.sum([ s.get('abscoeff', wunit=waveunit, Iunit=units0['abscoeff'])[1] for s in slabs ], axis=0) assert len(w_noconv) == len(abscoeff_eq) added['abscoeff'] = (w_noconv, abscoeff_eq) if 'absorbance' in recompute: if 'abscoeff' in added: if __debug__: printdbg('... merge: calculating absorbance A=k*L') _, abscoeff_eq = added['abscoeff'] absorbance_eq = abscoeff_eq * path_length else: raise NotImplementedError('recalculate abscoeff first') added['absorbance'] = (w_noconv, absorbance_eq) # ... transmittance if 'transmittance_noslit' in recompute: if 'absorbance' in added: if __debug__: printdbg('... merge: calculating transmittance T=exp(-A)') _, absorbance_eq = added['absorbance'] transmittance_noslit_eq = exp(-absorbance_eq) else: raise NotImplementedError('recalculate absorbance first') added['transmittance_noslit'] = (w_noconv, transmittance_noslit_eq) # ... emission coefficient if 'emisscoeff' in recompute: emisscoeff_eq = np.zeros_like(w_noconv) for i, s in enumerate(slabs): # Manual loop in case all Slabs dont have the same keys # Could also do a slab.update() first then sum emisscoeff directly if 'emisscoeff' in list(s._q.keys()): if __debug__: printdbg('... merge: calculating emisscoeff: j+=j_i') _, emisscoeff = s.get('emisscoeff', wunit=waveunit, Iunit=units0['emisscoeff']) elif optically_thin and 'radiance_noslit' in list(s._q.keys()): if __debug__: printdbg('... merge: calculating emisscoeff: j+=I_i/L '+\ '(optically thin case)') _, I = s.get('radiance_noslit', wunit=waveunit, Iunit=units0['radiance_noslit']) emisscoeff = I / path_length emisscoeff_eq += emisscoeff else: wI, I = s.get('radiance_noslit', wunit=waveunit, Iunit=units0['radiance_noslit']) if __debug__: printdbg( '... merge: calculating emisscoeff j+=[k*I/(1-T)]_i)' ) try: wT, T = s.get('transmittance_noslit', wunit=waveunit, Iunit=units0['transmittance_noslit']) wk, k = s.get('abscoeff', wunit=waveunit, Iunit=units0['abscoeff']) except KeyError: raise KeyError('Need transmittance_noslit and abscoeff to '+\ 'recompute emission coefficient') b = (T == 1) # optically thin mask emisscoeff = np.zeros_like(T) emisscoeff[b] = I[b] / path_length # optically thin case emisscoeff[~b] = I[~b] / (1 - T[~b]) * k[~b] emisscoeff_eq += emisscoeff added['emisscoeff'] = (w_noconv, emisscoeff_eq) # ... derive global radiance (result of analytical RTE solving) if 'radiance_noslit' in recompute: if 'emisscoeff' in added and optically_thin: if __debug__: printdbg( '... merge: recalculating radiance_noslit I=j*L(optically_thin)' ) (_, emisscoeff_eq) = added['emisscoeff'] radiance_noslit_eq = emisscoeff_eq * path_length # optically thin elif ('emisscoeff' in added and 'transmittance_noslit' in added and 'abscoeff' in added): if __debug__: printdbg( '... merge: recalculating radiance_noslit I=j/k*(1-T)') (_, emisscoeff_eq) = added['emisscoeff'] (_, abscoeff_eq) = added['abscoeff'] (_, transmittance_noslit_eq) = added['transmittance_noslit'] b = (abscoeff_eq == 0) # optically thin mask radiance_noslit_eq = np.zeros_like(emisscoeff_eq) radiance_noslit_eq[ b] = emisscoeff_eq[b] * path_length # optically thin limit radiance_noslit_eq[~b] = emisscoeff_eq[~b] / abscoeff_eq[ ~b] * (1 - transmittance_noslit_eq[~b]) elif optically_thin: if __debug__: printdbg( '... merge: recalculating radiance_noslit I=sum(I_i) (optically thin)' ) radiance_noslit_eq = np.zeros_like(w_noconv) for s in slabs: if 'radiance_noslit' in list(s._q.keys()): radiance_noslit_eq += s.get( 'radiance_noslit', wunit=waveunit, Iunit=units0['radiance_noslit'])[1] else: raise KeyError('Need radiance_noslit for all slabs to '+\ 'recalculate for the MergeSlab (could also '+\ 'get it from emisscoeff but not implemented)') else: if optically_thin: raise ValueError('Missing data to recalculate radiance_noslit'+\ '. Try optically_thin mode?') else: raise ValueError( 'Missing data to recalculate radiance_noslit') added['radiance_noslit'] = (w_noconv, radiance_noslit_eq) # ... emissivity no slit if 'emissivity_noslit' in requested: added['emissivity_noslit'] = w_noconv, np.ones_like( w_noconv) * np.nan # if verbose: # warn('emissivity dropped during MergeSlabs') # Todo: deal with equilibrium cases? # Store output quantities = {} for k in requested: quantities[k] = added[k] # name name = '//'.join([s.get_name() for s in slabs]) # TODO: check units are consistent in all slabs inputs return Spectrum(quantities=quantities, conditions=conditions, cond_units=cond_units, units=units0, name=name)
def sPlanck(wavenum_min=None, wavenum_max=None, wavelength_min=None, wavelength_max=None, T=None, eps=1, wstep=0.01, medium="air", **kwargs): """Return a RADIS Spectrum object with blackbody radiation. It's easier to plug in a MergeSlabs / SerialSlabs config than the Planck radiance calculated by iPlanck. And you don't need to worry about units as they are handled internally. See radis.lbl.Spectrum documentation for more information Parameters ---------- wavenum_min / wavenum_max: (cm-1) minimum / maximum wavenumber to be processed in cm^-1. wavelength_min / wavelength_max: (nm) minimum / maximum wavelength to be processed in nm T: float (K) blackbody temperature eps: float [0-1] blackbody emissivity. Default 1 Other Parameters ---------------- wstep: float (cm-1 or nm) wavespace step for calculation **kwargs: other keyword inputs all are forwarded to spectrum conditions. For instance you can add a 'path_length=1' after all the other arguments Example ------- Generate Earth blackbody:: s = sPlanck(wavelength_min=3000, wavelength_max=50000, T=288, eps=1) s.plot() """ from radis.spectrum.spectrum import Spectrum # Check inputs if (wavelength_min is not None or wavelength_max is not None) and (wavenum_min is not None or wavenum_max is not None): raise ValueError("You cannot give both wavelengths and wavenumbers") if wavenum_min is not None and wavenum_max is not None: assert wavenum_min < wavenum_max waveunit = "cm-1" else: assert wavelength_min < wavelength_max if medium == "air": waveunit = "nm" elif medium == "vacuum": waveunit = "nm_vac" else: raise ValueError(medium) if T is None: raise ValueError("T must be defined") if not (eps >= 0 and eps <= 1): raise ValueError("Emissivity must be in [0-1]") # Test range is correct: if waveunit == "cm-1": # generate the vector of wavenumbers (shape M) w = arange(wavenum_min, wavenum_max + wstep, wstep) Iunit = "mW/sr/cm2/cm-1" I = planck_wn(w, T, eps=eps, unit=Iunit) else: # generate the vector of lengths (shape M) w = arange(wavelength_min, wavelength_max + wstep, wstep) Iunit = "mW/sr/cm2/nm" if waveunit == "nm_vac": w_vac = w elif waveunit == "nm": w_vac = air2vacuum(w) # calculate planck with wavelengths in vacuum I = planck(w_vac, T, eps=eps, unit=Iunit) conditions = {"wstep": wstep} # add all extra parameters in conditions (ex: path_length) conditions.update(**kwargs) return Spectrum( quantities={ "radiance_noslit": (w, I), "transmittance_noslit": (w, zeros_like(w)), "absorbance": (w, ones_like(w) * inf), }, conditions=conditions, units={ "radiance_noslit": Iunit, "transmittance_noslit": "", "absorbance": "" }, cond_units={"wstep": waveunit}, waveunit=waveunit, name="Planck {0}K, eps={1:.2g}".format(T, eps), )
def calculated_spectrum(w, I, wunit='nm', Iunit='mW/cm2/sr/nm', conditions=None, cond_units=None, populations=None, name=None): # -> Spectrum: ''' Convert (w, I) into a Spectrum object that has unit conversion, plotting and slit convolution capabilities Parameters ---------- w, I: np.array wavelength and intensity wunit: 'nm', 'cm-1' wavespace unit Iunit: str intensity unit (can be 'counts', 'mW/cm2/sr/nm', etc...). Default 'mW/cm2/sr/nm' (note that non-convoluted Specair spectra are in 'mW/cm2/sr/µm') Other Parameters ---------------- conditions: dict (optional) calculation conditions to be stored with Spectrum. Default ``None`` cond_units: dict (optional) calculation conditions units. Default ``None`` populations: dict populations to be stored in Spectrum. Default ``None`` name: str (optional) give a name See Also -------- :func:`~radis.spectrum.spectrum.transmittance_spectrum`, :func:`~radis.spectrum.spectrum.experimental_spectrum`, :meth:`~radis.spectrum.spectrum.Spectrum.from_array`, :meth:`~radis.spectrum.spectrum.Spectrum.from_txt`, :func:`~radis.tools.database.load_spec` ''' return Spectrum.from_array(np.array(w), np.array(I), 'radiance_noslit', waveunit=wunit, unit=Iunit, conditions=conditions, cond_units=cond_units, populations=populations, name=name)
def _json_to_spec(jsondict, file=''): ''' Builds a Spectrum object from a JSON dictionary. Called by load_spec Parameters ---------- jsondict: dict Spectrum object content stored under a dictonary Returns ------- s: Spectrum a :class:`~radis.spectrum.spectrum.Spectrum` object ''' # Test format / correct deprecated format: sload = _fix_format(file, jsondict) # ... Back to real stuff: conditions = sload['conditions'] # Get quantities if 'quantities' in sload: # old format -saved with tuples (w,I) under 'quantities'): heavier, but # easier to generate a spectrum quantities = { k: (np.array(v[0]), array(v[1])) for (k, v) in sload['quantities'].items() } warn("File {0}".format(basename(file))+" has a deprecrated structure ("+\ "quantities are stored with shared wavespace: uses less space). "+\ "Regenerate database ASAP.", DeprecationWarning) else: quantities = { k: (sload['_q']['wavespace'], v) for k, v in sload['_q'].items() if k != 'wavespace' } quantities.update({ k: (sload['_q_conv']['wavespace'], v) for k, v in sload['_q_conv'].items() if k != 'wavespace' }) # Generate spectrum: waveunit = sload['conditions']['waveunit'] # Only `quantities` and `conditions` is required. The rest is just extra # details kwargs = {} # ... load slit if exists if 'slit' in sload: slit = {k: np.array(v) for k, v in sload['slit'].items()} else: slit = {} # ... load lines if exist if 'lines' in sload: df = sload['lines'] kwargs['lines'] = df else: kwargs['lines'] = None # ... load populations if exist if 'populations' in sload: # Fix some problems in json-tricks # ... cast isotopes to int (getting sload['populations'] directly doesnt do that) kwargs['populations'] = {} for molecule, isotopes in sload['populations'].items(): kwargs['populations'][molecule] = {} for isotope, states in isotopes.items(): try: isotope = int(isotope) except ValueError: pass # keep isotope as it was kwargs['populations'][molecule][isotope] = states else: kwargs['populations'] = None # ... load other properties if exist for attr in ['units', 'cond_units', 'name']: try: kwargs[attr] = sload[attr] except KeyError: kwargs[attr] = None s = Spectrum(quantities=quantities, conditions=conditions, waveunit=waveunit, **kwargs) # ... add slit s._slit = slit return s
def test_broadening_vs_hapi(rtol=1e-2, verbose=True, plot=False, *args, **kwargs): """ Test broadening against HAPI and tabulated data We're looking at CO(0->1) line 'R1' at 2150.86 cm-1 """ from hapi import absorptionCoefficient_Voigt, db_begin, fetch, tableList if plot: # Make sure matplotlib is interactive so that test are not stuck in pytest plt.ion() setup_test_line_databases() # add HITRAN-CO-TEST in ~/.radis if not there # Conditions T = 3000 p = 0.0001 wstep = 0.001 wmin = 2150 # cm-1 wmax = 2152 # cm-1 broadening_max_width = 10 # cm-1 # %% HITRAN calculation # ----------- # Generate HAPI database locally hapi_data_path = join(dirname(__file__), __file__.replace(".py", "_HAPIdata")) db_begin(hapi_data_path) if not "CO" in tableList(): # only if data not downloaded already fetch("CO", 5, 1, wmin - broadening_max_width / 2, wmax + broadening_max_width / 2) # HAPI doesnt correct for side effects # Calculate with HAPI nu, coef = absorptionCoefficient_Voigt( SourceTables="CO", Environment={ "T": T, "p": p / 1.01325, }, # K # atm WavenumberStep=wstep, HITRAN_units=False, ) s_hapi = Spectrum.from_array(nu, coef, "abscoeff", "cm-1", "cm-1", conditions={"Tgas": T}, name="HAPI") # %% Calculate with RADIS # ---------- sf = SpectrumFactory( wavenum_min=wmin, wavenum_max=wmax, mole_fraction=1, path_length=1, # doesnt change anything wstep=wstep, pressure=p, broadening_max_width=broadening_max_width, isotope=[1], warnings={ "MissingSelfBroadeningWarning": "ignore", "NegativeEnergiesWarning": "ignore", "HighTemperatureWarning": "ignore", "GaussianBroadeningWarning": "ignore", }, ) # 0.2) sf.load_databank(path=join(hapi_data_path, "CO.data"), format="hitran", parfuncfmt="hapi") # s = pl.non_eq_spectrum(Tvib=T, Trot=T, Ttrans=T) s = sf.eq_spectrum(Tgas=T, name="RADIS") if plot: # plot broadening of line of largest linestrength sf.plot_broadening(i=sf.df1.S.idxmax()) # Plot and compare res = abs(get_residual_integral(s, s_hapi, "abscoeff")) if plot: plot_diff( s, s_hapi, var="abscoeff", title="{0} bar, {1} K (residual {2:.2g}%)".format(p, T, res * 100), show_points=False, ) plt.xlim((wmin, wmax)) if verbose: printm("residual:", res) assert res < rtol
def sPlanck(wavenum_min=None, wavenum_max=None, wavelength_min=None, wavelength_max=None, T=None, eps=1, wstep=0.01, medium='air', **kwargs): ''' Return a RADIS Spectrum object with blackbody radiation. It's easier to plug in a MergeSlabs / SerialSlabs config than the Planck radiance calculated by iPlanck. And you don't need to worry about units as they are handled internally. See neq.spec.Spectrum documentation for more information Parameters ---------- wavenum_min / wavenum_max: (cm-1) minimum / maximum wavenumber to be processed in cm^-1. wavelength_min / wavelength_max: (nm) minimum / maximum wavelength to be processed in nm T: float (K) blackbody temperature eps: float [0-1] blackbody emissivity. Default 1 Other Parameters ---------------- wstep: float (cm-1 or nm) wavespace step for calculation **kwargs: other keyword inputs all are forwarded to spectrum conditions. For instance you can add a 'path_length=1' after all the other arguments Example ------- Generate Earth blackbody:: s = sPlanck(wavelength_min=3000, wavelength_max=50000, T=288, eps=1) s.plot() ''' from radis.spectrum.spectrum import Spectrum # Check inputs if ((wavelength_min is not None or wavelength_max is not None) and (wavenum_min is not None or wavenum_max is not None)): raise ValueError('Wavenumber and Wavelength both given... you twart') if (wavenum_min is not None and wavenum_max is not None): assert (wavenum_min < wavenum_max) waveunit = 'cm-1' else: assert (wavelength_min < wavelength_max) waveunit = 'nm' if T is None: raise ValueError('T must be defined') if not (eps >= 0 and eps <= 1): raise ValueError('Emissivity must be in [0-1]') # Test range is correct: if waveunit == 'cm-1': w = arange(wavenum_min, wavenum_max + wstep, wstep) #generate the vector of wavenumbers (shape M) Iunit = 'mW/sr/cm2/cm_1' I = planck_wn(w, T, eps=eps, unit=Iunit) else: w = arange(wavelength_min, wavelength_max + wstep, wstep) #generate the vector of wavenumbers (shape M) Iunit = 'mW/sr/cm2/nm' I = planck(w, T, eps=eps, unit=Iunit) conditions = {'wstep': wstep, 'medium': medium} conditions.update( **kwargs) # add all extra parameters in conditions (ex: path_length) return Spectrum(quantities={ 'radiance_noslit': (w, I), 'transmittance_noslit': (w, zeros_like(w)), 'absorbance': (w, ones_like(w) * inf) }, conditions=conditions, units={ 'radiance_noslit': Iunit, 'transmittance_noslit': 'I/I0', 'absorbance': '-ln(I/I0)' }, cond_units={'wstep': waveunit}, waveunit=waveunit)
def experimental_spectrum(w, I, wunit="nm", Iunit="counts", conditions={}, cond_units=None, name=None): # -> Spectrum: """Convert ``(w, I)`` into a :py:class:`~radis.spectrum.spectrum.Spectrum` object that has unit conversion and plotting capabilities. Convolution is not available as the spectrum is assumed to have be measured experimentally (hence it is already convolved with the slit function) Parameters ---------- w: np.array wavelength, or wavenumber I: np.array intensity wunit: ``'nm'``, ``'cm-1'``, ``'nm_vac'`` wavespace unit: wavelength in air (``'nm'``), wavenumber (``'cm-1'``), or wavelength in vacuum (``'nm_vac'``). Default ``'nm'``. Iunit: str intensity unit (can be 'counts', 'mW/cm2/sr/nm', etc...). Default 'counts' (default Winspec output) Other Parameters ---------------- conditions: dict (optional) calculation conditions to be stored with Spectrum cond_units: dict (optional) calculation conditions units name: str (optional) give a name Examples -------- Load and plot an experimental spectrum:: from numpy import loadtxt from radis import experimental_spectrum w, I = loadtxt('my_file.txt').T # assuming 2 columns s = experimental_spectrum(w, I, Iunit='mW/cm2/sr/nm') # creates 'radiance' s.plot() See Also -------- :func:`~radis.spectrum.models.calculated_spectrum`, :func:`~radis.spectrum.models.transmittance_spectrum`, :meth:`~radis.spectrum.spectrum.Spectrum.from_array`, :meth:`~radis.spectrum.spectrum.Spectrum.from_txt`, :func:`~radis.tools.database.load_spec` """ if np.shape(w) != np.shape(I): raise ValueError( "Wavelength {0} and intensity {1} do not have the same shape". format(np.shape(w), np.shape(I))) return Spectrum.from_array( np.array(w), np.array(I), "radiance", waveunit=wunit, unit=Iunit, conditions=conditions, cond_units=cond_units, name=name, )
def eq_bands(self, Tgas, mole_fraction=None, path_length=None, pressure=None, levels='all', drop_lines=True): ''' Return all vibrational bands as a list of spectra for a spectrum calculated under equilibrium. By default, drop_lines is set to True so line_survey cannot be done on spectra. See drop_lines for more information Parameters ---------- Tgas: float Gas temperature (K) mole_fraction: float database species mole fraction. If None, Factory mole fraction is used. path_length: float slab size (cm). If None, Factory mole fraction is used. pressure: float pressure (bar). If None, the default Factory pressure is used. Other Parameters ---------------- levels: ``'all'``, int, list of str calculate only bands that feature certain levels. If ``'all'``, all bands are returned. If N (int), return bands for the first N levels (sorted by energy). If list of str, return for all levels in the list. The remaining levels are also calculated and returned merged together in the ``'others'`` key. Default ``'all'`` drop_lines: boolean if False remove the line database from each bands. Helps save a lot of space, but line survey cannot be performed anymore. Default ``True``. Returns ------- bands: list of :class:`~radis.spectrum.spectrum.Spectrum` objects Use .get(something) to get something among ['radiance', 'radiance_noslit', 'absorbance', etc...] Or directly .plot(something) to plot it Notes ----- Process: - Calculate line strenghts correcting the CDSD reference one. - Then call the main routine that sums over all lines ''' try: # update defaults if path_length is not None: self.input.path_length = path_length if mole_fraction is not None: self.input.mole_fraction = mole_fraction if pressure is not None: self.input.pressure_mbar = pressure * 1e3 if not is_float(Tgas): raise ValueError( 'Tgas should be float. Use ParallelFactory for multiple cases' ) assert type(levels) in [str, list, int] if type(levels) == str: assert levels == 'all' # Temporary: if type(levels) == int: raise NotImplementedError # Get temperatures self.input.Tgas = Tgas self.input.Tvib = Tgas # just for info self.input.Trot = Tgas # just for info # Init variables pressure_mbar = self.input.pressure_mbar mole_fraction = self.input.mole_fraction path_length = self.input.path_length verbose = self.verbose # %% Retrieve from database if exists if self.autoretrievedatabase: s = self._retrieve_bands_from_database() if s is not None: return s # Print conditions if verbose: print('Calculating Equilibrium bands') self.print_conditions() # Start t0 = time() # %% Make sure database is loaded if self.df0 is None: raise AttributeError('Load databank first (.load_databank())') if not 'band' in self.df0: self._add_bands() # %% Calculate the spectrum # --------------------------------------------------- t0 = time() self._reinitialize() # -------------------------------------------------------------------- # First calculate the linestrength at given temperature self._calc_linestrength_eq(Tgas) self._cutoff_linestrength() # ---------------------------------------------------------------------- # Calculate line shift self._calc_lineshift() # ---------------------------------------------------------------------- # Line broadening # ... calculate broadening FWHM self._calc_broadening_FWHM() # ... find weak lines and calculate semi-continuum (optional) I_continuum = self._calculate_pseudo_continuum() if I_continuum: raise NotImplementedError( 'pseudo continuum not implemented for bands') # ... apply lineshape and get absorption coefficient # ... (this is the performance bottleneck) wavenumber, abscoeff_v_bands = self._calc_broadening_bands() # : : # cm-1 1/(#.cm-2) # # ... add semi-continuum (optional) # abscoeff_v_bands = self._add_pseudo_continuum(abscoeff_v_bands, I_continuum) # ---------------------------------------------------------------------- # Remove certain bands if levels != 'all': # Filter levels that feature the given energy levels. The rest # is stored in 'others' lines = self.df1 # We need levels to be explicitely stated for given molecule assert hasattr(lines, 'viblvl_u') assert hasattr(lines, 'viblvl_l') # Get bands to remove merge_bands = [] for band in abscoeff_v_bands: # note: could be vectorized with pandas str split. # TODO viblvl_l, viblvl_u = band.split('->') if not viblvl_l in levels and not viblvl_u in levels: merge_bands.append(band) # Remove bands from bandlist and add them to `others` abscoeff_others = np.zeros_like(wavenumber) for band in merge_bands: abscoeff = abscoeff_v_bands.pop(band) abscoeff_others += abscoeff abscoeff_v_bands['others'] = abscoeff_others if verbose: print('{0} bands grouped under `others`'.format( len(merge_bands))) # ---------------------------------------------------------------------- # Generate spectra # Progress bar for spectra generation Nbands = len(abscoeff_v_bands) if self.verbose: print('Generating bands ({0})'.format(Nbands)) pb = ProgressBar(Nbands, active=self.verbose) if Nbands < 100: pb.set_active(False) # hide for low line number # Generate spectra s_bands = {} for i, (band, abscoeff_v) in enumerate(abscoeff_v_bands.items()): # incorporate density of molecules (see equation (A.16) ) density = mole_fraction * ((pressure_mbar * 100) / (k_b * Tgas)) * 1e-6 # : # (#/cm3) abscoeff = abscoeff_v * density # cm-1 # ============================================================================== # Warning # --------- # if the code is extended to multi-species, then density has to be added # before lineshape broadening (as it would not be constant for all species) # ============================================================================== # get absorbance (technically it's the optical depth `tau`, # absorbance `A` being `A = tau/ln(10)` ) absorbance = abscoeff * path_length # Generate output quantities transmittance_noslit = exp(-absorbance) emissivity_noslit = 1 - transmittance_noslit radiance_noslit = calc_radiance( wavenumber, emissivity_noslit, Tgas, unit=self.units['radiance_noslit']) # ----------------------------- Export: lines = self.df1[self.df1.band == band] # if band == 'others': all lines will be None. # TODO populations = None # self._get_vib_populations(lines) # Store results in Spectrum class if drop_lines: lines = None if self.save_memory: try: del self.df1 # saves some memory except AttributeError: # already deleted pass conditions = self.get_conditions() # Add band name and hitran band name in conditions conditions.update({'band': band}) if lines: def add_attr(attr): if attr in lines: if band == 'others': val = 'N/A' else: # all have to be the same val = lines[attr].iloc[0] conditions.update({attr: val}) add_attr('band_htrn') add_attr('viblvl_l') add_attr('viblvl_u') s = Spectrum( quantities={ 'abscoeff': (wavenumber, abscoeff), 'absorbance': (wavenumber, absorbance), 'emissivity_noslit': (wavenumber, emissivity_noslit), 'transmittance_noslit': (wavenumber, transmittance_noslit), # (mW/cm2/sr/nm) 'radiance_noslit': (wavenumber, radiance_noslit), }, conditions=conditions, populations=populations, lines=lines, units=self.units, cond_units=self.cond_units, waveunit=self.params.waveunit, # cm-1 name=band, # dont check input (much faster, and Spectrum warnings=False, # is freshly baken so probably in a good format ) # # update database if asked so # if self.autoupdatedatabase: # self.SpecDatabase.add(s) # # Tvib=Trot=Tgas... but this way names in a database # # generated with eq_spectrum are consistent with names # # in one generated with non_eq_spectrum s_bands[band] = s pb.update(i) # progress bar pb.done() if verbose: print(('... process done in {0:.1f}s'.format(time() - t0))) return s_bands except: # An error occured: clean before crashing self._clean_temp_file() raise
def sPlanck( wavenum_min=None, wavenum_max=None, wavelength_min=None, wavelength_max=None, T=None, eps=1, wstep=0.01, medium="air", **kwargs ): r"""Return a RADIS :py:class:`~radis.spectrum.spectrum.Spectrum` object with blackbody radiation. It's easier to plug in a :py:func:`~radis.los.slabs.SerialSlabs` line-of-sight than the Planck radiance calculated by :py:func:`~radis.phys.blackbody.planck`. And you don't need to worry about units as they are handled internally. See :py:class:`~radis.spectrum.spectrum.Spectrum` documentation for more information Parameters ---------- wavenum_min / wavenum_max: ():math:`cm^{-1}`) minimum / maximum wavenumber to be processed in :math:`cm^{-1}`. wavelength_min / wavelength_max: (:math:`nm`) minimum / maximum wavelength to be processed in :math:`nm`. T: float (K) blackbody temperature eps: float [0-1] blackbody emissivity. Default ``1`` Other Parameters ---------------- wstep: float (cm-1 or nm) wavespace step for calculation **kwargs: other keyword inputs all are forwarded to spectrum conditions. For instance you can add a 'path_length=1' after all the other arguments Examples -------- Generate Earth blackbody:: s = sPlanck(wavelength_min=3000, wavelength_max=50000, T=288, eps=1) s.plot() Examples using sPlanck : .. minigallery:: radis.sPlanck References ---------- In wavelength: .. math:: \epsilon \frac{2h c^2}{{\lambda}^5} \frac{1}{\operatorname{exp}\left(\frac{h c}{\lambda k T}\right)-1} In wavenumber: .. math:: \epsilon 2h c^2 {\nu}^3 \frac{1}{\operatorname{exp}\left(\frac{h c \nu}{k T}\right)-1} See Also -------- :py:func:`~radis.phys.blackbody.planck`, :py:func:`~radis.phys.blackbody.planck_wn` """ from radis.spectrum.spectrum import Spectrum # Check inputs if (wavelength_min is not None or wavelength_max is not None) and ( wavenum_min is not None or wavenum_max is not None ): raise ValueError("You cannot give both wavelengths and wavenumbers") if wavenum_min is not None and wavenum_max is not None: assert wavenum_min < wavenum_max waveunit = "cm-1" else: assert wavelength_min < wavelength_max if medium == "air": waveunit = "nm" elif medium == "vacuum": waveunit = "nm_vac" else: raise ValueError(medium) if T is None: raise ValueError("T must be defined") if not (eps >= 0 and eps <= 1): raise ValueError("Emissivity must be in [0-1]") # Test range is correct: if waveunit == "cm-1": # generate the vector of wavenumbers (shape M) w = arange(wavenum_min, wavenum_max + wstep, wstep) Iunit = "mW/sr/cm2/cm-1" I = planck_wn(w, T, eps=eps, unit=Iunit) else: # generate the vector of lengths (shape M) w = arange(wavelength_min, wavelength_max + wstep, wstep) Iunit = "mW/sr/cm2/nm" if waveunit == "nm_vac": w_vac = w elif waveunit == "nm": w_vac = air2vacuum(w) # calculate planck with wavelengths in vacuum I = planck(w_vac, T, eps=eps, unit=Iunit) conditions = {"wstep": wstep} # add all extra parameters in conditions (ex: path_length) conditions.update(**kwargs) return Spectrum( quantities={ "radiance_noslit": (w, I), "transmittance_noslit": (w, zeros_like(w)), "absorbance": (w, ones_like(w) * inf), }, conditions=conditions, units={"radiance_noslit": Iunit, "transmittance_noslit": "", "absorbance": ""}, cond_units={"wstep": waveunit}, waveunit=waveunit, name="Planck {0}K, eps={1:.2g}".format(T, eps), )
def transmittance_spectrum(w, T, wunit="nm", Tunit="", conditions=None, cond_units=None, name=None): # -> Spectrum: """Convert ``(w, I)`` into a :py:class:`~radis.spectrum.spectrum.Spectrum` object that has unit conversion, plotting and slit convolution capabilities Parameters ---------- w: np.array wavelength, or wavenumber T: np.array transmittance (no slit) wunit: ``'nm'``, ``'cm-1'``, ``'nm_vac'`` wavespace unit: wavelength in air (``'nm'``), wavenumber (``'cm-1'``), or wavelength in vacuum (``'nm_vac'``). Default ``'nm'``. Iunit: str intensity unit. Default ``""`` (adimensionned) Other Parameters ---------------- conditions: dict (optional) calculation conditions to be stored with Spectrum cond_units: dict (optional) calculation conditions units name: str (optional) give a name Examples -------- :: # w, T are numpy arrays for wavelength and transmittance from radis import transmittance_spectrum s2 = transmittance_spectrum(w, T, wunit='nm') # creates 'transmittance_noslit' See Also -------- :func:`~radis.spectrum.models.calculated_spectrum`, :func:`~radis.spectrum.models.experimental_spectrum`, :meth:`~radis.spectrum.spectrum.Spectrum.from_array`, :meth:`~radis.spectrum.spectrum.Spectrum.from_txt`, :func:`~radis.tools.database.load_spec` """ return Spectrum.from_array( np.array(w), np.array(T), "transmittance_noslit", waveunit=wunit, unit=Tunit, conditions=conditions, cond_units=cond_units, name=name, )
def add_spectra(s1, s2, var=None, force=False): """Return a new spectrum with ``s2`` added to ``s1``. Equivalent to:: s1 + s2 .. warning:: we are just algebrically adding the quantities. If you want to merge spectra while preserving the radiative transfer equation, see :func:`~radis.los.slabs.MergeSlabs` and :func:`~radis.los.slabs.SerialSlabs` Parameters ---------- s1, s2: Spectrum objects Spectrum you want to substract var: str quantity to manipulate: 'radiance', 'transmittance', ... If ``None``, get the unique spectral quantity of ``s1``, or the unique spectral quantity of ``s2``, or raises an error if there is any ambiguity Returns ------- s: Spectrum Spectrum object with the same units and waveunits as ``s1`` See Also -------- :func:`~radis.los.slabs.MergeSlabs`, :func:`~radis.spectrum.operations.substract_spectra` """ # Get variable if var is None: try: var = _get_unique_var( s2, var, inplace=False) # unique variable of 2nd spectrum except KeyError: var = _get_unique_var( s1, var, inplace=False ) # if doesnt exist, unique variable of 1st spectrum # if it fails, let it fail # Make sure it is in both Spectra if var not in s1.get_vars(): raise KeyError("Variable {0} not in Spectrum {1}".format( var, s1.get_name())) if var not in s2.get_vars(): raise KeyError("Variable {0} not in Spectrum {1}".format( var, s1.get_name())) if var in ["transmittance_noslit", "transmittance"] and not force: raise ValueError( "It does not make much physical sense to sum transmittances. Are " + "you sure of what you are doing? See also `//` (MergeSlabs), `>` " + "(SerialSlabs) and `concat_spectra`. If you're sure, use `force=True`" ) # Get s1 units Iunit1 = s1.units[var] wunit1 = s1.get_waveunit() # Resample s2 on s1 s2 = s2.resample(s1, inplace=False) # Add, change output unit if needed. w1, I1 = s1.get(var=var, Iunit=Iunit1, wunit=wunit1) w2, I2 = s2.get(var=var, Iunit=Iunit1, wunit=wunit1) name = s1.get_name() + "+" + s2.get_name() sub = Spectrum.from_array(w1, I1 + I2, var, waveunit=wunit1, unit=Iunit1, name=name) # warn("Conditions of the left spectrum were copied in the substraction.", Warning) return sub
def calculated_spectrum( w, I, wunit="nm", Iunit="mW/cm2/sr/nm", conditions=None, cond_units=None, populations=None, name=None, ): # -> Spectrum: """Convert ``(w, I)`` into a :py:class:`~radis.spectrum.spectrum.Spectrum` object that has unit conversion, plotting and slit convolution capabilities Parameters ---------- w: np.array wavelength, or wavenumber I: np.array intensity (no slit) wunit: ``'nm'``, ``'cm-1'``, ``'nm_vac'`` wavespace unit: wavelength in air (``'nm'``), wavenumber (``'cm-1'``), or wavelength in vacuum (``'nm_vac'``). Default ``'nm'``. Iunit: str intensity unit (can be 'counts', 'mW/cm2/sr/nm', etc...). Default 'mW/cm2/sr/nm' (note that non-convoluted Specair spectra are in 'mW/cm2/sr/µm') Other Parameters ---------------- conditions: dict (optional) calculation conditions to be stored with Spectrum. Default ``None`` cond_units: dict (optional) calculation conditions units. Default ``None`` populations: dict populations to be stored in Spectrum. Default ``None`` name: str (optional) give a name Examples -------- :: # w, I are numpy arrays for wavelength and radiance from radis import calculated_spectrum s = calculated_spectrum(w, I, wunit='nm', Iunit='W/cm2/sr/nm') # creates 'radiance_noslit' See Also -------- :func:`~radis.spectrum.models.transmittance_spectrum`, :func:`~radis.spectrum.models.experimental_spectrum`, :meth:`~radis.spectrum.spectrum.Spectrum.from_array`, :meth:`~radis.spectrum.spectrum.Spectrum.from_txt`, :func:`~radis.tools.database.load_spec` """ return Spectrum.from_array( np.array(w), np.array(I), "radiance_noslit", waveunit=wunit, unit=Iunit, conditions=conditions, cond_units=cond_units, populations=populations, name=name, )
def test_slit_unit_conversions_spectrum_in_nm( 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.spectrum.spectrum import Spectrum from radis.test.utils import getTestFile if plot: # dont get stuck with Matplotlib if executing through pytest plt.ion() if close_plots: plt.close("all") # %% Get a Spectrum (stored in nm) s_nm = Spectrum.from_txt( getTestFile("calc_N2C_spectrum_Trot1200_Tvib3000.txt"), quantity="radiance_noslit", waveunit="nm", unit="mW/cm2/sr/µm", conditions={"self_absorption": False}, ) with catch_warnings(): filterwarnings( "ignore", "Condition missing to know if spectrum is at equilibrium:" ) # just because it makes better units s_nm.rescale_path_length(1, 0.001) wstep = np.diff(s_nm.get_wavelength())[0] assert s_nm.get_waveunit() == "nm" # ensures it's stored in cm-1 for shape in ["gaussian", "triangular"]: # Apply slit in nm slit_nm = 0.5 s_nm.name = "Spec in nm, slit {0:.2f} nm".format(slit_nm) s_nm.apply_slit(slit_nm, unit="nm", 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_nm.get_slit()) assert np.isclose(slit_nm, fwhm, atol=2 * wstep) # Apply slit in nm this time s_cm = s_nm.copy() w_nm = s_nm.get_wavelength(which="non_convoluted") slit_cm = dnm2dcm(slit_nm, w_nm[len(w_nm) // 2]) s_cm.name = "Spec in nm, slit {0:.2f} cm-1".format(slit_cm) s_cm.apply_slit(slit_cm, unit="cm-1", shape=shape, mode="same") plotargs = {} if plot: plotargs["title"] = "test_slit_unit_conversions: {0} ({1} nm)".format( shape, slit_nm ) s_nm.compare_with( s_cm, spectra_only="radiance", rtol=1e-3, verbose=verbose, plot=plot, **plotargs )