Esempio n. 1
0
def expand_metadata(df, metadata):
    """Turn metadata from a float to a column

    For some reason metadata are sometimes not copied when a DataFrame is
    sliced or copied, even if they explicitely figure in the df._metadata
    attribute. Here we add them as column before such operations.

    Parameters
    ----------

    df: pandas DataFrame
        ...

    Returns
    -------

    None:
        df modified in place

    """

    for k in metadata:
        if __debug__ and k not in df._metadata:
            from radis.misc.debug import printdbg

            printdbg("WARNING. {0} not in _metadata: {1}".format(
                k, df._metadata))
        df[k] = getattr(df, k)
Esempio n. 2
0
def transfer_metadata(df1, df2, metadata):
    """Transfer metadata between a DataFrame df1 and df2

    For some reason metadata are sometimes not copied when a DataFrame is
    sliced or copied, even if they explicitely figure in the df._metadata
    attribute. Here we copy them back

    Parameters
    ----------

    df1: pandas DataFrame
        copy from df1

    df2: pandas DataFrame
        copy to df2
    """

    for k in metadata:
        if __debug__ and k not in df1._metadata:
            from radis.misc.debug import printdbg

            printdbg("WARNING. {0} not in _metadata: {1}".format(
                k, df1._metadata))
        if not hasattr(df2, k):
            assert k not in df1.columns  # point is not to copy columns!
            setattr(df2, k, getattr(df1, k))
Esempio n. 3
0
def resample(
    xspace,
    vector,
    xspace_new,
    k=1,
    ext="error",
    energy_threshold=1e-3,
    print_conservation=True,
):
    """Resample (xspace, vector) on a new space (xspace_new) of evenly distributed
    data and whose bounds are taken as the same as `xspace`.

    Uses spline interpolation to create the intermediary points. Number of points
    is the same as the initial xspace, times a resolution factor. Verifies energy
    conservation on the intersecting range at the end.


    Parameters
    ----------

    xspace: array
        space on which vector was generated

    vector: array
        quantity to resample

    resfactor: array
        xspace vector to resample on

    k: int
        order of spline interpolation. 3: cubic, 1: linear. Default 1.

    ext: 'error', 'extrapolate', 0, 1
        Controls the value returned for elements of xspace_new not in the interval
        defined by xspace. If 'error', raise a ValueError. If 'extrapolate', well,
        extrapolate. If '0' or 0, then fill with 0. If 1, fills with 1.
        Default 'error'.

    energy_threshold: float
        if energy conservation (integrals on the intersecting range) is above
        this threshold, raise an error. If None, dont check for energy conservation
        Default 1e-3 (0.1%)

    print_conservation: boolean
        if True, prints energy conservation


    Returns
    -------

    vector_new: array
        resampled vector on evenly spaced array. Number of element is conserved.

    Note that depending upon the from_space > to_space operation, sorting may
    be reversed.


    Examples
    --------

    Resample a :class:`~radis.spectrum.spectrum.Spectrum` radiance
    on an evenly spaced wavenumber space::

        w_nm, I_nm = s.get('radiance')
        w_cm, I_cm = resample_even(nm2cm(w_nm), I_nm)


    """

    if len(xspace) != len(vector):
        raise ValueError(
            "vector and xspace should have the same length. "
            + "Got {0}, {1}".format(len(vector), len(xspace))
        )

    # Check reversed (interpolation requires objects are sorted)
    if is_sorted(xspace):
        reverse = False
    elif is_sorted_backward(xspace):
        reverse = True
    else:
        raise ValueError("Resampling requires wavespace to be sorted. It is not!")

    if reverse:
        xspace = xspace[::-1]
        xspace_new = xspace_new[::-1]
        vector = vector[::-1]

    # translate ext in FITPACK syntax for splev
    if ext == "extrapolate":
        ext_fitpack = 0  # splev returns extrapolated value
    elif ext in [0, "0", 1, "1", nan, "nan"]:
        ext_fitpack = 1  # splev returns 0  (fixed in post-processing)
    elif ext == "error":
        ext_fitpack = 2  # splev raises ValueError
    else:
        raise ValueError("Unexpected value for `ext`: {0}".format(ext))

    if isnan(vector).sum() > 0:
        raise ValueError(
            "Resampled vector has {0} nans. Interpolation will fail".format(
                isnan(vector).sum()
            )
        )

    # Resample the slit function on the spectrum grid
    try:
        tck = splrep(xspace, vector, k=k)
    except ValueError:
        # Probably error on input data. Print it before crashing.
        print("\nValueError - Input data below:")
        print("-" * 5)
        print(xspace)
        print(vector)
        print("Check plot 101 too")
        import matplotlib.pyplot as plt

        plt.figure(101).clear()
        plt.plot(xspace, vector)
        plt.xlabel("xspace")
        plt.ylabel("vector")
        plt.title("ValueError")
        raise
    vector_new = splev(xspace_new, tck, ext=ext_fitpack)

    # ... get masks
    b = (xspace >= xspace_new.min()) * (xspace <= xspace_new.max())
    b_new = (xspace_new >= xspace.min()) * (xspace_new <= xspace.max())

    # fix filling for out of boundary values
    if ext in [1, "1"]:
        vector_new[~b_new] = 1
        if __debug__:
            printdbg(
                "Filling with 1 on w<{0}, w>{1} ({2} points)".format(
                    xspace.min(), xspace.max(), (1 - b_new).sum()
                )
            )
    elif ext in [nan, "nan"]:
        vector_new[~b_new] = nan
        if __debug__:
            printdbg(
                "Filling with nans on w<{0}, w>{1} ({2} points)".format(
                    xspace.min(), xspace.max(), (1 - b_new).sum()
                )
            )

    # Check energy conservation:

    # ... calculate energy
    energy0 = abs(trapz(vector[b], x=xspace[b]))
    energy_new = abs(trapz(vector_new[b_new], x=xspace_new[b_new]))
    if energy_new == 0:  # deal with particular case of energy = 0
        if energy0 == 0:
            energy_ratio = 1
        else:
            energy_ratio = 0
    else:  # general case
        energy_ratio = energy0 / energy_new
    if energy_threshold:
        if abs(energy_ratio - 1) > energy_threshold:
            import matplotlib.pyplot as plt

            plt.figure(101).clear()
            plt.plot(xspace, vector, "-ok", label="original")
            plt.plot(xspace_new, vector_new, "-or", label="resampled")
            plt.xlabel("xspace")
            plt.ylabel("vector")
            plt.legend()
            raise ValueError(
                "Error in resampling: "
                + "energy conservation ({0:.5g}%) below tolerance level ({1:.5g}%)".format(
                    (1 - energy_ratio) * 100, energy_threshold * 100
                )
                + ". Check graph 101. "
                + "Increasing energy_threshold is possible but not recommended"
            )
    if print_conservation:
        print("Resampling - Energy conservation: {0:.5g}%".format(energy_ratio * 100))

    # Reverse again
    if reverse:
        # xspace_new = xspace_new[::-1]
        vector_new = vector_new[::-1]

    return vector_new
