Beispiel #1
0
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
Beispiel #3
0
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
Beispiel #4
0
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
Beispiel #5
0
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)