def _broaden_lines_bands(self, df): ''' Divide over chuncks not to process to many lines in memory at the same time (note that this is not where the parallelisation is done: all lines are processed on the same core) Band specific version: returns a list of all broadened vibrational bands : Implementation ----------- note: there is no more splitting over line chuncks of given different (NotImplemented). This may result in large arrays and MemoryErrors for extreme spectral ranges. If that ever happens we may have to insert a chunck splitting loop in the band groupby loop See _calc_lineshape for more information ''' # Reactivate one-time warnings for new run reset_warnings(self.warnings) # -------------------------- gb = df.groupby('band') abscoeff_bands = {} pb = ProgressBar(len(gb), active=self.verbose) for i, (band, dg) in enumerate(gb): line_profile = self._calc_lineshape(dg) (wavenumber, absorption) = self._apply_lineshape(dg.S, line_profile, dg.shiftwav) abscoeff_bands[band] = absorption pb.update(i) pb.done() return wavenumber, abscoeff_bands
def _broaden_lines_noneq_bands(self, df): ''' Divide over chuncks not to process to many lines in memory at the same time (note that this is not where the parallelisation is done: all lines are processed on the same core) Band specific version: returns a list of all broadened vibrational bands Implementation ----------- note: there is no more splitting over line chuncks of given different (NotImplemented). This may result in large arrays and MemoryErrors for extreme spectral ranges. If that ever happens we may have to insert a chunck splitting loop in the band groupby loop See _calc_lineshape for more information ''' # Reactivate one-time warnings for new run reset_warnings(self.warnings) # -------------------------- abscoeff_bands = {} emisscoeff_bands = {} gb = df.groupby('band') chunksize = self.misc.chunksize # used for DLM keyword in 0.9.20 until proper implementation is done pb = ProgressBar(len(gb), active=self.verbose) for i, (band, dg) in enumerate(gb): if chunksize == 'DLM': line_profile_DLM, wL, wG, wL_dat, wG_dat = self._calc_lineshape_DLM( dg) (wavenumber, absorption) = self._apply_lineshape_DLM( dg.S.values, line_profile_DLM, dg.shiftwav.values, wL, wG, wL_dat, wG_dat) (_, emission) = self._apply_lineshape_DLM( dg.Ei.values, line_profile_DLM, dg.shiftwav.values, wL, wG, wL_dat, wG_dat) else: line_profile = self._calc_lineshape(dg) (wavenumber, absorption) = self._apply_lineshape(dg.S.values, line_profile, dg.shiftwav.values) (_, emission) = self._apply_lineshape(dg.Ei.values, line_profile, dg.shiftwav.values) abscoeff_bands[band] = absorption # emisscoeff_bands[band] = emission pb.update(i) pb.done() return wavenumber, abscoeff_bands, emisscoeff_bands
def test_progress_bar(*args, **kwargs): ''' Minimal example of a progress bar ''' from radis.misc.progress_bar import ProgressBar from time import sleep from numpy.random import rand print('Testing progress bar') a = 0 r = list(range(1000)) N = len(r) pb = ProgressBar(N) for i in r: pb.update(i, modulo=10) a += i sleep(rand() * 3e-3) pb.done() return True # nothing implemented
def fetch_hitemp( molecule, local_databases="~/.radisdb/", databank_name="HITEMP-{molecule}", isotope=None, load_wavenum_min=None, load_wavenum_max=None, cache=True, verbose=True, chunksize=100000, clean_cache_files=True, ): """Stream HITEMP file from HITRAN website. Unzip and build a HDF5 file directly. Returns a Pandas DataFrame containing all lines. Parameters ---------- molecule: `"CO2", "N2O", "CO", "CH4", "NO", "NO2", "OH"` HITEMP molecule. See :py:attr:`~radis.io.hitemp.HITEMP_SOURCE_FILES` local_databases: str where to create the RADIS HDF5 files. Default ``"~/.radisdb/"`` databank_name: str name of the databank in RADIS :ref:`Configuration file <label_lbl_config_file>` Default ``"HITEMP-{molecule}"`` isotope: str load only certain isotopes : ``'2'``, ``'1,2'``, etc. If ``None``, loads everything. Default ``None``. load_wavenum_min, load_wavenum_max: float (cm-1) load only specific wavenumbers. Other Parameters ---------------- cache: bool, or ``'regen'`` if ``True``, use existing HDF5 file. If ``False`` or ``'regen'``, rebuild it. verbose: bool chunksize: int number of lines to process at a same time. Higher is usually faster but can create Memory problems and keep the user uninformed of the progress. clean_cache_files: bool if ``True`` clean downloaded cache files after HDF5 are created. Returns ------- df: pd.DataFrame Line list A HDF5 file is also created in ``local_databases`` and referenced in the :ref:`RADIS config file <label_lbl_config_file>` with name ``databank_name`` Notes ----- if using ``load_only_wavenum_above/below`` or ``isotope``, the whole database is anyway downloaded and uncompressed to ``local_databases`` fast access .HDF5 files (which will take a long time on first call). Only the expected wavenumber range & isotopes are returned. The .HFD5 parsing uses :py:func:`~radis.io.hdf5.hdf2df` See Also -------- :py:func:`~radis.io.hdf5.hdf2df` """ # TODO ? : unzip only parts of the database # see https://github.com/radis/radis/pull/194 if databank_name == "HITEMP-{molecule}": databank_name = databank_name.format(**{"molecule": molecule}) local_databases = abspath(local_databases.replace("~", expanduser("~"))) if molecule in ["H2O", "CO2"]: raise NotImplementedError( "Automatic HITEMP download not implemented for {0} : multiple files. Download manually on https://hitran.org/hitemp/ " .format(molecule)) try: inputf = HITEMP_SOURCE_FILES[molecule] except KeyError as err: raise KeyError( f"Please choose one of HITEMP molecules : {list(HITEMP_SOURCE_FILES.keys())}. Got '{molecule}'" ) from err urlname = BASE_URL + inputf try: os.mkdir(local_databases) except OSError: pass else: if verbose: print("Created folder :", local_databases) output = abspath( join(local_databases, molecule + "-" + inputf.replace(".par.bz2", ".h5"))) if not cache or cache == "regen": # Delete existing HDF5 file if exists(output): if verbose: print("Removing existing file ", output) # TODO: also clean the getDatabankList? Todo once it is in JSON format. https://github.com/radis/radis/issues/167 os.remove(output) if exists(output): # check metadata : check_not_deprecated( output, metadata_is={}, metadata_keys_contain=["wavenumber_min", "wavenumber_max"], ) # check database is registered in ~/.radis if not databank_name in getDatabankList(): # if not, check number of rows is correct : error_msg = "" with pd.HDFStore(output, "r") as store: nrows = store.get_storer("df").nrows if nrows != INFO_HITEMP_LINE_COUNT[molecule]: error_msg += ( f"\nNumber of lines in local database ({nrows:,}) " + "differ from the expected number of lines for " + f"HITEMP {molecule}: {INFO_HITEMP_LINE_COUNT[molecule]}" ) file_metadata = store.get_storer("df").attrs.metadata for k in [ "wavenumber_min", "wavenumber_max", "download_url", "download_date", ]: if k not in file_metadata: error_msg += ( "\nMissing key in file metadata to register the database " + f"automatically : {k}") if error_msg: raise ValueError( f"{databank_name} not declared in your RADIS ~/.config file although " + f"{output} exists. {error_msg}\n" + "If you know this file, add it to ~/.radisdb manually. " + "Else regenerate the database with:\n\t" + ">>> radis.SpectrumFactory().fetch_databank(..., use_cached='regen')" + "\nor\n\t" + ">>> radis.io.hitemp.fetch_hitemp({molecule}, cache='regen')" + "\n\n⚠️ It will re-download & uncompress the whole database " + "from HITEMP.\n\nList of declared databanks: {getDatabankList()}.\n" + f"{output} metadata: {file_metadata}") # Else database looks ok : register it if verbose: print( f"{databank_name} not declared in your RADIS ~/.config file although " + f"{output} exists. Registering the database automatically." ) register_database( databank_name, [output], molecule=molecule, wmin=file_metadata["wavenumber_min"], wmax=file_metadata["wavenumber_max"], download_date=file_metadata["download_date"], urlname=file_metadata["download_url"], verbose=verbose, ) if verbose: print(f"Using existing database {databank_name}") return hdf2df( output, isotope=isotope, load_wavenum_min=load_wavenum_min, load_wavenum_max=load_wavenum_max, verbose=verbose, ) # Doesnt exist : download ds = DataSource(join(local_databases, "downloads")) if verbose: print(f"Downloading {inputf} for {molecule}.") download_date = date.today().strftime("%d %b %Y") columns = columns_2004 # Get linereturn (depends on OS, but file may also have been generated # on a different OS. Here we simply read the file to find out) with ds.open(urlname) as gfile: # locally downloaded file dt = _create_dtype( columns, "a2" ) # 'a2' allocates space to get \n or \n\r for linereturn character b = np.zeros(1, dtype=dt) gfile.readinto(b) linereturnformat = _get_linereturnformat(b, columns) with ds.open(urlname) as gfile: # locally downloaded file dt = _create_dtype(columns, linereturnformat) b = np.zeros(chunksize, dtype=dt) # receives the HITRAN 160-character data. wmin = np.inf wmax = 0 if verbose: print( f"Download complete. Building {molecule} database to {output}") with pd.HDFStore(output, mode="a", complib="blosc", complevel=9) as f: Nlines = 0 Ntotal_lines_expected = INFO_HITEMP_LINE_COUNT[molecule] pb = ProgressBar(N=Ntotal_lines_expected, active=verbose) for nbytes in iter(lambda: gfile.readinto(b), 0): if not b[-1]: # End of file flag within the chunk (but does not start # with End of file flag) so nbytes != 0 b = get_last(b) df = _ndarray2df(b, columns, linereturnformat) # df.to_hdf( # output, "df", format="table", append=True, complib="blosc", complevel=9 # ) f.put( key="df", value=df, append=True, format="table", data_columns=DATA_COLUMNS, ) wmin = np.min((wmin, df.wav.min())) wmax = np.max((wmax, df.wav.max())) Nlines += len(df) pb.update( Nlines, message= f"Parsed {Nlines:,} / {Ntotal_lines_expected:,} lines. Wavenumber range {wmin:.2f}-{wmax:.2f} cm-1 is complete.", ) # Reinitialize for next read b = np.zeros( chunksize, dtype=dt) # receives the HITRAN 160-character data. f.get_storer("df").attrs.metadata = { "wavenumber_min": wmin, "wavenumber_max": wmax, "download_date": download_date, "download_url": urlname, "version": radis.__version__, } pb.done() # Done: add final checks # ... check on the created file that all lines are there : with pd.HDFStore(output, "r") as store: nrows = store.get_storer("df").nrows assert nrows == Nlines if nrows != INFO_HITEMP_LINE_COUNT[molecule]: raise AssertionError( f"Number of lines in local database ({nrows:,}) " + "differ from the expected number of lines for " + f"HITEMP {molecule}: {INFO_HITEMP_LINE_COUNT[molecule]}" + ". Check that there was no recent update on HITEMP. " + "Else it may be a download error ?") # Add database to ~/.radis register_database(databank_name, [output], molecule, wmin, wmax, download_date, urlname, verbose) # Fully unzipped : clean if clean_cache_files: os.remove(ds._findfile(urlname)) if verbose >= 3: from radis.misc.printer import printg printg("... removed downloaded cache file") return hdf2df( output, isotope=isotope, load_wavenum_min=load_wavenum_min, load_wavenum_max=load_wavenum_max, verbose=verbose, )
def test_optically_thick_limit_1iso(verbose=True, plot=True, *args, **kwargs): """ Test that we find Planck in the optically thick limit In particular, this test will fail if : - linestrength are not properly calculated - at noneq, linestrength and emission integrals are mixed up The test should be run for 1 and several isotopes, because different calculations paths are used internally, and this can lead to different errors. Also, this test is used to run with DEBUG_MODE = True, which will check that isotopes and molecule ids are what we expect in all the groupby() loops that make the production code very fast. Notes ----- switched from large band calculation with [HITRAN-2016]_ to a calculation with the embedded [HITEMP-2010]_ fragment (shorter range, but no need to download files) """ if plot: # Make sure matplotlib is interactive so that test are not stuck in pytest plt.ion() # Force DEBUG_MODE DEBUG_MODE = radis.DEBUG_MODE radis.DEBUG_MODE = True try: wavenum_min = 2284.2 wavenum_max = 2284.6 P = 0.017 # bar wstep = 0.001 # cm-1 Tgas = 1200 # %% Generate some CO2 emission spectra # -------------- sf = SpectrumFactory( wavenum_min=wavenum_min, wavenum_max=wavenum_max, molecule="CO2", mole_fraction=1, path_length=0.05, cutoff=1e-25, broadening_max_width=1, export_populations=False, #'vib', export_lines=False, isotope=1, use_cached=True, wstep=wstep, pseudo_continuum_threshold=0, pressure=P, verbose=False, ) # sf.fetch_databank('astroquery') sf.warnings["NegativeEnergiesWarning"] = "ignore" sf.load_databank("HITEMP-CO2-TEST") pb = ProgressBar(3, active=verbose) s_eq = sf.eq_spectrum(Tgas=Tgas, mole_fraction=1, name="Equilibrium") pb.update(1) s_2T = sf.non_eq_spectrum(Tvib=Tgas, Trot=Tgas, mole_fraction=1, name="Noneq (2T)") pb.update(2) s_4T = sf.non_eq_spectrum(Tvib=(Tgas, Tgas, Tgas), Trot=Tgas, mole_fraction=1, name="Noneq (4T)") pb.update(3) s_plck = sPlanck( wavelength_min=2000, # =wavelength_min, wavelength_max= 5000, # =wavelength_max - wstep, # there is a border effect on last point T=Tgas, ) pb.done() # %% Post process: # MAke optically thick, and compare with Planck for s in [s_eq, s_2T, s_4T]: s.rescale_path_length(1e6) if plot: nfig = "test_opt_thick_limit_1iso {0}".format(s.name) plt.figure(nfig).clear() s.plot(wunit="nm", nfig=nfig, lw=4) s_plck.plot(wunit="nm", nfig=nfig, Iunit="mW/cm2/sr/nm", lw=2) plt.legend() if verbose: printm( "Residual between opt. thick CO2 spectrum ({0}) and Planck: {1:.2g}" .format( s.name, get_residual(s, s_plck, "radiance_noslit", ignore_nan=True), )) # assert get_residual(s, s_plck, 'radiance_noslit', ignore_nan=True) < 1e-3 assert get_residual(s, s_plck, "radiance_noslit", ignore_nan=True) < 0.9e-4 if verbose: printm("Tested optically thick limit is Planck (1 isotope): OK") finally: # Reset DEBUG_MODE radis.DEBUG_MODE = DEBUG_MODE
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
# Calculate atmosphere layers slabs = [] print('Calculating Atmosphere layers') pb = ProgressBar(len(atm)) for i, r in atm.iterrows(): pb.update(i) s = sf.eq_spectrum( Tgas=r.T_K, mole_fraction=x_CO2, path_length=r.path_length * 1e5, # cm pressure=r.P_Pa * 1e-5, # bar ) slabs.append(s) pb.done() # now solve the line of sight for the atmosphere: print('Solving radiative transfer equation') s_atm = SerialSlabs(*slabs) # %% Calculate the total Upward radiation s_los_400 = SerialSlabs(s_earth_0, s_atm, resample='intersect') # Note: RADIS does not have an irradiance unit by default. # Here we create the irradiance from the radiance s_los_400._q['irradiance'] = s_los_400.get('radiance_noslit')[1] * pi s_los_400.units['irradiance'] = s_los_400.units['radiance_noslit'].replace( '/sr', '') s_los_400.name = 'Earth + Atmosphere'