Esempio n. 4
0
    def get(self, conditions='', **kwconditions):
        '''   Returns a list of spectra that match given conditions

        Parameters
        ----------

        database: list of Spectrum objects
            the database

        conditions: str
            a list of conditions
            >>> get('Tvib==3000 & Trot==1500')

        kwconditions: dict
            an unfolded dict of conditions
            >>> get(Tvib=3000, Trot=1500)
            
        Other Parameters
        ----------------
        
        inplace: boolean
            if True, return the actual object in the database. Else, return
            copies. Default False

        Examples
        --------

        >>> spec_list = db.get('Tvib==3000 & Trot==1300')
        
        or
        
        >>> spec_list = db.get(Tvib=3000, Trot=1300)

        
        See Also
        --------
        
        :meth:`~radis.tools.database.SpecDatabase.get_unique`
        :meth:`~radis.tools.database.SpecDatabase.get_closest`
        :meth:`~radis.tools.database.SpecDatabase.items`
        
        '''

        # Test inputs
        for (k, _) in kwconditions.items():
            if not k in self.df.columns:
                raise ValueError(
                    '{0} not a correct condition name. Use one of: {1}'.format(
                        k, self.df.columns))
        if len(self.df) == 0:
            warn('Empty database')
            return []

        inplace = kwconditions.pop('inplace',
                                   False)  # type: bool, default False

        # Unique condition method
        if conditions != '' and kwconditions != {}:
            raise ValueError(
                "Please choose one of the two input format (str or dict) exclusively"
            )

        if conditions == '' and kwconditions == {}:
            return list(self.df['Spectrum'])

        # Find Spectrum that match conditions
        if conditions != '':  # ... with input conditions query directly
            dg = self.df.query(conditions)
        else:  # ... first write input conditions query
            query = []
            for (k, v) in kwconditions.items():
                if isinstance(v, string_types):
                    query.append("{0} == '{1}'".format(k, v))
                else:
                    #                    query.append('{0} == {1}'.format(k,v))
                    query.append('{0} == {1}'.format(k, v.__repr__()))
                    # ... for som reason {1}.format() would remove some digit
                    # ... to floats in Python2. Calling .__repr__() keeps
                    # ... the correct format, and has no other consequences as far
                    # ... as I can tell

            # There is a limitation in numpy: a max of 32 arguments is required.
            # Below we write a workaround when the Spectrum has more than 32 conditions
            if len(query) < 32:
                query = ' & '.join(query)
                if __debug__: printdbg('Database query: {0}'.format(query))
                dg = self.df.query(query)
            else:
                # cut in <32-long parts
                N = len(query) // 32 + 1
                querypart = ' & '.join(query[::N])
                dg = self.df.query(querypart)
                for i in range(1, N + 1):
                    querypart = ' & '.join(query[i::N])
                    if __debug__:
                        printdbg('Database query: {0}'.format(querypart))
                    dg = dg.query(querypart)

        out = list(dg['Spectrum'])

        if not inplace:
            out = [s.copy() for s in out]

        return out
