def expand_metadata(df, metadata): """Turn metadata from a float to a column For some reason metadata are sometimes not copied when a DataFrame is sliced or copied, even if they explicitely figure in the df._metadata attribute. Here we add them as column before such operations. Parameters ---------- df: pandas DataFrame ... Returns ------- None: df modified in place """ for k in metadata: if __debug__ and k not in df._metadata: from radis.misc.debug import printdbg printdbg("WARNING. {0} not in _metadata: {1}".format( k, df._metadata)) df[k] = getattr(df, k)
def transfer_metadata(df1, df2, metadata): """Transfer metadata between a DataFrame df1 and df2 For some reason metadata are sometimes not copied when a DataFrame is sliced or copied, even if they explicitely figure in the df._metadata attribute. Here we copy them back Parameters ---------- df1: pandas DataFrame copy from df1 df2: pandas DataFrame copy to df2 """ for k in metadata: if __debug__ and k not in df1._metadata: from radis.misc.debug import printdbg printdbg("WARNING. {0} not in _metadata: {1}".format( k, df1._metadata)) if not hasattr(df2, k): assert k not in df1.columns # point is not to copy columns! setattr(df2, k, getattr(df1, k))
def resample( xspace, vector, xspace_new, k=1, ext="error", energy_threshold=1e-3, print_conservation=True, ): """Resample (xspace, vector) on a new space (xspace_new) of evenly distributed data and whose bounds are taken as the same as `xspace`. Uses spline interpolation to create the intermediary points. Number of points is the same as the initial xspace, times a resolution factor. Verifies energy conservation on the intersecting range at the end. Parameters ---------- xspace: array space on which vector was generated vector: array quantity to resample resfactor: array xspace vector to resample on k: int order of spline interpolation. 3: cubic, 1: linear. Default 1. ext: 'error', 'extrapolate', 0, 1 Controls the value returned for elements of xspace_new not in the interval defined by xspace. If 'error', raise a ValueError. If 'extrapolate', well, extrapolate. If '0' or 0, then fill with 0. If 1, fills with 1. Default 'error'. energy_threshold: float if energy conservation (integrals on the intersecting range) is above this threshold, raise an error. If None, dont check for energy conservation Default 1e-3 (0.1%) print_conservation: boolean if True, prints energy conservation Returns ------- vector_new: array resampled vector on evenly spaced array. Number of element is conserved. Note that depending upon the from_space > to_space operation, sorting may be reversed. Examples -------- Resample a :class:`~radis.spectrum.spectrum.Spectrum` radiance on an evenly spaced wavenumber space:: w_nm, I_nm = s.get('radiance') w_cm, I_cm = resample_even(nm2cm(w_nm), I_nm) """ if len(xspace) != len(vector): raise ValueError( "vector and xspace should have the same length. " + "Got {0}, {1}".format(len(vector), len(xspace)) ) # Check reversed (interpolation requires objects are sorted) if is_sorted(xspace): reverse = False elif is_sorted_backward(xspace): reverse = True else: raise ValueError("Resampling requires wavespace to be sorted. It is not!") if reverse: xspace = xspace[::-1] xspace_new = xspace_new[::-1] vector = vector[::-1] # translate ext in FITPACK syntax for splev if ext == "extrapolate": ext_fitpack = 0 # splev returns extrapolated value elif ext in [0, "0", 1, "1", nan, "nan"]: ext_fitpack = 1 # splev returns 0 (fixed in post-processing) elif ext == "error": ext_fitpack = 2 # splev raises ValueError else: raise ValueError("Unexpected value for `ext`: {0}".format(ext)) if isnan(vector).sum() > 0: raise ValueError( "Resampled vector has {0} nans. Interpolation will fail".format( isnan(vector).sum() ) ) # Resample the slit function on the spectrum grid try: tck = splrep(xspace, vector, k=k) except ValueError: # Probably error on input data. Print it before crashing. print("\nValueError - Input data below:") print("-" * 5) print(xspace) print(vector) print("Check plot 101 too") import matplotlib.pyplot as plt plt.figure(101).clear() plt.plot(xspace, vector) plt.xlabel("xspace") plt.ylabel("vector") plt.title("ValueError") raise vector_new = splev(xspace_new, tck, ext=ext_fitpack) # ... get masks b = (xspace >= xspace_new.min()) * (xspace <= xspace_new.max()) b_new = (xspace_new >= xspace.min()) * (xspace_new <= xspace.max()) # fix filling for out of boundary values if ext in [1, "1"]: vector_new[~b_new] = 1 if __debug__: printdbg( "Filling with 1 on w<{0}, w>{1} ({2} points)".format( xspace.min(), xspace.max(), (1 - b_new).sum() ) ) elif ext in [nan, "nan"]: vector_new[~b_new] = nan if __debug__: printdbg( "Filling with nans on w<{0}, w>{1} ({2} points)".format( xspace.min(), xspace.max(), (1 - b_new).sum() ) ) # Check energy conservation: # ... calculate energy energy0 = abs(trapz(vector[b], x=xspace[b])) energy_new = abs(trapz(vector_new[b_new], x=xspace_new[b_new])) if energy_new == 0: # deal with particular case of energy = 0 if energy0 == 0: energy_ratio = 1 else: energy_ratio = 0 else: # general case energy_ratio = energy0 / energy_new if energy_threshold: if abs(energy_ratio - 1) > energy_threshold: import matplotlib.pyplot as plt plt.figure(101).clear() plt.plot(xspace, vector, "-ok", label="original") plt.plot(xspace_new, vector_new, "-or", label="resampled") plt.xlabel("xspace") plt.ylabel("vector") plt.legend() raise ValueError( "Error in resampling: " + "energy conservation ({0:.5g}%) below tolerance level ({1:.5g}%)".format( (1 - energy_ratio) * 100, energy_threshold * 100 ) + ". Check graph 101. " + "Increasing energy_threshold is possible but not recommended" ) if print_conservation: print("Resampling - Energy conservation: {0:.5g}%".format(energy_ratio * 100)) # Reverse again if reverse: # xspace_new = xspace_new[::-1] vector_new = vector_new[::-1] return vector_new
def get(self, conditions='', **kwconditions): ''' Returns a list of spectra that match given conditions Parameters ---------- database: list of Spectrum objects the database conditions: str a list of conditions >>> get('Tvib==3000 & Trot==1500') kwconditions: dict an unfolded dict of conditions >>> get(Tvib=3000, Trot=1500) Other Parameters ---------------- inplace: boolean if True, return the actual object in the database. Else, return copies. Default False Examples -------- >>> spec_list = db.get('Tvib==3000 & Trot==1300') or >>> spec_list = db.get(Tvib=3000, Trot=1300) See Also -------- :meth:`~radis.tools.database.SpecDatabase.get_unique` :meth:`~radis.tools.database.SpecDatabase.get_closest` :meth:`~radis.tools.database.SpecDatabase.items` ''' # Test inputs for (k, _) in kwconditions.items(): if not k in self.df.columns: raise ValueError( '{0} not a correct condition name. Use one of: {1}'.format( k, self.df.columns)) if len(self.df) == 0: warn('Empty database') return [] inplace = kwconditions.pop('inplace', False) # type: bool, default False # Unique condition method if conditions != '' and kwconditions != {}: raise ValueError( "Please choose one of the two input format (str or dict) exclusively" ) if conditions == '' and kwconditions == {}: return list(self.df['Spectrum']) # Find Spectrum that match conditions if conditions != '': # ... with input conditions query directly dg = self.df.query(conditions) else: # ... first write input conditions query query = [] for (k, v) in kwconditions.items(): if isinstance(v, string_types): query.append("{0} == '{1}'".format(k, v)) else: # query.append('{0} == {1}'.format(k,v)) query.append('{0} == {1}'.format(k, v.__repr__())) # ... for som reason {1}.format() would remove some digit # ... to floats in Python2. Calling .__repr__() keeps # ... the correct format, and has no other consequences as far # ... as I can tell # There is a limitation in numpy: a max of 32 arguments is required. # Below we write a workaround when the Spectrum has more than 32 conditions if len(query) < 32: query = ' & '.join(query) if __debug__: printdbg('Database query: {0}'.format(query)) dg = self.df.query(query) else: # cut in <32-long parts N = len(query) // 32 + 1 querypart = ' & '.join(query[::N]) dg = self.df.query(querypart) for i in range(1, N + 1): querypart = ' & '.join(query[i::N]) if __debug__: printdbg('Database query: {0}'.format(querypart)) dg = dg.query(querypart) out = list(dg['Spectrum']) if not inplace: out = [s.copy() for s in out] return out
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 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 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