def test_populations(verbose=True, *args, **kwargs): """ Test that vib and rovib populations are calculated correctly """ from radis.lbl import SpectrumFactory from radis.misc.basics import all_in export = ["vib", "rovib"] sf = SpectrumFactory( 2000, 2300, export_populations=export, db_use_cached=True, cutoff=1e-25, isotope="1", ) sf.warnings.update({ "MissingSelfBroadeningWarning": "ignore", "VoigtBroadeningWarning": "ignore" }) sf.load_databank("HITRAN-CO-TEST") s = sf.non_eq_spectrum(2000, 2000) pops = sf.get_populations(export) if not all_in(["rovib", "vib"], list(pops["CO"][1]["X"].keys())): raise AssertionError( "vib and rovib levels should be defined after non_eq_spectrum calculation!" ) if not "nvib" in list(pops["CO"][1]["X"]["vib"].keys()): raise AssertionError( "Vib populations should be defined after non_eq_spectrum calculation!" ) s = sf.eq_spectrum(300) pops = sf.get_populations(export) if "nvib" in list(pops["CO"][1]["X"]["vib"].keys()): raise AssertionError( "Vib levels should not be defined anymore after eq_spectrum calculation!" ) # Any of these is True and something went wrong s = sf.non_eq_spectrum(2000, 2000) s2 = sf.non_eq_spectrum(300, 300) # printm(all(s2.get_vib_levels(isotope=1) == s.get_vib_levels(isotope=1))) assert not (s2.get_vib_levels() is s.get_vib_levels()) assert not (s2.get_rovib_levels() == s.get_rovib_levels()).all().all() assert not (s2.get_rovib_levels() is s.get_rovib_levels()) return True # if no AssertionError
def test_populations(verbose=True, *args, **kwargs): ''' Test that vib and rovib populations are calculated correctly ''' from radis.lbl import SpectrumFactory from radis.misc.basics import all_in export = ['vib', 'rovib'] sf = SpectrumFactory(2000, 2300, export_populations=export, db_use_cached=True, cutoff=1e-25, isotope='1') sf.warnings.update({ 'MissingSelfBroadeningWarning': 'ignore', 'VoigtBroadeningWarning': 'ignore' }) sf.load_databank('HITRAN-CO-TEST') s = sf.non_eq_spectrum(2000, 2000) pops = sf.get_populations(export) if not all_in(['rovib', 'vib'], list(pops['CO'][1]['X'].keys())): raise AssertionError( 'vib and rovib levels should be defined after non_eq_spectrum calculation!' ) if not 'nvib' in list(pops['CO'][1]['X']['vib'].keys()): raise AssertionError( 'Vib populations should be defined after non_eq_spectrum calculation!' ) s = sf.eq_spectrum(300) pops = sf.get_populations(export) if 'nvib' in list(pops['CO'][1]['X']['vib'].keys()): raise AssertionError( 'Vib levels should not be defined anymore after eq_spectrum calculation!' ) # Any of these is True and something went wrong s = sf.non_eq_spectrum(2000, 2000) s2 = sf.non_eq_spectrum(300, 300) #printm(all(s2.get_vib_levels(isotope=1) == s.get_vib_levels(isotope=1))) assert not (s2.get_vib_levels() is s.get_vib_levels()) assert not (s2.get_rovib_levels() == s.get_rovib_levels()).all().all() assert not (s2.get_rovib_levels() is s.get_rovib_levels()) return True # if no AssertionError
def calc_spectrum( wavenum_min=None, wavenum_max=None, wavelength_min=None, wavelength_max=None, Tgas=None, Tvib=None, Trot=None, pressure=1.01325, molecule=None, isotope="all", mole_fraction=1, path_length=1, databank="hitran", medium="air", wstep=0.01, broadening_max_width=10, cutoff=1e-27, optimization="min-RMS", overpopulation=None, name=None, save_to="", use_cached=True, mode="cpu", export_lines=False, verbose=True, **kwargs ): """Multipurpose function to calculate a :class:`~radis.spectrum.spectrum.Spectrum` from automatically downloaded databases (HITRAN/HITEMP) or manually downloaded local databases, under equilibrium or non-equilibrium, with or without overpopulation, using either CPU or GPU. It is a wrapper to :class:`~radis.lbl.factory.SpectrumFactory` class. For advanced used, please refer to the aforementionned class. Parameters ---------- wavenum_min, wavenum_max: float [:math:`cm^{-1}`] or `~astropy.units.quantity.Quantity` wavenumber range to be processed in :math:`cm^{-1}` wavelength_min, wavelength_max: float [:math:`nm`] or `~astropy.units.quantity.Quantity` wavelength range to be processed in :math:`nm`. Wavelength in ``'air'`` or ``'vacuum'`` depending of the value of ``'medium'``. Use arbitrary units:: Tgas: float [:math:`K`] Gas temperature. If non equilibrium, is used for :math:`T_{translational}`. Default ``300``K Tvib, Trot: float [:math:`K`] Vibrational and rotational temperatures (for non-LTE calculations). If ``None``, they are at equilibrium with ``Tgas`` pressure: float [:math:`bar`] or `~astropy.units.quantity.Quantity` partial pressure of gas in bar. Default ``1.01325`` (1 atm). Use arbitrary units:: import astropy.units as u calc_spectrum(..., pressure=20*u.mbar) molecule: int, str, list or ``None`` molecule id (HITRAN format) or name. For multiple molecules, use a list. The ``'isotope'``, ``'mole_fraction'``, ``'databank'`` and ``'overpopulation'`` parameters must then be dictionaries. If ``None``, the molecule can be infered from the database files being loaded. See the list of supported molecules in :py:data:`~radis.db.MOLECULES_LIST_EQUILIBRIUM` and :py:data:`~radis.db.MOLECULES_LIST_NONEQUILIBRIUM`. Default ``None``. isotope: int, list, str of the form ``'1,2'``, or ``'all'``, or dict isotope id (sorted by relative density: (eg: 1: CO2-626, 2: CO2-636 for CO2). See [HITRAN-2016]_ documentation for isotope list for all species. If ``'all'``, all isotopes in database are used (this may result in larger computation times!). Default ``'all'``. For multiple molecules, use a dictionary with molecule names as keys :: mole_fraction={'CO2':0.8 , 'CO':0.2 } mole_fraction: float or dict database species mole fraction. Default ``1``. For multiple molecules, use a dictionary with molecule names as keys :: mole_fraction={'CO2': 0.8, 'CO':0.2} path_length: float [:math:`cm`] or `~astropy.units.quantity.Quantity` slab size. Default ``1`` cm. Use arbitrary units:: import astropy.units as u calc_spectrum(..., path_length=1000*u.km) databank: str or dict can be either: - ``'hitran'``, to fetch automatically the latest HITRAN version through :py:func:`~radis.io.query.fetch_astroquery - ``'hitemp'``, to fetch automatically the latest HITEMP version through :py:func:`~radis.io.hitemp.fetch_hitemp`. - the name of a a valid database file, in which case the format is inferred. For instance, ``'.par'`` is recognized as ``hitran/hitemp`` format. Accepts wildcards ``'*'`` to select multiple files. - the name of a spectral database registered in your ``~/.radis`` :ref:`configuration file <label_lbl_config_file>`. This allows to use multiple database files. Default ``'hitran'``. See :class:`~radis.lbl.loader.DatabankLoader` for more information on line databases, and :data:`~radis.misc.config.DBFORMAT` for your ``~/.radis`` file format. For multiple molecules, use a dictionary with molecule names as keys:: databank='hitran' # automatic download (or 'hitemp') databank='PATH/TO/05_HITEMP2019.par' # path to a file databank='*CO2*.par' #to get all the files that have CO2 in their names (case insensitive) databank='HITEMP-2019-CO' # user-defined database in Configuration file databank = {'CO2' : 'PATH/TO/05_HITEMP2019.par', 'CO' : 'hitran'} # for multiple molecules Other Parameters ---------------- medium: ``'air'``, ``'vacuum'`` propagating medium when giving inputs with ``'wavenum_min'``, ``'wavenum_max'``. Does not change anything when giving inputs in wavenumber. Default ``'air'``. wstep: float (:math:`cm^{-1}`) Spacing of calculated spectrum. Default ``0.01 cm-1``. broadening_max_width: float (cm-1) Full width over which to compute the broadening. Large values will create a huge performance drop (scales as ~broadening_width^2 without DLM) The calculated spectral range is increased (by broadening_max_width/2 on each side) to take into account overlaps from out-of-range lines. Default ``10`` cm-1. cutoff: float (~ unit of Linestrength: :math:`cm^{-1}/(#.cm^{-2})`) discard linestrengths that are lower that this, to reduce calculation times. ``1e-27`` is what is generally used to generate databases such as CDSD. If ``0``, no cutoff. Default ``1e-27``. optimization : ``"simple"``, ``"min-RMS"``, ``None`` If either ``"simple"`` or ``"min-RMS"`` DLM optimization for lineshape calculation is used: - ``"min-RMS"`` : weights optimized by analytical minimization of the RMS-error (See: [Spectral Synthesis Algorithm]_) - ``"simple"`` : weights equal to their relative position in the grid If using the LDM optimization, broadening method is automatically set to ``'fft'``. If ``None``, no lineshape interpolation is performed and the lineshape of all lines is calculated. Refer to [Spectral Synthesis Algorithm]_ for more explanation on the LDM method for lineshape interpolation. Default ``"min-RMS"``. overpopulation: dict dictionary of overpopulation compared to the given vibrational temperature. Default ``None``. Example:: overpopulation = {'CO2' : {'(00`0`0)->(00`0`1)': 2.5, '(00`0`1)->(00`0`2)': 1, '(01`1`0)->(01`1`1)': 1, '(01`1`1)->(01`1`2)': 1 } } export_lines: boolean if ``True``, saves details of all calculated lines in Spectrum. This is necessary to later use :py:meth:`~radis.spectrum.spectrum.Spectrum.line_survey`, but can take some space. Default ``False``. name: str name of the output Spectrum. If ``None``, a unique ID is generated. save_to: str save to a `.spec` file which contains absorption & emission features, all calculation parameters, and can be opened with :py:func:`~radis.tools.database.load_spec`. File can be reloaded and exported to text formats afterwards, see :py:meth:`~radis.spectrum.spectrum.Spectrum.savetxt`. If file already exists, replace. use_cached: boolean use cached files for line database and energy database. Default ``True``. verbose: boolean, or int If ``False``, stays quiet. If ``True``, tells what is going on. If ``>=2``, gives more detailed messages (for instance, details of calculation times). Default ``True``. mode: ``'cpu'``, ``'gpu'`` if set to ``'cpu'``, computes the spectra purely on the CPU. if set to ``'gpu'``, offloads the calculations of lineshape and broadening steps to the GPU making use of parallel computations to speed up the process. Default ``'cpu'``. Note that ``mode='gpu'`` requires CUDA compatible hardware to execute. For more information on how to setup your system to run GPU-accelerated methods using CUDA and Cython, check `GPU Spectrum Calculation on RADIS <https://radis.readthedocs.io/en/latest/lbl/gpu.html>`__ **kwargs: other inputs forwarded to SpectrumFactory For instance: ``warnings``. See :class:`~radis.lbl.factory.SpectrumFactory` documentation for more details on input. Returns ------- s: :class:`~radis.spectrum.spectrum.Spectrum` Output spectrum. Use the :py:meth:`~radis.spectrum.spectrum.Spectrum.get` method to retrieve a spectral quantity (``'radiance'``, ``'radiance_noslit'``, ``'absorbance'``, etc...) Or the :py:meth:`~radis.spectrum.spectrum.Spectrum.plot` method to plot it directly. See [1]_ to get an overview of all Spectrum methods References ---------- .. [1] RADIS doc: `Spectrum how to? <https://radis.readthedocs.io/en/latest/spectrum/spectrum.html#label-spectrum>`__ .. [2] RADIS GPU support: 'GPU Calculations on RADIS <https://radis.readthedocs.io/en/latest/lbl/gpu.html>' Examples -------- Calculate a CO spectrum from the HITRAN database:: s = calc_spectrum(1900, 2300, # cm-1 molecule='CO', isotope='1,2,3', pressure=1.01325, # bar Tgas=1000, mole_fraction=0.1, databank='hitran', # or 'hitemp' ) s.apply_slit(0.5, 'nm') s.plot('radiance') This example uses the :py:meth:`~radis.spectrum.spectrum.Spectrum.apply_slit` and :py:meth:`~radis.spectrum.spectrum.Spectrum.plot` methods. See also :py:meth:`~radis.spectrum.spectrum.Spectrum.line_survey`:: s.line_survey(overlay='radiance') Calculate a CO2 spectrum from the CDSD-4000 database:: s = calc_spectrum(2200, 2400, # cm-1 molecule='CO2', isotope='1', databank='/path/to/cdsd/databank/in/npy/format/', pressure=0.1, # bar Tgas=1000, mole_fraction=0.1, mode='gpu' ) s.plot('absorbance') This example uses the :py:meth:`~radis.lbl.factory.eq_spectrum_gpu` method to calculate the spectrum on the GPU. The databank points to the CDSD-4000 databank that has been pre-processed and stored in ``numpy.npy`` format. Refer to the online :ref:`Examples <label_examples>` for more cases, and to the :ref:`Spectrum page <label_spectrum>` for details on post-processing methods. For more details on how to use the GPU method and process the database, refer to the examples linked above and the documentation on :ref:`GPU support for RADIS <label_radis_gpu>`. See Also -------- :class:`~radis.lbl.factory.SpectrumFactory`, and the :ref:`Spectrum page <label_spectrum>` """ from radis.los.slabs import MergeSlabs if molecule is not None and type(molecule) != list: molecule = [molecule] # fall back to the other case: multiple molecules # Stage 1. Find all molecules, whatever the user input configuration # ... Input arguments that CAN be dictionaries of molecules. DICT_INPUT_ARGUMENTS = { "isotope": isotope, "mole_fraction": mole_fraction, "databank": databank, } # Same, but when the values of the arguments themselves are already a dict. # (dealt with separately because we cannot use them to guess what are the input molecules) DICT_INPUT_DICT_ARGUMENTS = {"overpopulation": overpopulation} def _check_molecules_are_consistent( molecule_reference_set, reference_name, new_argument, new_argument_name ): """Will test that molecules set are the same in molecule_reference_set and new_argument, if new_argument is a dict. molecule_reference_set is a set of molecules (yeah!). reference_name is the name of the argument from which we guessed the list of molecules (used to have a clear error message). new_argument is the new argument to check new_argument_name is its name. Returns the set of molecules as found in new_argument, if applicable, else the molecule_reference_set (this allows us to parse all arguments too) Note that names are just here to provide clear error messages to the user if there is a contradiction. """ if isinstance(new_argument, dict): if molecule_reference_set is None: # input molecules are still unknown return set(new_argument.keys()), new_argument_name elif set(new_argument.keys()) != set(molecule_reference_set): raise ValueError( "Keys of molecules in the {0} dictionary must be the same as given in `{1}=`, i.e: {2}. Instead, we got {3}".format( new_argument_name, reference_name, molecule_reference_set, set(new_argument.keys()), ) ) else: return ( set(new_argument.keys()), new_argument_name, ) # so now we changed the reference else: return molecule_reference_set, reference_name # Parse all inputs: molecule_reference_set = molecule reference_name = "molecule" for argument_name, argument in DICT_INPUT_ARGUMENTS.items(): molecule_reference_set, reference_name = _check_molecules_are_consistent( molecule_reference_set, reference_name, argument, argument_name ) # ... Now we are sure there are no contradctions. Just ensure we have molecules: if molecule_reference_set is None: raise ValueError( "Please enter the molecule(s) to calculate in the `molecule=` argument or as a dictionary in the following: {0}".format( list(DICT_INPUT_ARGUMENTS.keys()) ) ) # Stage 2. Now we have the list of molecules. Let's get the input arguments for each of them. # ... Initialize and fill the master-dictionary molecule_dict = {} for molecule in molecule_reference_set: molecule_dict[molecule] = {} for argument_name, argument_dict in DICT_INPUT_ARGUMENTS.items(): if isinstance(argument_dict, dict): # Choose the correspond value molecule_dict[molecule][argument_name] = argument_dict[molecule] # Will raise a KeyError if not defined. That's fine! # TODO: maybe need to catch KeyError and raise a better error message? else: # argument_name is not a dictionary. # Let's distribute the same value to every molecule: molecule_dict[molecule][argument_name] = argument_dict # If wrong argument, it will be caught in _calc_spectrum() later. # ... Special case of dictionary arguments. Find out if they were given as default, or per dictionary of molecules: is_same_for_all_molecules = dict.fromkeys(DICT_INPUT_DICT_ARGUMENTS) for argument_name, argument_dict in DICT_INPUT_DICT_ARGUMENTS.items(): if not isinstance(argument_dict, dict): is_same_for_all_molecules[argument_name] = True else: # Argument is a dictionary. Guess if keys are molecules, or levels. # Ex: overpopulation dict could be {'CO2':{'(0,0,0,1)':10}} or directly {{'(0,0,0,1)':10}} argument_keys = set(argument_dict.keys()) if all_in(argument_keys, molecule_reference_set): is_same_for_all_molecules[argument_name] = False else: is_same_for_all_molecules[argument_name] = True # ... now fill them in: for argument_name, argument_dict in DICT_INPUT_DICT_ARGUMENTS.items(): if is_same_for_all_molecules[argument_name]: for mol in molecule_reference_set: # copy the value for everyone molecule_dict[mol][argument_name] = deepcopy( argument_dict ) # in case it gets edited. else: # argument_dict keys are the molecules: for mol in molecule_reference_set: molecule_dict[mol][argument_name] = argument_dict[mol] # Stage 3: Now let's calculate all the spectra s_list = [] for molecule, dict_arguments in molecule_dict.items(): kwargs_molecule = deepcopy( kwargs ) # these are the default supplementary arguments. Deepcopy ensures that they remain the same for all molecules, even if modified in _calc_spectrum # We add all of the DICT_INPUT_ARGUMENTS values: kwargs_molecule.update(**dict_arguments) s_list.append( _calc_spectrum( wavenum_min=wavenum_min, wavenum_max=wavenum_max, wavelength_min=wavelength_min, wavelength_max=wavelength_max, Tgas=Tgas, Tvib=Tvib, Trot=Trot, pressure=pressure, # overpopulation=overpopulation, # now in dict_arguments molecule=molecule, # isotope=isotope, # now in dict_arguments # mole_fraction=mole_fraction, # now in dict_arguments path_length=path_length, # databank=databank, # now in dict_arguments medium=medium, wstep=wstep, broadening_max_width=broadening_max_width, cutoff=cutoff, optimization=optimization, name=name, use_cached=use_cached, verbose=verbose, mode=mode, **kwargs_molecule ) ) # Stage 4: merge all molecules and return s = MergeSlabs(*s_list) if save_to: s.store(path=save_to, if_exists_then="replace", verbose=verbose) return s
def add_bands(df, dbformat, lvlformat, verbose=True): ''' Assign all transitions to a vibrational band: Add 'band', 'viblvl_l' and 'viblvl_u' attributes for each line to allow parsing the lines by band with:: df0.groupby('band') Parameters ---------- df: pandas Dataframe Line (transitions) database dbformat: one of :data:`~radis.lbl.loader.KNOWN_DBFORMAT` : ``'cdsd```, ``'hitemp'`` format of Line database lvlformat: 'cdsd`, 'hitemp' format of Returns ------- None input df is changed Examples -------- Add transitions in a Dataframe based on CDSD (p, c, j, n) format:: add_bands(df, 'cdsd') Notes ----- Performance with test case (CDSD CO2 2380-2400 cm-1): - Initial: with .apply() 8.08 s ± 95.2 ms - with groupby(): 9s worse!! - using simple (and more readable) astype(str) statements: 523 ms ± 19.6 ms ''' # Check inputs if not dbformat in KNOWN_DBFORMAT: raise ValueError('dbformat ({0}) should be one of: {1}'.format( dbformat, KNOWN_DBFORMAT)) if not lvlformat in KNOWN_LVLFORMAT: raise ValueError('lvlformat ({0}) should be one of: {1}'.format( lvlformat, KNOWN_LVLFORMAT)) if verbose: t0 = time() print('... sorting lines by vibrational bands') # Calculate bands: id = list(pd.unique(df['id'])) if len(id) > 1: raise ValueError('Cant calculate vibrational bands for multiple ' + 'molecules yet') # although it's an easy fix. Just # groupby id molecule = get_molecule(id[0]) if molecule == 'CO2': vib_lvl_name_hitran = vib_lvl_name_hitran_class5 if lvlformat in ['cdsd-pc', 'cdsd-pcN', 'cdsd-hamil']: # ensures that vib_lvl_name functions wont crash if dbformat not in ['cdsd', 'cdsd4000', 'hitran']: raise NotImplementedError( 'lvlformat {0} not supported with dbformat {1}'.format( lvlformat, dbformat)) # Use vibrational nomenclature of CDSD (p,c,j,n) or HITRAN (v1v2l2v3J) # depending on the Level Database. # In both cases, store the other one. # ... note: vib level in a CDSD (p,c,j,n) database is ambiguous. # ... a vibrational energy Evib can have been defined for every (p, c) group: if lvlformat in ['cdsd-pc']: viblvl_l_cdsd = vib_lvl_name_cdsd_pc(df.polyl, df.wangl) viblvl_u_cdsd = vib_lvl_name_cdsd_pc(df.polyu, df.wangu) # ... or for every (p, c, N) group: elif lvlformat in ['cdsd-pcN']: viblvl_l_cdsd = vib_lvl_name_cdsd_pcN(df.polyl, df.wangl, df.rankl) viblvl_u_cdsd = vib_lvl_name_cdsd_pcN(df.polyu, df.wangu, df.ranku) # ... or for every level (p, c, J ,N) (that's the case if coupling terms # are used taken into account... it also takes a much longer time # to look up vibrational energies in the LineDatabase, warning!): elif lvlformat in ['cdsd-hamil']: viblvl_l_cdsd = vib_lvl_name_cdsd_pcJN(df.polyl, df.wangl, df.jl, df.rankl) viblvl_u_cdsd = vib_lvl_name_cdsd_pcJN(df.polyu, df.wangu, df.ju, df.ranku) else: raise ValueError( 'Unexpected level format: {0}'.format(lvlformat)) band_cdsd = viblvl_l_cdsd + '->' + viblvl_u_cdsd df.loc[:, 'viblvl_l'] = viblvl_l_cdsd df.loc[:, 'viblvl_u'] = viblvl_u_cdsd df.loc[:, 'band'] = band_cdsd # Calculate HITRAN format too (to store them)) if all_in(['v1l', 'v2l', 'l2l', 'v3l'], df): viblvl_l_hitran = vib_lvl_name_hitran(df.v1l, df.v2l, df.l2l, df.v3l) viblvl_u_hitran = vib_lvl_name_hitran(df.v1u, df.v2u, df.l2u, df.v3u) band_hitran = viblvl_l_hitran + '->' + viblvl_u_hitran df.loc[:, 'viblvl_htrn_l'] = viblvl_l_hitran df.loc[:, 'viblvl_htrn_u'] = viblvl_u_hitran df.loc[:, 'band_htrn'] = band_hitran # 'radis' uses Dunham development based on v1v2l2v3 HITRAN convention elif lvlformat in ['radis']: if dbformat not in ['hitran', 'cdsd']: raise NotImplementedError( 'lvlformat `{0}` not supported with dbformat `{1}`'.format( lvlformat, dbformat)) # Calculate bands with HITRAN convention viblvl_l_hitran = vib_lvl_name_hitran(df.v1l, df.v2l, df.l2l, df.v3l) viblvl_u_hitran = vib_lvl_name_hitran(df.v1u, df.v2u, df.l2u, df.v3u) band_hitran = viblvl_l_hitran + '->' + viblvl_u_hitran df.loc[:, 'viblvl_l'] = viblvl_l_hitran df.loc[:, 'viblvl_u'] = viblvl_u_hitran df.loc[:, 'band'] = band_hitran else: raise NotImplementedError( 'Cant deal with lvlformat={0} for {1}'.format( lvlformat, molecule)) elif molecule in HITRAN_CLASS1: # includes 'CO' # Note. TODO. Move that in loader.py (or somewhere consistent with # classes defined in cdsd.py / hitran.py) if lvlformat in ['radis']: # ensures that vib_lvl_name functions wont crash if dbformat not in ['hitran']: raise NotImplementedError( 'lvlformat {0} not supported with dbformat {1}'.format( lvlformat, dbformat)) vib_lvl_name = vib_lvl_name_hitran_class1 df.loc[:, 'viblvl_l'] = vib_lvl_name(df['vl']) df.loc[:, 'viblvl_u'] = vib_lvl_name(df['vu']) df.loc[:, 'band'] = df['viblvl_l'] + '->' + df['viblvl_u'] else: raise NotImplementedError( 'Lvlformat not defined for {0}: {1}'.format( molecule, lvlformat)) else: raise NotImplementedError( 'Vibrational bands not yet defined for molecule: ' + '{0} with database format: {1}. '.format(molecule, dbformat) + 'Update add_bands()') if verbose: print(('... lines sorted in {0:.1f}s'.format(time() - t0))) return
def calc_spectrum( wavenum_min=None, wavenum_max=None, wavelength_min=None, wavelength_max=None, Tgas=None, Tvib=None, Trot=None, pressure=1.01325, molecule=None, isotope="all", mole_fraction=1, path_length=1, medium="air", databank="fetch", wstep=0.01, broadening_max_width=10, optimization="min-RMS", overpopulation=None, name=None, use_cached=True, verbose=True, mode="cpu", **kwargs ): """Multipurpose function to calculate :class:`~radis.spectrum.spectrum.Spectrum` under equilibrium (using either CPU or GPU), or non-equilibrium, with or without overpopulation. It's a wrapper to :class:`~radis.lbl.factory.SpectrumFactory` class. For advanced used, please refer to the aforementionned class. Parameters ---------- wavenum_min: float [cm-1] minimum wavenumber to be processed in cm^-1 wavenum_max: float [cm-1] maximum wavenumber to be processed in cm^-1 wavelength_min: float [nm] minimum wavelength to be processed in nm. Wavelength in ``'air'`` or ``'vacuum'`` depending of the value of the parameter ``'medium='`` wavelength_max: float [nm] maximum wavelength to be processed in nm. Wavelength in ``'air'`` or ``'vacuum'`` depending of the value of the parameter ``'medium='`` Tgas: float [K] Gas temperature. If non equilibrium, is used for Ttranslational. Default ``300`` K Tvib: float [K] Vibrational temperature. If ``None``, equilibrium calculation is run with Tgas Trot: float [K] Rotational temperature. If ``None``, equilibrium calculation is run with Tgas pressure: float [bar] partial pressure of gas in bar. Default ``1.01325`` (1 atm) molecule: int, str, list or ``None`` molecule id (HITRAN format) or name. For multiple molecules, use a list. The `isotope`, `mole_fraction`, `databank` and `overpopulation` parameters must then be dictionaries. If ``None``, the molecule can be infered from the database files being loaded. See the list of supported molecules in :py:data:`~radis.db.MOLECULES_LIST_EQUILIBRIUM` and :py:data:`~radis.db.MOLECULES_LIST_NONEQUILIBRIUM`. Default ``None``. isotope: int, list, str of the form ``'1,2'``, or ``'all'``, or dict isotope id (sorted by relative density: (eg: 1: CO2-626, 2: CO2-636 for CO2). See [HITRAN-2016]_ documentation for isotope list for all species. If ``'all'``, all isotopes in database are used (this may result in larger computation times!). Default ``'all'``. For multiple molecules, use a dictionary with molecule names as keys. Example:: mole_fraction={'CO2':0.8 , 'CO':0.2 } mole_fraction: float or dict database species mole fraction. Default ``1``. For multiple molecules, use a dictionary with molecule names as keys. Example:: mole_fraction={'CO2': 0.8, 'CO':0.2} path_length: float [cm] slab size. Default ``1``. databank: str or dict can be either: - ``'fetch'``, to fetch automatically from [HITRAN-2016]_ through astroquery. .. warning:: [HITRAN-2016]_ is valid for low temperatures (typically < 700 K). For higher temperatures you may need [HITEMP-2010]_ - the name of a a valid database file, in which case the format is inferred. For instance, ``'.par'`` is recognized as ``hitran/hitemp`` format. Accepts wildcards ``'*'`` to select multiple files. - the name of a spectral database registered in your ``~/.radis`` configuration file. This allows to use multiple database files. See :ref:`Configuration file <label_lbl_config_file>`. Default ``'fetch'``. See :class:`~radis.lbl.loader.DatabankLoader` for more information on line databases, and :data:`~radis.misc.config.DBFORMAT` for your ``~/.radis`` file format For multiple molecules, use a dictionary with molecule names as keys:: databank='fetch' # automatic download databank='PATH/TO/05_HITEMP2019.par' # path to a file databank='*CO2*.par' #to get all the files that have CO2 in their names (case insensitive) databank='HITEMP-2019-CO' # user-defined database in Configuration file databank = {'CO2' : 'PATH/TO/05_HITEMP2019.par', 'CO' : 'fetch'} # for multiple molecules medium: ``'air'``, ``'vacuum'`` propagating medium when giving inputs with ``'wavenum_min'``, ``'wavenum_max'``. Does not change anything when giving inputs in wavenumber. Default ``'air'`` wstep: float (cm-1) Spacing of calculated spectrum. Default ``0.01 cm-1`` broadening_max_width: float (cm-1) Full width over which to compute the broadening. Large values will create a huge performance drop (scales as ~broadening_width^2 without DLM) The calculated spectral range is increased (by broadening_max_width/2 on each side) to take into account overlaps from out-of-range lines. Default ``10`` cm-1. Other Parameters ---------------- optimization : ``"simple"``, ``"min-RMS"``, ``None`` If either ``"simple"`` or ``"min-RMS"`` DLM optimization for lineshape calculation is used: - ``"min-RMS"`` : weights optimized by analytical minimization of the RMS-error (See: [DLM_article]_) - ``"simple"`` : weights equal to their relative position in the grid If using the DLM optimization, broadening method is automatically set to ``'fft'``. If ``None``, no lineshape interpolation is performed and the lineshape of all lines is calculated. Refer to [DLM_article]_ for more explanation on the DLM method for lineshape interpolation. Default ``"min-RMS"`` overpopulation: dict dictionary of overpopulation compared to the given vibrational temperature. Default ``None``. Example:: overpopulation = {'CO2' : {'(00`0`0)->(00`0`1)': 2.5, '(00`0`1)->(00`0`2)': 1, '(01`1`0)->(01`1`1)': 1, '(01`1`1)->(01`1`2)': 1 } } slit: float, str, or ``None`` if float, FWHM of a triangular slit function. If str, path to an experimental slit function. If None, no slit is applied. Default ``None``. plot: str any parameter such as 'radiance' (if slit is given), 'radiance_noslit', 'absorbance', etc... Default ``None`` name: str name of the case. If None, a unique ID is generated. Default ``None`` use_cached: boolean use cached files for line database and energy database. Default ``True`` verbose: boolean, or int If ``False``, stays quiet. If ``True``, tells what is going on. If ``>=2``, gives more detailed messages (for instance, details of calculation times). Default ``True``. **kwargs: other inputs forwarded to SpectrumFactory For instance: ``warnings``. See :class:`~radis.lbl.factory.SpectrumFactory` documentation for more details on input. For instance: pseudo_continuum_threshold: float if not 0, first calculate a rough approximation of the spectrum, then moves all lines whose linestrength intensity is less than this threshold of the maximum in a semi-continuum. Values above 0.01 can yield significant errors, mostly in highly populated areas. 80% of the lines can typically be moved in a continuum, resulting in 5 times faster spectra. If 0, no semi-continuum is used. Default 0. mode: ``'cpu'``, ``'gpu'`` if set to 'cpu', computes the spectra purely on the CPU. if set to 'gpu', offloads the calculations of lineshape and broadening steps to the GPU making use of parallel computations to speed up the process. Default 'cpu'. Note that mode='gpu' requires CUDA compatible hardware to execute. For more information on how to setup your system to run GPU-accelerated methods using CUDA and Cython, check `GPU Spectrum Calculation on RADIS <https://radis.readthedocs.io/en/latest/lbl/gpu.html>` Returns ------- s: :class:`~radis.spectrum.spectrum.Spectrum` Output spectrum. Use the :py:meth:`~radis.spectrum.spectrum.Spectrum.get` method to retrieve a spectral quantity (``'radiance'``, ``'radiance_noslit'``, ``'absorbance'``, etc...) Or the :py:meth:`~radis.spectrum.spectrum.Spectrum.plot` method to plot it directly. See [1]_ to get an overview of all Spectrum methods References ---------- .. [1] RADIS doc: `Spectrum how to? <https://radis.readthedocs.io/en/latest/spectrum/spectrum.html#label-spectrum>`__ .. [2] RADIS GPU support: 'GPU Calculations on RADIS <https://radis.readthedocs.io/en/latest/lbl/gpu.html>' Examples -------- Calculate a CO spectrum from the HITRAN database:: s = calc_spectrum(1900, 2300, # cm-1 molecule='CO', isotope='1,2,3', pressure=1.01325, # bar Tgas=1000, mole_fraction=0.1, ) s.apply_slit(0.5, 'nm') s.plot('radiance') This example uses the :py:meth:`~radis.spectrum.spectrum.Spectrum.apply_slit` and :py:meth:`~radis.spectrum.spectrum.Spectrum.plot` methods. See also :py:meth:`~radis.spectrum.spectrum.Spectrum.line_survey`:: s.line_survey(overlay='radiance') Calculate a CO2 spectrum from the CDSD-4000 database: s = calc_spectrum(2200, 2400, # cm-1 molecule='CO2', isotope='1', databank='/path/to/cdsd/databank/in/npy/format/', pressure=0.1, # bar Tgas=1000, mole_fraction=0.1, mode='gpu' ) s.plot('absorbance') This example uses the :py:meth:`~radis.lbl.factor.eq_spectrum_gpu` method to calculate the spectrum on the GPU. The databank points to the CDSD-4000 databank that has been pre-processed and stored in `numpy.npy` format. Refer to the online :ref:`Examples <label_examples>` for more cases, and to the :ref:`Spectrum page <label_spectrum>` for details on post-processing methods. For more details on how to use the GPU method and process the database, refer to the examples linked above and the documentation on :ref:`GPU support for RADIS <label_radis_gpu>`. See Also -------- :class:`~radis.lbl.factory.SpectrumFactory`, the :ref:`Spectrum page <label_spectrum>` """ from radis.los.slabs import MergeSlabs if molecule is not None and type(molecule) != list: molecule = [molecule] # fall back to the other case: multiple molecules # Stage 1. Find all molecules, whatever the user input configuration # ... Input arguments that CAN be dictionaries of molecules. DICT_INPUT_ARGUMENTS = { "isotope": isotope, "mole_fraction": mole_fraction, "databank": databank, } # Same, but when the values of the arguments themselves are already a dict. # (dealt with separately because we cannot use them to guess what are the input molecules) DICT_INPUT_DICT_ARGUMENTS = {"overpopulation": overpopulation} def _check_molecules_are_consistent( molecule_reference_set, reference_name, new_argument, new_argument_name ): """Will test that molecules set are the same in molecule_reference_set and new_argument, if new_argument is a dict. molecule_reference_set is a set of molecules (yeah!). reference_name is the name of the argument from which we guessed the list of molecules (used to have a clear error message). new_argument is the new argument to check new_argument_name is its name Returns the set of molecules as found in new_argument, if applicable, else the molecule_reference_set (this allows us to parse all arguments too) Note that names are just here to provide clear error messages to the user if there is a contradiction. """ if isinstance(new_argument, dict): if molecule_reference_set is None: # input molecules are still unknown return set(new_argument.keys()), new_argument_name elif set(new_argument.keys()) != set(molecule_reference_set): raise ValueError( "Keys of molecules in the {0} dictionary must be the same as given in `{1}=`, i.e: {2}. Instead, we got {3}".format( new_argument_name, reference_name, molecule_reference_set, set(new_argument.keys()), ) ) else: return ( set(new_argument.keys()), new_argument_name, ) # so now we changed the reference else: return molecule_reference_set, reference_name # Parse all inputs: molecule_reference_set = molecule reference_name = "molecule" for argument_name, argument in DICT_INPUT_ARGUMENTS.items(): molecule_reference_set, reference_name = _check_molecules_are_consistent( molecule_reference_set, reference_name, argument, argument_name ) # ... Now we are sure there are no contradctions. Just ensure we have molecules: if molecule_reference_set is None: raise ValueError( "Please enter the molecule(s) to calculate in the `molecule=` argument or as a dictionary in the following: {0}".format( list(DICT_INPUT_ARGUMENTS.keys()) ) ) # Stage 2. Now we have the list of molecules. Let's get the input arguments for each of them. # ... Initialize and fill the master-dictionary molecule_dict = {} for molecule in molecule_reference_set: molecule_dict[molecule] = {} for argument_name, argument_dict in DICT_INPUT_ARGUMENTS.items(): if isinstance(argument_dict, dict): # Choose the correspond value molecule_dict[molecule][argument_name] = argument_dict[molecule] # Will raise a KeyError if not defined. That's fine! # TODO: maybe need to catch KeyError and raise a better error message? else: # argument_name is not a dictionary. # Let's distribute the same value to every molecule: molecule_dict[molecule][argument_name] = argument_dict # If wrong argument, it will be caught in _calc_spectrum() later. # ... Special case of dictionary arguments. Find out if they were given as default, or per dictionary of molecules: is_same_for_all_molecules = dict.fromkeys(DICT_INPUT_DICT_ARGUMENTS) for argument_name, argument_dict in DICT_INPUT_DICT_ARGUMENTS.items(): if not isinstance(argument_dict, dict): is_same_for_all_molecules[argument_name] = True else: # Argument is a dictionary. Guess if keys are molecules, or levels. # Ex: overpopulation dict could be {'CO2':{'(0,0,0,1)':10}} or directly {{'(0,0,0,1)':10}} argument_keys = set(argument_dict.keys()) if all_in(argument_keys, molecule_reference_set): is_same_for_all_molecules[argument_name] = False else: is_same_for_all_molecules[argument_name] = True # ... now fill them in: for argument_name, argument_dict in DICT_INPUT_DICT_ARGUMENTS.items(): if is_same_for_all_molecules[argument_name]: for mol in molecule_reference_set: # copy the value for everyone molecule_dict[mol][argument_name] = deepcopy( argument_dict ) # in case it gets edited. else: # argument_dict keys are the molecules: for mol in molecule_reference_set: molecule_dict[mol][argument_name] = argument_dict[mol] # Stage 3: Now let's calculate all the spectra s_list = [] for molecule, dict_arguments in molecule_dict.items(): kwargs_molecule = deepcopy( kwargs ) # these are the default supplementary arguments. Deepcopy ensures that they remain the same for all molecules, even if modified in _calc_spectrum # We add all of the DICT_INPUT_ARGUMENTS values: kwargs_molecule.update(**dict_arguments) s_list.append( _calc_spectrum( wavenum_min=wavenum_min, wavenum_max=wavenum_max, wavelength_min=wavelength_min, wavelength_max=wavelength_max, Tgas=Tgas, Tvib=Tvib, Trot=Trot, pressure=pressure, # overpopulation=overpopulation, # now in dict_arguments molecule=molecule, # isotope=isotope, # now in dict_arguments # mole_fraction=mole_fraction, # now in dict_arguments path_length=path_length, # databank=databank, # now in dict_arguments medium=medium, wstep=wstep, broadening_max_width=broadening_max_width, optimization=optimization, name=name, use_cached=use_cached, verbose=verbose, mode=mode, **kwargs_molecule ) ) # Stage 4: merge all molecules and return return MergeSlabs(*s_list)