Esempio n. 5
0
def MergeSlabs(*slabs, **kwargs):
    # type: (*Spectrum, **dict) -> Spectrum
    r"""Combines several slabs into one. Useful to calculate multi-gas slabs.
    Linear absorption coefficient is calculated as the sum of all linear absorption
    coefficients, and the RTE is recalculated to get the total radiance.
    You can also simply use::

        s1//s2
    Merged spectrum ``1+2`` is calculated with Eqn (4.3) of the [RADIS-2018]_ article,
    generalized to N slabs :

    .. math::

        j_{\lambda, 1+2} = j_{\lambda, 1} + j_{\lambda, 2}

        k_{\lambda, 1+2} = k_{\lambda, 1} + k_{\lambda, 2}

    where

    .. math:: j_{\lambda}, k_{\lambda}

    are the emission coefficient and absorption coefficient of the two slabs ``1`` and ``2``.
    Emission and absorption coefficients are calculated if not given in the
    initial slabs (if possible).

    Parameters
    ----------
    slabs: list of Spectra, each representing a slab
        ``path_length`` must be given in Spectrum conditions, and equal for all
        spectra.

        line-of-sight::

            slabs
                        [0]        \====
            light       [1]  ->     )===  observer
                        [n]        /====

    Other Parameters
    ----------------
    kwargs input:
    resample: ``'never'``, ``'intersect'``, ``'full'``
        what to do when spectra have different wavespaces:

        - If ``'never'``, raises an error
        - If ``'intersect'``, uses the intersection of all ranges, and resample
          spectra on the most resolved wavespace.
        - If ``'full'``, uses the overlap of all ranges, resample spectra on the
          most resolved wavespace, and fill missing data with 0 emission and 0
          absorption

        Default ``'never'``
    out: ``'transparent'``, ``'nan'``, ``'error'``
        what to do if resampling is out of bounds:

        - ``'transparent'``: fills with transparent medium.
        - ``'nan'``: fills with nan.
        - ``'error'``: raises an error.

        Default ``'nan'``
    optically_thin: boolean
        if ``True``, merge slabs in optically thin mode. Default ``False``
    verbose: boolean
        if ``True``, print messages and warnings. Default ``False``
    modify_inputs: False
        if ``True``, slabs are modified directly when they are resampled. This
        avoids making a copy so is slightly faster. Default ``False``.

    Returns
    -------
    Spectrum object representing total emission and total transmittance as
    observed at the output. Conditions and units are transported too,
    unless there is a mismatch then conditions are dropped (and units mismatch
    raises an error because it doesnt make sense)

    Examples
    --------
    Merge two spectra calculated with different species (physically correct
    only if broadening coefficients dont change much)::
        from radis import calc_spectrum, MergeSlabs
        s1 = calc_spectrum(...)
        s2 = calc_spectrum(...)
        s3 = MergeSlabs(s1, s2)
    The last line is equivalent to::

        s3 = s1//s2
    Load a spectrum precalculated on several partial spectral ranges, for a same
    molecule (i.e, partial spectra are optically thin on the rest of the spectral
    range)::
        from radis import load_spec, MergeSlabs
        spectra = []
        for f in ['spec1.spec', 'spec2.spec', ...]:
            spectra.append(load_spec(f))
        s = MergeSlabs(*spectra, resample='full', out='transparent')
        s.update()   # Generate missing spectral quantities
        s.plot()

    See Also
    --------
    :func:`~radis.los.slabs.SerialSlabs`

    See more examples in :ref:`Line-of-Sight module <label_los_index>`

    """

    # Deprecation warnings
    if "resample_wavespace" in kwargs:
        warn(
            DeprecationWarning(
                "'resample_wavespace' replaced with 'resample'"))
        kwargs["resample"] = kwargs.pop("resample_wavespace")
    if "out_of_bounds" in kwargs:
        warn(DeprecationWarning("'out_of_bounds' replaced with 'out'"))
        kwargs["out"] = kwargs.pop("out_of_bounds")

    # Check inputs, get defaults
    # inputs (Python 2 compatible)
    resample_wavespace = kwargs.pop("resample", "never")  # default 'never'
    out_of_bounds = kwargs.pop("out", "nan")  # default 'nan'
    optically_thin = kwargs.pop("optically_thin", False)  # default False
    verbose = kwargs.pop("verbose", False)  # type: bool
    kwargs.pop("debug", False)  # type: bool
    modify_inputs = kwargs.pop("modify_inputs", False)  # type: bool
    if len(kwargs) > 0:
        raise ValueError("Unexpected input: {0}".format(list(kwargs.keys())))

    # Check inputs
    if resample_wavespace not in ["never", "intersect", "full"]:
        raise ValueError("'resample' should be one of: {0}".format(", ".join(
            ["never", "intersect", "full"])))

    if len(slabs) == 0:
        raise ValueError("Empty list of slabs")

    elif len(slabs) == 1:
        if not is_spectrum(slabs[0]):
            raise TypeError(
                "MergeSlabs takes an unfolded list of Spectrum as " +
                "argument: (got {0})".format(type(slabs[0])))
        return slabs[0]

    else:  # calculate serial slabs

        slabs = list(slabs)

        #        # Check all items are valid Spectrum objects
        for s in slabs:
            _check_valid(s)

        # Check all path_lengths are defined and they exist
        try:
            path_lengths = [s.conditions["path_length"] for s in slabs]
        except KeyError:
            raise ValueError(
                "path_length must be defined for all slabs in MergeSlabs. " +
                "Set it with `s.conditions['path_length']=`. ")
        if not all([L == path_lengths[0] for L in path_lengths[1:]]):
            raise ValueError(
                "path_length must be equal for all MergeSlabs inputs" +
                "  (got {0})".format(path_lengths))

        # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1')
        waveunit = slabs[0].get_waveunit()
        # Make all our slabs copies with the same wavespace range
        # (note: wavespace range may be different for different quantities, but
        # equal for all slabs)
        slabs = resample_slabs(waveunit, resample_wavespace, out_of_bounds,
                               modify_inputs, *slabs)
        w_noconv = slabs[0]._get_wavespace()

        # %%

        # Get conditions of the Merged spectrum
        conditions = slabs[0].conditions
        conditions["waveunit"] = waveunit
        cond_units = slabs[0].cond_units
        units0 = slabs[0].units
        # Define conditions as intersection of everything (N/A if unknown)
        # ... this will only keep intensive parameters (same for all)
        for s in slabs[1:]:
            conditions = intersect(conditions, s.conditions)
            cond_units = intersect(cond_units, s.cond_units)
            # units = intersect(units0, s.units)  # we're actually using [slabs0].units insteads
        # ... Add extensive parameters
        for cond in ["molecule"]:
            if in_all(cond, [s.conditions for s in slabs]):
                conditions[cond] = set([s.conditions[cond] for s in slabs])

        # %% Get quantities that should be calculated
        # Try to keep all the quantities of the initial slabs:
        requested = merge_lists([s.get_vars() for s in slabs])
        recompute = requested[:]  # copy
        if "radiance_noslit" in requested and not optically_thin:
            recompute.append("emisscoeff")
            recompute.append("abscoeff")
        if "abscoeff" in recompute and "path_length" in conditions:
            recompute.append("absorbance")
            recompute.append("transmittance_noslit")

        # To make it easier, we start from abscoeff and emisscoeff of all slabs
        # Let's recompute them all
        # TODO: if that changes the initial Spectra, maybe we should just work on copies
        for s in slabs:
            if "abscoeff" in recompute and not "abscoeff" in list(s._q.keys()):
                s.update("abscoeff", verbose=False)
                # that may crash if Spectrum doesnt have the correct inputs.
                # let update() handle that
            if "emisscoeff" in recompute and not "emisscoeff" in list(
                    s._q.keys()):
                s.update("emisscoeff", verbose=False)
                # same

        # %% Calculate total emisscoeff and abscoeff
        added = {}

        # ... absorption coefficient (cm-1)
        if "abscoeff" in recompute:
            # TODO: deal with all cases
            if __debug__:
                printdbg("... merge: calculating abscoeff k=sum(k_i)")
            abscoeff_eq = np.sum(
                [
                    s.get("abscoeff", wunit=waveunit,
                          Iunit=units0["abscoeff"])[1] for s in slabs
                ],
                axis=0,
            )
            assert len(w_noconv) == len(abscoeff_eq)
            added["abscoeff"] = (w_noconv, abscoeff_eq)

        # ... emission coefficient
        if "emisscoeff" in recompute:
            if __debug__:
                printdbg("... merge: calculating emisscoeff j=sum(j_i)")
            emisscoeff_eq = np.sum(
                [
                    s.get("emisscoeff",
                          wunit=waveunit,
                          Iunit=units0["emisscoeff"])[1] for s in slabs
                ],
                axis=0,
            )
            assert len(w_noconv) == len(emisscoeff_eq)
            added["emisscoeff"] = (w_noconv, emisscoeff_eq)

        # name
        name = "//".join([s.get_name() for s in slabs])

        # TODO: check units are consistent in all slabs inputs
        s = Spectrum(
            quantities=added,
            conditions=conditions,
            cond_units=cond_units,
            units=units0,
            name=name,
        )

        # %% Calculate all quantities from emisscoeff and abscoeff

        if "emissivity_noslit" in requested and (
                "thermal_equilibrium" not in s.conditions
                or s.is_at_equilibrium() != True):
            requested.remove("emissivity_noslit")
            if __debug__:
                printdbg(
                    "... merge: all slabs are not proven to be at equilibrium. "
                    + "Emissivity was not calculated")

        # Add the rest of the spectral quantities afterwards:
        s.update(
            [k for k in requested if k not in ["emisscoeff", "abscoeff"]],
            optically_thin=optically_thin,
            verbose=verbose,
        )

        return s
