def get_label_cdsd_hitran(row, details): label = "CO2[iso{iso}] [{branch}{jl:.0f}]({v1l:.0f}{v2l:.0f}`{l2l:.0f}`{v3l:.0f})->({v1u:.0f}{v2u:.0f}`{l2u:.0f}`{v3u:.0f})".format( **dict( [ (k, row[k]) for k in [ "v1u", "v2u", "l2u", "v3u", "v1l", "v2l", "l2l", "v3l", "jl", "iso", ] ] + [("branch", _fix_branch_format[row["branch"]])] ) ) for k in details: name, _, unit = details[k] if is_float(row[k]): label += "<br>{0} {1}: {2:.3g} {3}".format(k, name, row[k], unit) else: label += "<br>{0} {1}: {2} {3}".format(name, k, row[k], unit) return label
def get_label_cdsd(row, details): label = "CO2[iso{iso}] [{branch}{jl:.0f}](p{polyl:.0f}c{wangl:.0f}n{rankl:.0f})->(p{polyu:.0f}c{wangu:.0f}n{ranku:.0f})".format( **dict( [ (k, row[k]) for k in [ "polyl", "wangl", "rankl", "polyu", "wangu", "ranku", "jl", "iso", ] ] + [("branch", _fix_branch_format[row["branch"]])] ) ) for k in details: name, _, unit = details[k] if is_float(row[k]): label += "<br>{0} {1}: {2:.3g} {3}".format(k, name, row[k], unit) else: label += "<br>{0} {1}: {2} {3}".format(name, k, row[k], unit) return label
def get_label_hitran_locglob(row, details): ''' Todo ------- replace with simple astype(str) statements and str operations ex: > '['+df[locl].astype(str)+']('+df[globl].astype(str)+'->'+ > df[globu].astype(str)'+)' will be much faster! ''' molecule = get_molecule(row.id) label = ('{0}[iso{1}]'.format(molecule, row['iso']) + '[{locl}]({globl})->({globu})'.format( **dict([(k, row[k]) for k in ['locl', 'globl', 'globu']]))) for k in details: name, _, unit = details[k] if is_float(row[k]): label += '\n{0} {1}: {2:.3g} {3}'.format(k, name, row[k], unit) else: label += '\n{0} {1}: {2} {3}'.format(k, name, row[k], unit) return label
def _h5_compatible(a_dict): """ Make dictionary ``a_dict`` h5 compatible """ out = {} for k, v in a_dict.items(): if v is None: continue # dont store None elif is_float(v): out[k] = v else: out[k] = str(v) # convert to str return out
def get_label_hitran(row, details): ''' Todo ------- replace with simple astype(str) statements and str operations ex: > '['+df[locl].astype(str)+']('+df[globl].astype(str)+'->'+ > df[globu].astype(str)'+)' will be much faster! ''' molecule = get_molecule(row.id) # Get global labels if molecule in HITRAN_CLASS1: label = ( '{molec}[iso{iso:.0f}] [{branch}{jl:.0f}]({vl:.0f})->({vu:.0f})' .format( **dict([(k, row[k]) for k in ['vu', 'vl', 'jl', 'iso']] + [('molec', molecule), ('branch', _fix_branch_format[row['branch']])]))) elif molecule in HITRAN_CLASS4: label = ( '{molec}[iso{iso:.0f}] [{branch}{jl:.0f}]({v1l:.0f}{v2l:.0f}`{l2l:.0f}`{v3l:.0f})->({v1u:.0f}{v2u:.0f}`{l2u:.0f}`{v3u:.0f})' .format(**dict([(k, row[k]) for k in [ 'v1u', 'v2u', 'l2u', 'v3u', 'v1l', 'v2l', 'l2l', 'v3l', 'jl', 'iso' ]] + [('molec', molecule), ('branch', _fix_branch_format[row['branch']])]))) elif molecule in HITRAN_CLASS5: label = ( '{molec}[iso{iso:.0f}] [{branch}{jl:.0f}]({v1l:.0f}{v2l:.0f}`{l2l:.0f}`{v3l:.0f} {rl:.0f})->({v1u:.0f}{v2u:.0f}`{l2u:.0f}`{v3u:.0f} {ru:.0f})' .format(**dict([(k, row[k]) for k in [ 'v1u', 'v2u', 'l2u', 'v3u', 'v1l', 'v2l', 'l2l', 'v3l', 'rl', 'ru', 'jl', 'iso' ]] + [('molec', molecule), ('branch', _fix_branch_format[row['branch']])]))) else: raise NotImplementedError( 'No label for {0}. Please add it!'.format(molecule)) # Add details about some line properties for k in details: name, _, unit = details[k] if is_float(row[k]): label += '\n{0} {1}: {2:.3g} {3}'.format(k, name, row[k], unit) else: label += '\n{0} {1}: {2} {3}'.format(k, name, row[k], unit) return label
def get_label_cdsd(row, details): label = ('CO2[iso{iso}] [{branch}{jl:.0f}]({v1l:.0f}{v2l:.0f}`{l2l:.0f}`{v3l:.0f})->({v1u:.0f}{v2u:.0f}`{l2u:.0f}`{v3u:.0f})'.format( **dict([(k, row[k]) for k in ['v1u', 'v2u', 'l2u', 'v3u', 'v1l', 'v2l', 'l2l', 'v3l', 'jl', 'iso']]+ [('branch',_fix_branch_format[row['branch']])]))) for k in details: name, _, unit = details[k] if is_float(row[k]): label += '\n{0} {1}: {2:.3g} {3}'.format(k, name, row[k], unit) else: label += '\n{0} {1}: {2} {3}'.format(name, k, row[k], unit) return label
def get_label_hitran_branchjv(row, details): molecule = get_molecule(row.id) label = ('{0}[iso{1}]'.format(molecule, row['iso']) + '[{branch}{jl}]({v1l})->({v1u})'.format( **dict([(k, row[k]) for k in ['branch', 'jl', 'v1l', 'v1u']]))) for k in details: name, _, unit = details[k] if is_float(row[k]): label += '\n{0} {1}: {2:.3g} {3}'.format(k, name, row[k], unit) else: label += '\n{0} {1}: {2} {3}'.format(k, name, row[k], unit) return label
def _format_input(self, **kwargs): """ Format the input of parallel calculation request. Returns lists of same lengths that can be parsed with zip(). User input can be lists, or floats instead of constant-values list """ N = None # list length if there are list involved kwout = {} for k, v in kwargs.items(): if is_list(v): if N is None: N = len(v) else: if len(v) != N: raise ValueError( "Invalid input for {0}: ".format(k) + "all list should have the same length" "(you may use floats too)" ) kwout[k] = v # Now let's turn floats into list of length N if N is None: warn("Using ParallelFactory for single case") N = 1 for k, v in kwargs.items(): if type(v) in [int, np.int64]: v = float(v) # int is not serializable (for some reason) if is_list(v): continue # already done elif is_float(v) or v is None: kwout[k] = [v] * N else: raise ValueError( "Invalid input for {0}: ".format(k) + "input should be list-like or float-like" + "({0} instead)".format(type(v)) ) # check output is correct: for v in kwout.values(): assert len(v) == N return kwout
def _calc_vib_populations(self, Tvib, vib_distribution="boltzmann", overpopulation=None): """Calculate vibrational populations from Tref to new Tvib and store results in vib_levels dataframe This does not modify the spectra yet! By constructions populations are calculated divided by the state degeneracy, i.e, g = gv * (2J+1) * gi * gs This means we should only use ratios for rescaling The information on state degeneracy and isotopic abundance is already included in the linestrength / emission integral, hence in the pre-calculated emisscoeff / abscoeff """ vib_levels = self.vib_levels if overpopulation is None: overpopulation = {} if is_float(Tvib): # Get new population E_vib = vib_levels["Evib"] g = 1 # explicitely calculate populations divided by degeneracy # this means we should only use ratios for rescaling if vib_distribution == "boltzmann": nvibQvib = g * exp(-hc_k * E_vib / Tvib) else: raise NotImplementedError( "vib_distribution: {0}".format(vib_distribution)) else: Tvib1, Tvib2, Tvib3 = Tvib # Get new population E_vib1 = vib_levels["Evib1"] E_vib2 = vib_levels["Evib2"] E_vib3 = vib_levels["Evib3"] g = 1 # explicitely calculate populations divided by degeneracy # this means we should only use ratios for rescaling if vib_distribution == "boltzmann": nvibQvib = (g * exp(-hc_k * E_vib1 / Tvib1) * exp(-hc_k * E_vib2 / Tvib2) * exp(-hc_k * E_vib3 / Tvib3)) else: raise NotImplementedError( "vib_distribution: {0}".format(vib_distribution)) # Add overpopulation if overpopulation != {}: for k, ov in overpopulation.items(): nvibQvib.loc[k] *= ov # TODO: test warn( "NotImplemented: partition function overpopulation correction not tested" ) # Normalize with partition function Qvib = nvibQvib.sum() nvib = nvibQvib / Qvib # update dataframe vib_levels["nvib"] = nvib vib_levels["Qvib"] = Qvib return Qvib
def _get_fout_name(path, if_exists_then, add_date, add_info, sjson, verbose): ''' Get final output name (add info, extension, increment number if needed) ''' conditions = sjson['conditions'] if isdir(path): fold, name = path, '' else: fold, name = split(path) # ... add date info if add_date not in ['', None, False]: date = strftime(add_date) else: date = '' # ... add conditions info if add_info not in [[], {}, None, False]: # complete name with info about calculation conditions info = [] for k in add_info: if k in conditions: v = conditions[k] # Format info # ... special cases if k in ['Tvib', 'Tgas', 'Trot']: vs = "{0:.0f}".format(v) # ... general case elif is_float(v): vs = "{0:.3g}".format(v) else: vs = "{0}".format(v) try: un = sjson['cond_units'][k] except KeyError: # units not defined, or no units for this condition un = '' info.append("{0}{1}{2}".format(k, vs, un)) # Note: should test for filename validity here. # See https://stackoverflow.com/questions/9532499/check-whether-a-path-is-valid-in-python-without-creating-a-file-at-the-paths-ta # but it looks long. Best is probably to just test write a file else: if verbose: print(('Warning. {0} not a valid condition'.format(k))) info = '_'.join([_f for _f in info if _f]) else: info = '' # ... clean from forbidden characters for c in [r'/']: info = info.replace(c, '') # ... get extension rad, ext = splitext(name) if ext == '': ext = '.spec' # default extension # ... Write full name name = '_'.join([_f for _f in [date, rad, info] if _f]) + ext fout = join(fold, name) # ... Test for existence, replace if needed if exists(fout): if if_exists_then == 'increment': if verbose: print('Warning. File already exists. Filename is incremented') i = 0 while exists(fout): i += 1 name = '_'.join( [_f for _f in [date, rad, info, str(i)] if _f]) + ext fout = join(fold, name) elif if_exists_then == 'replace': if verbose: print(('File exists and will be replaced: {0}'.format(name))) else: raise ValueError('File already exists {0}. Choose another filename'.format(fout)+\ ', or set the `if_exists_then` option to `replace` or ìncrement`') return fout
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 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 get_label_hitran(row, details): """ Todo ------- replace with simple astype(str) statements and str operations ex: > '['+df[locl].astype(str)+']('+df[globl].astype(str)+'->'+ > df[globu].astype(str)'+)' will be much faster! """ molecule = get_molecule(row.id) # Get global labels if molecule in HITRAN_CLASS1: label = ( "{molec}[iso{iso:.0f}] [{branch}{jl:.0f}]({vl:.0f})->({vu:.0f})" .format( **dict([(k, row[k]) for k in ["vu", "vl", "jl", "iso"]] + [ ("molec", molecule), ("branch", _fix_branch_format[row["branch"]]), ]))) elif molecule in HITRAN_CLASS4: label = "{molec}[iso{iso:.0f}] [{branch}{jl:.0f}]({v1l:.0f}{v2l:.0f}`{l2l:.0f}`{v3l:.0f})->({v1u:.0f}{v2u:.0f}`{l2u:.0f}`{v3u:.0f})".format( **dict([(k, row[k]) for k in [ "v1u", "v2u", "l2u", "v3u", "v1l", "v2l", "l2l", "v3l", "jl", "iso", ]] + [ ("molec", molecule), ("branch", _fix_branch_format[row["branch"]]), ])) elif molecule in HITRAN_CLASS5: label = "{molec}[iso{iso:.0f}] [{branch}{jl:.0f}]({v1l:.0f}{v2l:.0f}`{l2l:.0f}`{v3l:.0f} {rl:.0f})->({v1u:.0f}{v2u:.0f}`{l2u:.0f}`{v3u:.0f} {ru:.0f})".format( **dict([(k, row[k]) for k in [ "v1u", "v2u", "l2u", "v3u", "v1l", "v2l", "l2l", "v3l", "rl", "ru", "jl", "iso", ]] + [ ("molec", molecule), ("branch", _fix_branch_format[row["branch"]]), ])) else: raise NotImplementedError( "No label for {0}. Please add it!".format(molecule)) # Add details about some line properties for k in details: name, _, unit = details[k] if is_float(row[k]): label += "<br>{0} {1}: {2:.3g} {3}".format( k, name, row[k], unit) else: label += "<br>{0} {1}: {2} {3}".format(k, name, row[k], unit) return label
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