Esempio n. 6
0
def MergeSlabs(*slabs, **kwargs):
    ''' Combines several slabs into one. Useful to calculate multi-gas slabs. 
    Linear absorption coefficient is calculated as the sum of all linear absorption
    coefficients, and the RTE is recalculated to get the total radiance

    
    Parameters    
    ----------
    
    slabs: list of Spectra, each representing a slab
        If given in conditions, all path_length have to be same

    Other Parameters
    ----------------
    
    kwargs input:

    resample_wavespace: 'never', 'intersect', 'full'
        what to do when spectra have different wavespaces. 
        - If 'never', raises an error
        - If 'intersect', uses the intersection of all ranges, and resample
        spectra on the most resolved wavespace. 
        - If 'full', uses the overlap of all ranges, resample spectra on the 
        most resolved wavespace, and fill missing data with 0 emission and 0
        absorption
        Default 'never'
        
    out_of_bounds: 'transparent', 'nan', 'error'
        what to do if resampling is out of bounds. 'transparent': fills with 
        transparent medium. 'nan': fills with nan. 'error': raises an error. 
        Default 'nan'

    optically_thin: boolean
        if True, merge slabs in optically thin mode. Default False 
                            
    verbose: boolean
        if True, print messages and warnings. Default True
        

    Returns
    -------
    
    Spectrum object representing total emission and total transmittance as 
    observed at the output. Conditions and units are transported too,
    unless there is a mismatch then conditions are dropped (and units mismatch
    raises an error because it doesnt make sense)
    
    
    Examples
    --------
    
    Merge two spectra calculated with different species (true only if broadening
    coefficient dont change much):
    
        >>> from radis import calc_spectrum, MergeSlabs
        >>> s1 = calc_spectrum(...)
        >>> s2 = calc_spectrum(...)
        >>> s3 = MergeSlabs(s1, s2)
        
    Load a spectrum precalculated on several partial spectral ranges, for a same 
    molecule (i.e, partial spectra are optically thin on the rest of the spectral 
    range)
    
        >>> from radis import load_spec, MergeSlabs
        >>> spectra = []
        >>> for f in ['spec1.spec', 'spec2.spec', ...]:
        >>>     spectra.append(load_spec(f))
        >>> s = MergeSlabs(*spectra, resample_wavespace='full', out_of_bounds='transparent')
        >>> s.update()   # Generate missing spectral quantities
        >>> s.plot()
        
        
    See Also
    --------
    
    :func:`~radis.los.slabs.SerialSlabs`
    '''

    # inputs (Python 2 compatible)
    resample_wavespace = kwargs.pop('resample_wavespace',
                                    'never')  # default 'never'
    out_of_bounds = kwargs.pop('out_of_bounds', 'nan')  # default 'nan'
    optically_thin = kwargs.pop('optically_thin', False)  # default False
    verbose = kwargs.pop('verbose', True)  # type: bool
    debug = kwargs.pop('debug', False)  # type: bool
    if len(kwargs) > 0:
        raise ValueError('Unexpected input: {0}'.format(list(kwargs.keys())))

    # Check inputs
    if resample_wavespace not in ['never', 'intersect', 'full']:
        raise ValueError("resample_wavespace should be one of: {0}".format(
            ', '.join(['never', 'intersect', 'full'])))

    if len(slabs) == 0:
        raise ValueError('Empty list of slabs')

    elif len(slabs) == 1:
        if not is_spectrum(slabs[0]):
            raise TypeError('MergeSlabs takes an unfolded list of Spectrum as '+\
                                 'argument: (got {0})'.format(type(slabs[0])))
        return slabs[0]

    else:  # calculate serial slabs

        slabs = list(slabs)

        #        # Check all items are valid Spectrum objects
        for s in slabs:
            _check_valid(s)

        # Just check all path_lengths are the same if they exist
        path_lengths = [
            s.conditions['path_length'] for s in slabs
            if 'path_length' in s.conditions
        ]
        if not all([L == path_lengths[0] for L in path_lengths[1:]]):
            raise ValueError('path_length must be equal for all MergeSlabs inputs'+\
                             '  (got {0})'.format(path_lengths))

        # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1')
        waveunit = slabs[0].get_waveunit()
        # Make all our slabs copies with the same wavespace range
        # (note: wavespace range may be different for different quantities, but
        # equal for all slabs)
        slabs = resample_slabs(waveunit, resample_wavespace, out_of_bounds,
                               *slabs)
        w_noconv = slabs[0]._get_wavespace()

        # %%

        # Get conditions
        conditions = slabs[0].conditions
        conditions['waveunit'] = waveunit
        cond_units = slabs[0].cond_units
        units0 = slabs[0].units
        for s in slabs[1:]:
            conditions = intersect(conditions, s.conditions)
            cond_units = intersect(cond_units, s.cond_units)
            #units = intersect(units0, s.units)  # we're actually using [slabs0].units insteads

        # %% Get quantities that should be calculated

        requested = merge_lists([s.get_vars() for s in slabs])
        recompute = requested[:]  #  copy
        if ('radiance_noslit' in requested and not optically_thin):
            recompute.append('emisscoeff')
            recompute.append('abscoeff')
        if 'abscoeff' in recompute and 'path_length' in conditions:
            recompute.append('absorbance')
            recompute.append('transmittance_noslit')

        # To make it easier, we start from abscoeff and emisscoeff of all slabs
        # Let's recompute them all
        # TODO: if that changes the initial Spectra, maybe we should just work on copies
        for s in slabs:
            if 'abscoeff' in recompute and not 'abscoeff' in list(s._q.keys()):
                s.update('abscoeff')
                # that may crash if Spectrum doesnt have the correct inputs.
                # let update() handle that
            if 'emisscoeff' in recompute and not 'emisscoeff' in list(
                    s._q.keys()):
                s.update('emisscoeff')
                # same

        path_length = conditions['path_length']

        # %% Calculate new quantites from emisscoeff and abscoeff
        # TODO: rewrite all of the above with simple calls to .update()
        added = {}

        # ... absorption coefficient (cm-1)
        if 'abscoeff' in recompute:
            #TODO: deal with all cases
            if __debug__:
                printdbg('... merge: calculating abscoeff k=sum(k_i)')
            abscoeff_eq = np.sum([
                s.get('abscoeff', wunit=waveunit, Iunit=units0['abscoeff'])[1]
                for s in slabs
            ],
                                 axis=0)
            assert len(w_noconv) == len(abscoeff_eq)
            added['abscoeff'] = (w_noconv, abscoeff_eq)

        if 'absorbance' in recompute:
            if 'abscoeff' in added:
                if __debug__:
                    printdbg('... merge: calculating absorbance A=k*L')
                _, abscoeff_eq = added['abscoeff']
                absorbance_eq = abscoeff_eq * path_length
            else:
                raise NotImplementedError('recalculate abscoeff first')
            added['absorbance'] = (w_noconv, absorbance_eq)

        # ... transmittance
        if 'transmittance_noslit' in recompute:
            if 'absorbance' in added:
                if __debug__:
                    printdbg('... merge: calculating transmittance T=exp(-A)')
                _, absorbance_eq = added['absorbance']
                transmittance_noslit_eq = exp(-absorbance_eq)
            else:
                raise NotImplementedError('recalculate absorbance first')
            added['transmittance_noslit'] = (w_noconv, transmittance_noslit_eq)

        # ... emission coefficient
        if 'emisscoeff' in recompute:
            emisscoeff_eq = np.zeros_like(w_noconv)
            for i, s in enumerate(slabs):
                # Manual loop in case all Slabs dont have the same keys
                # Could also do a slab.update() first then sum emisscoeff directly
                if 'emisscoeff' in list(s._q.keys()):
                    if __debug__:
                        printdbg('... merge: calculating emisscoeff: j+=j_i')
                    _, emisscoeff = s.get('emisscoeff',
                                          wunit=waveunit,
                                          Iunit=units0['emisscoeff'])
                elif optically_thin and 'radiance_noslit' in list(s._q.keys()):
                    if __debug__:                        printdbg('... merge: calculating emisscoeff: j+=I_i/L '+\
                          '(optically thin case)')
                    _, I = s.get('radiance_noslit',
                                 wunit=waveunit,
                                 Iunit=units0['radiance_noslit'])
                    emisscoeff = I / path_length
                    emisscoeff_eq += emisscoeff
                else:
                    wI, I = s.get('radiance_noslit',
                                  wunit=waveunit,
                                  Iunit=units0['radiance_noslit'])
                    if __debug__:
                        printdbg(
                            '... merge: calculating emisscoeff j+=[k*I/(1-T)]_i)'
                        )
                    try:
                        wT, T = s.get('transmittance_noslit',
                                      wunit=waveunit,
                                      Iunit=units0['transmittance_noslit'])
                        wk, k = s.get('abscoeff',
                                      wunit=waveunit,
                                      Iunit=units0['abscoeff'])
                    except KeyError:
                        raise KeyError('Need transmittance_noslit and abscoeff to '+\
                                       'recompute emission coefficient')
                    b = (T == 1)  # optically thin mask
                    emisscoeff = np.zeros_like(T)
                    emisscoeff[b] = I[b] / path_length  # optically thin case
                    emisscoeff[~b] = I[~b] / (1 - T[~b]) * k[~b]
                emisscoeff_eq += emisscoeff
            added['emisscoeff'] = (w_noconv, emisscoeff_eq)

        # ... derive global radiance (result of analytical RTE solving)
        if 'radiance_noslit' in recompute:
            if 'emisscoeff' in added and optically_thin:
                if __debug__:
                    printdbg(
                        '... merge: recalculating radiance_noslit I=j*L(optically_thin)'
                    )
                (_, emisscoeff_eq) = added['emisscoeff']
                radiance_noslit_eq = emisscoeff_eq * path_length  # optically thin
            elif ('emisscoeff' in added and 'transmittance_noslit' in added
                  and 'abscoeff' in added):
                if __debug__:
                    printdbg(
                        '... merge: recalculating radiance_noslit I=j/k*(1-T)')
                (_, emisscoeff_eq) = added['emisscoeff']
                (_, abscoeff_eq) = added['abscoeff']
                (_, transmittance_noslit_eq) = added['transmittance_noslit']
                b = (abscoeff_eq == 0)  # optically thin mask
                radiance_noslit_eq = np.zeros_like(emisscoeff_eq)
                radiance_noslit_eq[
                    b] = emisscoeff_eq[b] * path_length  # optically thin limit
                radiance_noslit_eq[~b] = emisscoeff_eq[~b] / abscoeff_eq[
                    ~b] * (1 - transmittance_noslit_eq[~b])
            elif optically_thin:
                if __debug__:
                    printdbg(
                        '... merge: recalculating radiance_noslit I=sum(I_i) (optically thin)'
                    )
                radiance_noslit_eq = np.zeros_like(w_noconv)
                for s in slabs:
                    if 'radiance_noslit' in list(s._q.keys()):
                        radiance_noslit_eq += s.get(
                            'radiance_noslit',
                            wunit=waveunit,
                            Iunit=units0['radiance_noslit'])[1]
                    else:
                        raise KeyError('Need radiance_noslit for all slabs to '+\
                                       'recalculate for the MergeSlab (could also '+\
                                       'get it from emisscoeff but not implemented)')
            else:
                if optically_thin:
                    raise ValueError('Missing data to recalculate radiance_noslit'+\
                                     '. Try optically_thin mode?')
                else:
                    raise ValueError(
                        'Missing data to recalculate radiance_noslit')
            added['radiance_noslit'] = (w_noconv, radiance_noslit_eq)

        # ... emissivity no slit
        if 'emissivity_noslit' in requested:
            added['emissivity_noslit'] = w_noconv, np.ones_like(
                w_noconv) * np.nan
#            if verbose:
#                warn('emissivity dropped during MergeSlabs')
# Todo: deal with equilibrium cases?

# Store output
        quantities = {}
        for k in requested:
            quantities[k] = added[k]

        # name
        name = '//'.join([s.get_name() for s in slabs])

        # TODO: check units are consistent in all slabs inputs
        return Spectrum(quantities=quantities,
                        conditions=conditions,
                        cond_units=cond_units,
                        units=units0,
                        name=name)
Esempio n. 7
0
def get_slit_function(slit_function,
                      unit='nm',
                      norm_by='area',
                      shape='triangular',
                      center_wavespace=None,
                      return_unit='same',
                      wstep=None,
                      plot=False,
                      resfactor=2,
                      *args,
                      **kwargs):
    ''' Import or generate slit function in correct wavespace
    Give a file path to import, or a float / tuple to generate arbitrary shapes

    Warning with units: read about unit and return_unit parameters.

    See :meth:`~radis.spectrum.spectrum.Spectrum.apply_slit` and 
    :func:`~radis.tools.slit.convolve_with_slit` for more info

    
    Parameters    
    ----------

    slit_function: tuple, or str
        If float:
            generate slit function with FWHM of `slit_function` (in `unit`)
        If .txt file:
            import experimental slit function (in `unit`): format must be 2-columns
            with wavelengths and intensity (doesn't have to be normalized)

    unit: 'nm' or 'cm-1'
        unit of slit_function FWHM, or unit of imported file

    norm_by: 'area', 'max', or None
        how to normalize. `area` conserves energy. With `max` the slit is normalized
        so that its maximum is one (that is what is done in Specair: it changes
        the outptut spectrum unit, e.g. from 'mW/cm2/sr/µm' to 'mW/cm2/sr')
        None doesnt normalize. Default 'area'

    shape: 'triangular', 'trapezoidal', 'gaussian'
        which shape to use when generating a slit. Default 'triangular'

    center_wavespace: float, or None
        center of slit when generated (in unit). Not used if slit is imported.

    return_unit: 'nm', 'cm-1', or 'same'
        if not 'same', slit is converted to the given wavespace.

    wstep: float
        which discretization step to use (in return_unit) when generating a slit
        function. Not used if importing


    Other Parameters
    ----------------

    resfactor: int
        resolution increase when resampling from nm to cm-1, or the other way
        round. Default 2.

    energy_threshold: float
         tolerance fraction. Only used when importing experimental slit as the
         theoretical slit functions are directly generated in spectrum wavespace
         Default 1e-3 (0.1%)
         

    Returns
    -------
    
    wslit, Islit: array
        wslit is in `return_unit` . Islit is normalized according to  `norm_by`
         
        
    Examples
    --------
    
    >>> wslit, Islit = get_slit_function(1, 'nm', shape='triangular', 
    center_wavespace=600, wstep=0.01)     
    
    Returns a triangular slit function of FWHM = 1 nm, centered on 600 nm, with
    a step of 0.01 nm
    
    >>> wslit, Islit = get_slit_function(1, 'nm', shape='triangular', 
    center_wavespace=600, return_unit='cm-1', wstep=0.01)   
    
    Returns a triangular slit function expressed in cm-1, with a FWHM = 1 nm 
    (converted in equivalent width in cm-1 at 600 nm), centered on 600 nm, with 
    a step of 0.01 cm-1 (!)   
    

    Notes
    -----

    In norm_by 'max' mode, slit is normalized by slit max. In RADIS, this is done
    in the spectrum wavespace (to avoid errors that would be caused by interpolating
    the spectrum). 
    
    A problem arise if the spectrum wavespace is different from the slit wavespace:
    typically, slit is in 'nm' but a spectrum calculated by RADIS is stored in 
    'cm-1': in that case, the convoluted spectrum is multiplied by /int(Islit*dν)
    instead of /int(Islit*dλ). The output unit is then [radiance]*[spec_unit]
    instead of [radiance]*[slit_unit], i.e, typically, [mW/cm2/sr/nm]*[cm-1]
    instead of [mW/cm2/sr/nm]*[nm]=[mW/cm2/sr]
    
    While this remains true if the units are taken into account, this is not the
    expected behaviour. For instance, Specair users are used to having a FWHM(nm) 
    factor between spectra convolved with slit normalized by max and slit normalized
    by area.
    
    The norm_by='max' behaviour adds a correction factor
    `/int(Islit*dλ)/int(Islit*dν)` to maintain an output spectrum in [radiance]*[slit_unit]
    
    See Also
    --------
    
    :meth:`~radis.spectrum.spectrum.Spectrum.apply_slit`, 
    :func:`~radis.tools.slit.convolve_with_slit`
    
    '''

    if 'waveunit' in kwargs:
        assert return_unit == 'same'  # default
        return_unit = kwargs.pop('waveunit')
        warn(DeprecationWarning('waveunit renamed return_unit'))
    if 'slit_unit' in kwargs:
        assert unit == 'nm'  # default
        unit = kwargs.pop('slit_unit')
        warn(DeprecationWarning('slit_unit renamed unit'))

    energy_threshold = kwargs.pop('energy_threshold', 1e-3)  # type: float

    # tolerance fraction
    # when resampling (only used in experimental slit as the)
    # theoretical slit functions are directly generated in
    # spectrum wavespace

    def check_input_gen():
        if center_wavespace is None:
            raise ValueError('center_wavespace has to be given when generating '+\
                             'slit function')
        if wstep is None:
            raise ValueError('wstep has to be given when generating '+\
                             'slit function')

    # Cast units
    if return_unit == 'same':
        return_unit = unit
    unit = cast_waveunit(unit)
    return_unit = cast_waveunit(return_unit)
    scale_slit = 1  # in norm_by=max mode, used to keep units in [Iunit]*return_unit in [Iunit]*unit
    # not used in norm_by=area mode

    # First get the slit in return_unit space
    if is_float(slit_function
                ):  # Generate slit function (directly in return_unit space)

        check_input_gen()

        # ... first get FWHM in return_unit  (it is in `unit` right now)
        FWHM = slit_function
        if return_unit == 'cm-1' and unit == 'nm':
            # center_wavespace ~ nm, FWHM ~ nm
            FWHM = dnm2dcm(FWHM, center_wavespace)  # wavelength > wavenumber
            center_wavespace = nm2cm(center_wavespace)
            if norm_by == 'max':
                scale_slit = slit_function / FWHM  # [unit/return_unit]
        elif return_unit == 'nm' and unit == 'cm-1':
            # center_wavespace ~ cm-1, FWHM ~ cm-1
            FWHM = dcm2dnm(FWHM, center_wavespace)  # wavenumber > wavelength
            center_wavespace = cm2nm(center_wavespace)
            if norm_by == 'max':
                scale_slit = slit_function / FWHM  # [unit/return_unit]
        else:
            pass  # correct unit already
        # Now FWHM is in 'return_unit'

        # ... now, build it (in our wavespace)
        if __debug__:
            printdbg(
                'get_slit_function: {0} FWHM {1:.2f}{2}, center {3:.2f}{2}, norm_by {4}'
                .format(shape, FWHM, return_unit, center_wavespace, norm_by))

        if shape == 'triangular':
            wslit, Islit = triangular_slit(FWHM,
                                           wstep,
                                           center=center_wavespace,
                                           bplot=plot,
                                           norm_by=norm_by,
                                           waveunit=return_unit,
                                           scale=scale_slit,
                                           *args,
                                           **kwargs)

        # Insert other slit shapes here
        # ...

        elif shape == 'gaussian':
            wslit, Islit = gaussian_slit(FWHM,
                                         wstep,
                                         center=center_wavespace,
                                         bplot=plot,
                                         norm_by=norm_by,
                                         waveunit=return_unit,
                                         scale=scale_slit,
                                         *args,
                                         **kwargs)

        elif shape == 'trapezoidal':
            raise TypeError(
                'A (top, base) tuple must be given with a trapezoidal slit')

        else:
            raise TypeError(
                'Slit function ({0}) not in known slit shapes: {1}'.format(
                    shape, SLIT_SHAPES))

    elif isinstance(slit_function, tuple):

        check_input_gen()

        try:
            top, base = slit_function
        except:
            raise TypeError(
                'Wrong format for slit function: {0}'.format(slit_function))
        if shape == 'trapezoidal':
            pass
        elif shape == 'triangular':  # it's the default
            warn(
                'Triangular slit given with a tuple: we used trapezoidal slit instead'
            )
            shape = 'trapezoidal'
        else:
            raise TypeError(
                'A (top, base) tuple must be used with a trapezoidal slit')

        # ... first get FWHM in our wavespace unit
        if return_unit == 'cm-1' and unit == 'nm':
            # center_wavespace ~ nm, FWHM ~ nm
            top = dnm2dcm(top, center_wavespace)  # wavelength > wavenumber
            base = dnm2dcm(base, center_wavespace)  # wavelength > wavenumber
            center_wavespace = nm2cm(center_wavespace)
            if norm_by == 'max':
                scale_slit = sum(slit_function) / (top + base
                                                   )  # [unit/return_unit]
        elif return_unit == 'nm' and unit == 'cm-1':
            # center_wavespace ~ cm-1, FWHM ~ cm-1
            top = dcm2dnm(top, center_wavespace)  # wavenumber > wavelength
            base = dcm2dnm(base, center_wavespace)  # wavenumber > wavelength
            center_wavespace = cm2nm(center_wavespace)
            if norm_by == 'max':
                scale_slit = sum(slit_function) / (top + base
                                                   )  # [unit/return_unit]
        else:
            pass  # correct unit already

        FWHM = (top + base) / 2

        # ... now, build it (in our wavespace)
        if __debug__:
            printdbg(
                'get_slit_function: {0}, FWHM {1:.2f}{2}, center {3:.2f}{2}, norm_by {4}'
                .format(shape, FWHM, return_unit, center_wavespace, norm_by))

        wslit, Islit = trapezoidal_slit(top,
                                        base,
                                        wstep,
                                        center=center_wavespace,
                                        bplot=plot,
                                        norm_by=norm_by,
                                        waveunit=return_unit,
                                        scale=scale_slit,
                                        *args,
                                        **kwargs)

    elif isinstance(slit_function, string_types):  # import it
        if __debug__:
            printdbg(
                'get_slit_function: {0} in {1}, norm_by {2}, return in {3}'.
                format(slit_function, unit, norm_by, return_unit))
        wslit, Islit = import_experimental_slit(
            slit_function,
            norm_by=norm_by,  # norm is done later anyway
            waveunit=unit,
            bplot=False,  # we will plot after resampling
            *args,
            **kwargs)
        # ... get unit
        # Normalize
        if norm_by == 'area':  # normalize by the area
            #        I_slit /= np.trapz(I_slit, x=w_slit)
            Iunit = '1/{0}'.format(unit)
        elif norm_by == 'max':  # set maximum to 1
            Iunit = '1'
        elif norm_by is None:
            Iunit = None
        else:
            raise ValueError(
                'Unknown normalization type: `norm_by` = {0}'.format(norm_by))

        # ... check it looks correct
        unq, counts = np.unique(wslit, return_counts=True)
        dup = counts > 1
        if dup.sum() > 0:
            raise ValueError(
                'Not all wavespace points are unique: slit function ' +
                'format may be wrong. Duplicates for w={0}'.format(unq[dup]))

        # ... resample if needed
        if return_unit == 'cm-1' and unit == 'nm':  # wavelength > wavenumber
            wold, Iold = wslit, Islit
            wslit, Islit = resample_even(nm2cm(wslit),
                                         Islit,
                                         resfactor=resfactor,
                                         energy_threshold=energy_threshold,
                                         print_conservation=True)
            scale_slit = trapz(Iold, wold) / trapz(Islit,
                                                   wslit)  # [unit/return_unit]
            renormalize = True
        elif return_unit == 'nm' and unit == 'cm-1':  # wavenumber > wavelength
            wold, Iold = wslit, Islit
            wslit, Islit = resample_even(cm2nm(wslit),
                                         Islit,
                                         resfactor=resfactor,
                                         energy_threshold=energy_threshold,
                                         print_conservation=True)
            scale_slit = trapz(Iold, wold) / trapz(Islit,
                                                   wslit)  # [unit/return_unit]
            renormalize = True
        else:  # return_unit == unit
            renormalize = False
        # Note: if wstep dont match with quantity it's alright as it gets
        # interpolated in the `convolve_with_slit` function

        # re-Normalize if needed (after changing units)
        if renormalize:
            if __debug__: printdbg('get_slit_function: renormalize')
            if norm_by == 'area':  # normalize by the area
                Islit /= abs(np.trapz(Islit, x=wslit))
                Iunit = '1/{0}'.format(return_unit)
            elif norm_by == 'max':  # set maximum to 1
                Islit /= abs(np.max(Islit))
                Islit *= scale_slit
                Iunit = '1'
                if scale_slit != 1:
                    Iunit += 'x{0}'.format(scale_slit)


#            elif norm_by == 'max2': # set maximum to 1    # removed this mode for simplification
#                Islit /= abs(np.max(Islit))
            elif norm_by is None:
                Iunit = None
            else:
                raise ValueError(
                    'Unknown normalization type: `norm_by` = {0}'.format(
                        norm_by))

        if plot:  # (plot after resampling / renormalizing)
            # Plot slit
            plot_slit(wslit, Islit, waveunit=return_unit, Iunit=Iunit)

    else:
        raise TypeError('Unexpected type for slit function: {0}'.format(
            type(slit_function)))

    return wslit, Islit