def _check_valid(s): # type: (Spectrum) -> bool ''' Check s is a valid Spectrum object. Raises an error if not Valid if: - is a Spectrum Also print a warning if: - quantities used for solving the LOS have nan ''' if not is_spectrum(s): raise TypeError('All inputs must be Spectrum objects (got: {0})'.format( type(s))) sdict = s._get_items() for k in sdict.keys(): if (k in ['transmittance_noslit', 'radiance_noslit', 'abscoeff', 'emisscoeff'] and count_nans(sdict.get(k)) > 0): warn('Nans detected in Spectrum object for multi-slab operation. ' + 'Results may be wrong!') return True
def _check_valid(s): # type: (Spectrum) -> bool """Check s is a valid Spectrum object. Raises an error if not. Valid if: - is a Spectrum Also print a warning if: - quantities used for solving the LOS have nan """ if not is_spectrum(s): raise TypeError( "All inputs must be Spectrum objects (got: {0})".format(type(s))) sdict = s._get_items() for k in sdict.keys(): if (k in [ "transmittance_noslit", "radiance_noslit", "abscoeff", "emisscoeff" ] and np.isnan(sdict.get(k)[1]).any()): warn( "Nans detected in Spectrum object for multi-slab operation. " + "Results may be wrong!") return True
def _check_valid(s): ''' Check s is a valid Spectrum object. Raises an error if not ''' if not is_spectrum(s): raise TypeError( 'All inputs must be Spectrum objects (got: {0})'.format(type(s))) sdict = s._get_items() for k in sdict.keys(): if count_nans(sdict.get(k)) > 0: warn('Nans detected in Spectrum object for multi-slab operation. '+\ 'Results may be wrong!') return True
def __init__(self, parfunc, bands, levelsfmt, sortby="Ei", copy_lines=False, verbose=True): """ Parameters ---------- bands: dict of bands bands are Spectrum objects calculated at equilibrium or non-equilibrim. Tgas, or (Tvib, Trot) must be given and the same in all bands conditions. """ # Check inputs for br, s in bands.items(): if not is_spectrum(s): raise ValueError( "`bands` must be a list of Spectrum objects. " + "Got {0}".format(type(s))) # Assert all conditions are the same bands_list = list(bands.values()) cond_ref = bands_list[0].conditions for s in bands_list[1:]: # type(s): Spectrum if list(s.conditions.keys()) != list(cond_ref.keys()): raise ValueError( "Input conditions must be the same for all bands") for k in cond_ref.keys(): if k in _IGNORE_KEYS: # don't compare these entries continue if s.conditions[k] != cond_ref[k]: raise ValueError( "Input conditions must be the same for all bands" + ". Got a difference for entry {0}: {1} vs {2}".format( k, s.conditions[k], cond_ref[k])) if "Tvib" in cond_ref: Tvib_ref = cond_ref["Tvib"] else: Tvib_ref = cond_ref["Tgas"] if "Trot" in cond_ref: Trot_ref = cond_ref["Trot"] else: Trot_ref = cond_ref["Tgas"] vib_distribution_ref = cond_ref["vib_distribution"] if "overpopulation" in cond_ref: raise NotImplementedError # stores input conditions self.Tvib_ref = Tvib_ref self.Trot_ref = Trot_ref self.mole_fraction_ref = cond_ref["mole_fraction"] self.path_length_ref = cond_ref["path_length"] self.vib_distribution_ref = vib_distribution_ref # stores computation parameters # TODO: store it in parfunc instead (and make parfunc an EnergyDatabase) self.levelsfmt = levelsfmt # %% Misc self.verbose = verbose self.copy_lines = copy_lines # %% Set up energies ( use a partition function object for the moment) self.parfunc = parfunc self._init_levels(sortby) self._connect_bands_to_levels(bands) Qvib = self._calc_vib_populations( Tvib=self.Tvib_ref, vib_distribution=self.vib_distribution_ref) self.Qref = parfunc.at_noneq( Tvib_ref, Trot_ref, vib_distribution=vib_distribution_ref ) # TODO: add overpopulations in partition function self.Qvib_ref = Qvib self.vib_levels["nvib_ref"] = self.vib_levels["nvib"] del self.vib_levels["nvib"] # %% Set up bands self._init_bands_ref(bands, sortby)
def add(self, spectrum, **kwargs): ''' Add Spectrum to database, whether it's a Spectrum object or a file that stores one. Check it's not in database already. Parameters ---------- spectrum: path to .spec file, or Spectrum object if Spectrum object: stores it in database first (using Spectrum.store()), then adds the file if path to file: will first copy the file in database folder, then adds the file **kwargs: **dict extra parameters used in the case where spectrum is a file and a .spec object has to be created (useless if `spectrum` is a file already). kwargs are forwarded to Spectrum.store() method. See :meth:`~radis.spectrum.spectrum.Spectrum.store` or database.:meth:`~radis.tools.database.SpecDatabase.save` for more information Other Parameters ---------------- :meth:`~radis.spectrum.spectrum.Spectrum.store` parameters given as kwargs arguments. compress: boolean if True, removes all quantities that can be regenerated with s.update(), e.g, transmittance if abscoeff and path length are given, radiance if emisscoeff and abscoeff are given in non-optically thin case, etc. Default False add_info: list append these parameters and their values if they are in conditions example:: nameafter = ['Tvib', 'Trot'] discard: list of str parameters to exclude. To save some memory for instance Default [`lines`, `populations`]: retrieved Spectrum will loose the line_survey ability, and plot_populations() (but it saves a ton of memory!) if_exists_then: 'increment', 'replace', 'error' what to do if file already exists. If increment an incremental digit is added. If replace file is replaced (yeah). If error (or anything else) an error is raised. Default `increment` Examples -------- Simply write:: db.add(s, discard=['populations']) See Also -------- :meth:`~radis.tools.database.SpecDatabase.get` :meth:`~radis.tools.database.SpecDatabase.get_unique` :meth:`~radis.tools.database.SpecDatabase.get_closest` ''' # Check inputs if 'path' in kwargs: raise ValueError('path is an invalid Parameter. The database path '+\ 'is used') compress = kwargs.pop('compress', False) # First, store the spectrum on a file # ... input is a Spectrum. Store it in database and load it from there if is_spectrum(spectrum): # add defaults if not 'add_info' in kwargs: kwargs['add_info'] = self.add_info if not 'add_date' in kwargs: kwargs['add_date'] = self.add_date file = spectrum.store(self.path, compress=compress, **kwargs) # Note we could have added the Spectrum directly # (saves the load stage) but it also serves to # check the file we just stored is readable # ... input is a file name. Copy it in database and load it elif isinstance(spectrum, string_types): if not exists(spectrum): raise FileNotFoundError( 'File doesnt exist: {0}'.format(spectrum)) fd, name = split(spectrum) # Assert a similar case name is not in database already if spectrum in list(self.df['file']): raise ValueError( 'File already in database: {0}. Database filenames should be unique' .format(spectrum)) if abspath(fd) != abspath(self.path): # Assert file doesnt exist in database already if name in os.listdir(self.path): raise ValueError('File already in database folder: {0}'.format(name)+\ '. Use db.update() if you added it there manually') # Ok. Copy it. file = join(self.path, name) copy2(spectrum, file) else: warn( 'You are manually adding a file that is in the database folder directly. Consider using db.update()' ) file = spectrum else: raise ValueError('Unvalid Spectrum type: {0}'.format( type(spectrum))) # Then, load the Spectrum again (so we're sure it works!) and add the # information to the database self.df = self.df.append(self._load_file(file, binary=compress), ignore_index=True) # Update index .csv self.print_index() return file
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
def SerialSlabs(*slabs, **kwargs): # type: (*Spectrum, **dict) -> Spectrum r"""Adds several slabs along the line-of-sight. If adding two slabs only, you can also use:: s1>s2 Serial spectrum ``1>2`` is calculated with Eqn (4.2) of the [RADIS-2018]_ article, generalized to N slabs : .. math:: I_{\lambda, 1>2} = I_{\lambda, 1} \tau_{\lambda, 2} + I_{\lambda, 2} \tau_{\lambda, 1+2} = \tau_{\lambda, 1} \cdot \tau_{\lambda, 2} where .. math:: I_{\lambda}, \tau_{\lambda} are the radiance and transmittance of the two slabs ``1`` and ``2``. Radiance and transmittance are calculated if not given in the initial slabs (if possible). Parameters ---------- slabs: list of Spectra, each representing a slab line-of-sight:: slabs [0] [1] ............... [n] : : : \==== light * -> * -> * -> )=== observer /==== 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: ``'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'`` Other Parameters ---------------- verbose: bool if ``True``, more blabla. Default ``False`` modify_inputs: False if ``True``, slabs wavelengths/wavenumbers are modified directly when they are resampled. This avoids making a copy so it is slightly faster. Default ``False``. ..note:: for large number of slabs (in radiative transfer calculations) you surely want to use this option ! Returns ------- Spectrum object representing total emission and total transmittance as observed at the output (slab[n+1]). 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 -------- Add s1 and s2 along the line of sight: s1 --> s2 :: s1 = calc_spectrum(...) s2 = calc_spectrum(...) s3 = SerialSlabs(s1, s2) The last line is equivalent to:: s3 = s1>s2 See Also -------- :func:`~radis.los.slabs.MergeSlabs` See more examples in the :ref:`Line-of-Sight module <label_los_index>` """ # TODO: rewrite with 'recompute' list like in MergeSlabs ? 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 resample_wavespace = kwargs.pop("resample", "never") # default 'never' out_of_bounds = kwargs.pop("out", "nan") # default 'nan' verbose = kwargs.pop("verbose", 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()))) 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( "SerialSlabs takes an unfolded list of Spectrum as " + "argument: *list (got {0})".format(type(slabs[0]))) return slabs[0] else: # recursively calculate serial slabs slabs = list(slabs) # Recursively deal with the rest of Spectra --> call it s sn = slabs.pop(-1) # type: Spectrum _check_valid(sn) # check it is a spectrum s = SerialSlabs(*slabs, resample=resample_wavespace, out=out_of_bounds, modify_inputs=modify_inputs) # Now calculate sn and s in Serial quantities = {} unitsn = sn.units # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1') # also make sure we use the same units waveunit = s.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) s, sn = resample_slabs(waveunit, resample_wavespace, out_of_bounds, modify_inputs, s, sn) try: w = s._q["wavespace"] except KeyError: raise KeyError( "Cannot calculate the RTE if non convoluted quantities " + "are not defined. Got: {0}".format(s.get_vars())) # Get all data # ------------- I, In, T, Tn = None, None, None, None # To make it easier, the radiative transfer equation is solved with 'radiance_noslit' and # 'transmittance_noslit' only. Here we first try to get these quantities: # ... get sn quantities try: sn.update("transmittance_noslit", verbose=verbose) except ValueError: pass else: Tn = sn.get( "transmittance_noslit", wunit=waveunit, Iunit=unitsn["transmittance_noslit"], copy=False, )[1] try: sn.update("radiance_noslit", verbose=verbose) except ValueError: pass else: In = sn.get( "radiance_noslit", wunit=waveunit, Iunit=unitsn["radiance_noslit"], copy=False, )[1] # ... get s quantities try: s.update("transmittance_noslit", verbose=verbose) except ValueError: pass else: T = s.get( "transmittance_noslit", wunit=waveunit, Iunit=unitsn["transmittance_noslit"], copy=False, )[1] try: s.update("radiance_noslit", verbose=verbose) except ValueError: pass else: I = s.get( "radiance_noslit", wunit=waveunit, Iunit=unitsn["radiance_noslit"], copy=False, )[1] # Solve radiative transfer equation # --------------------------------- if I is not None and In is not None: # case where we may use SerialSlabs just to compute the products of all transmittances quantities["radiance_noslit"] = (w, I * Tn + In) if T is not None: # note that we dont need the transmittance in the inner # slabs to calculate the total radiance quantities["transmittance_noslit"] = (w, Tn * T) # Get conditions (if they're different, fill with 'N/A') conditions = intersect(s.conditions, sn.conditions) conditions["waveunit"] = waveunit # sum path lengths if "path_length" in s.conditions and "path_length" in sn.conditions: conditions["path_length"] = (s.conditions["path_length"] + sn.conditions["path_length"]) cond_units = intersect(s.cond_units, sn.cond_units) # name name = _serial_slab_names(s, sn) return Spectrum( quantities=quantities, conditions=conditions, cond_units=cond_units, units=unitsn, name=name, warnings= False, # we already know waveranges are properly spaced, etc. )
def compare_spectra(first, other, spectra_only=False, plot=True, wunit="default", verbose=True, rtol=1e-5, ignore_nan=False, ignore_outliers=False, normalize=False, **kwargs): """Compare Spectrum with another Spectrum object Parameters ---------- first: type Spectrum a Spectrum to be compared other: type Spectrum another Spectrum to compare with spectra_only: boolean, or str if ``True``, only compares spectral quantities (in the same waveunit) and not lines or conditions. If str, compare a particular quantity name. If False, compare everything (including lines and conditions and populations). Default ``False`` plot: boolean if ``True``, use plot_diff to plot all quantities for the 2 spectra and the difference between them. Default ``True``. wunit: 'nm', 'cm-1', 'default' in which wavespace to compare (and plot). If default, natural wavespace of first Spectrum is taken rtol: float relative difference to use for spectral quantities comparison ignore_nan: boolean if ``True``, nans are ignored when comparing spectral quantities ignore_outliers: boolean, or float if not False, outliers are discarded. i.e, output is determined by:: out = (~np.isclose(I, Ie, rtol=rtol, atol=0)).sum()/len(I) < ignore_outliers normalize: bool Normalize the spectra to be plotted Other Parameters ---------------- kwargs: dict arguments are forwarded to :func:`~radis.spectrum.compare.plot_diff` Returns ------- equals: boolean return True if spectra are equal (respective to tolerance defined by rtol and other input conditions) Examples -------- Compare two Spectrum objects, or specifically the transmittance:: s1.compare_with(s2) s1.compare_with(s2, 'transmittance') Note that you can also simply use `s1 == s2`, that uses :meth:`~radis.spectrum.spectrum.Spectrum.compare_with` internally:: s1 == s2 # will return True or False """ # Check inputs if not 0 <= ignore_outliers < 1: raise ValueError("ignore_outliers should be < 1, or False") if not is_spectrum(other): raise TypeError("2nd object is not a Spectrum: got class {0}".format( other.__class__)) if isinstance(spectra_only, str): # case where we compare all quantities if not spectra_only in first.get_vars(): raise ValueError( "{0} is not a spectral quantity in our Spectrum ({1})".format( spectra_only, first.get_vars())) if not spectra_only in other.get_vars(): raise ValueError( "{0} is not a spectral quantity in the other Spectrum ({1})". format(spectra_only, other.get_vars())) if verbose: # print conditions what = spectra_only if isinstance(spectra_only, str) else "all quantities" msg = "compare {0} with rtol={1}".format(what, rtol) if ignore_nan: msg += ", ignore_nan" if ignore_outliers: msg += ", ignore_outliers={0}".format(ignore_outliers) print(msg) if not plot and len(kwargs) > 0: raise ValueError("Unexpected argument: {0}".format(kwargs)) if wunit == "default": wunit = first.get_waveunit() def _compare_dataframes(df1, df2, name): """ Parameters ---------- df1, df2: pandas Dataframe lines, or vib/rovib levels dataframes name: str for error message """ # if compare_lists(df1.keys(), df2.keys(), verbose=False) != 1: # if verbose: print('... keys in {0} dont match:'.format(name)) # compare_lists(list(df1.keys()), list(df2.keys()), # verbose=True) # out = False # elif compare_lists(df1.index, df2.index, verbose=False) != 1: # if verbose: print('... index in {0} dont match:'.format(name)) # compare_lists(list(df1.index), list(df2.index), # verbose=True) # out = False # else: # out = (df1 == df2).all().all() # # return out from pandas.util.testing import assert_frame_equal try: assert_frame_equal( df1.sort_index(axis=0).sort_index(axis=1), df2.sort_index(axis=0).sort_index(axis=1), check_names=True, check_column_type= False, # solves problem in Python 2/3 dataframes (unicode/str) ) out = True except AssertionError as err: if verbose: print("Comparing ", name) print(err.args[0]) out = False return out def _compare_variables(I, Ie): """ Compare spectral quantities I and Ie """ if ignore_nan: b = ~(np.isnan(I) + np.isnan(Ie)) I = I[b] Ie = Ie[b] if ignore_outliers: out = (~np.isclose(I, Ie, rtol=rtol, atol=0)).sum() / len(I) < ignore_outliers else: out = np.allclose(I, Ie, rtol=rtol, atol=0) return bool(out) def _display_difference(q, q0): error = np.nanmax(abs(q / q0 - 1)) avgerr = np.nanmean(abs(q / q0 - 1)) print( "...", k, "don't match (up to {0:.3}% diff.,".format(error * 100) + " average {0:.3f}%)".format(avgerr * 100), ) b = True if isinstance(spectra_only, str): # compare this quantity vars = [spectra_only] else: # compare all quantities b = set(first.get_vars()) == set(other.get_vars()) if not b and verbose: print("... list of quantities dont match: {0} vs {1}".format( first.get_vars(), other.get_vars())) vars = [k for k in first.get_vars() if k in other.get_vars()] if spectra_only: # Compare spectral variables # ----------- for k in vars: w, q = first.get(k, wunit=wunit) w0, q0 = other.get(k, wunit=wunit) if len(w) != len(w0): print("Wavespaces have different length (for {0}: {1} vs {2})". format(k, len(w), len(w0))) print("We interpolate one spectrum on the other one") from scipy.interpolate import interp1d if len(q) > len(q0): f = interp1d(w, q, kind="cubic") new_q = f(w0) b1 = _compare_variables(new_q, q0) if not b1 and verbose: _display_difference(new_q, q0) else: f = interp1d(w0, q0, kind="cubic") new_q0 = f(w) b1 = _compare_variables(q, new_q0) if not b1 and verbose: _display_difference(q, new_q0) else: # no need to interpolate b1 = np.allclose(w, w0, rtol=rtol, atol=0) b1 *= _compare_variables(q, q0) if not b1 and verbose: _display_difference(q, q0) b *= b1 if plot: try: plot_diff(first, other, var=k, wunit=wunit, normalize=normalize, verbose=verbose, **kwargs) except: print("... couldn't plot {0}".format(k)) else: # Compare spectral variables # ----------- for k in vars: w, q = first.get(k, wunit=wunit) w0, q0 = other.get(k, wunit=wunit) if len(w) != len(w0): print("Wavespaces have different length (for {0}: {1} vs {2})". format(k, len(w), len(w0))) b1 = False else: b1 = np.allclose(w, w0, rtol=rtol, atol=0) b1 *= _compare_variables(q, q0) if not b1 and verbose: error = np.nanmax(abs(q / q0 - 1)) avgerr = np.nanmean(abs(q / q0 - 1)) print( "...", k, "don't match (up to {0:.3}% diff.,".format(error * 100) + " average {0:.3f}%)".format(avgerr * 100), ) b *= b1 if plot: try: plot_diff(first, other, var=k, wunit=wunit, normalize=normalize, verbose=verbose, **kwargs) except: print( "... there was an error while plotting {0}".format(k)) # Compare conditions and units # ----------- verbose_dict = "if_different" if verbose else False b1 = compare_dict(first.conditions, other.conditions, verbose=verbose_dict) == 1 b2 = compare_dict(first.cond_units, other.cond_units, verbose=verbose_dict) == 1 b3 = compare_dict(first.units, other.units, verbose=verbose_dict) == 1 if not b1 and verbose: print("... conditions don't match") if not b2 and verbose: print("... conditions units don't match") if not b3 and verbose: print("... units don't match") b *= b1 * b2 * b3 # Compare lines # ----------- if first.lines is None and other.lines is None: b4 = True elif first.lines is None: b4 = False elif other.lines is None: b4 = False else: b4 = _compare_dataframes(first.lines, other.lines, "lines") if not b4 and verbose: print("... lines dont match") b *= b4 # Compare populations # ----------- if first.populations is None and other.populations is None: b5 = True elif first.populations is None: b5 = False elif other.populations is None: b5 = False else: # Compare keys in populations b5 = True if (compare_lists(first.populations, other.populations, verbose="if_different") == 1): # same molecules, compare isotopes for molecule, isotopes in first.populations.items(): if (compare_lists( isotopes, other.populations[molecule], verbose="if_different", ) == 1): # same isotopes, compare electronic states for isotope, states in isotopes.items(): if (compare_lists( states, other.populations[molecule][isotope], verbose="if_different", ) == 1): # same electronic states, compare levels + other information for state, content in states.items(): for k, v in content.items(): if k in ["vib", "rovib"]: b5 *= _compare_dataframes( v, other.populations[molecule] [isotope][state][k], "populations of {0}({1})(iso{2})" .format( molecule, state, isotope), ) else: b5 *= (v == other.populations[ molecule][isotope][state][k]) else: b5 = False if verbose: print( "{0}(iso{1}) states are different (see above)" .format(molecule, isotope)) else: b5 = False if verbose: print("{0} isotopes are different (see above)". format(molecule)) else: b5 = False if verbose: print("Molecules are different (see above)") if not b5 and verbose: print("... populations dont match (see detail above)") b *= b5 # Compare slit # ----------- if len(first._slit) == len(other._slit) == 0: # no slit anywhere b6 = True elif len(first._slit) != len(other._slit): b6 = False if verbose: print( "A spectrum has slit function array but the other doesnt") else: ws, Is = first.get_slit() ws0, Is0 = other.get_slit() if len(ws) != len(ws0): if verbose: print("Slits have different length") b6 = False else: b6 = np.allclose(ws, ws0, rtol=rtol, atol=0) b6 *= _compare_variables(Is, Is0) if not b6 and verbose: print("Slit functions dont match") b *= b6 return bool(b)
def SerialSlabs(*slabs, **kwargs): ''' Compute the result of several slabs Parameters ---------- slabs: list of Spectra, each representing a slab slabs [0] [1] ............... [n] : : : \==== light * -> * -> * -> )=== observer /==== 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' Returns ------- Spectrum object representing total emission and total transmittance as observed at the output (slab[n+1]). 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 -------- Add s1 and s2 along the line of sight: s1 --> s2 :: s1 = calc_spectrum(...) s2 = calc_spectrum(...) s3 = SerialSlabs(s1, s2) Notes ----- Todo: - rewrite with 'recompute' list like in MergeSlabs See Also -------- :func:`~radis.los.slabs.MergeSlabs` ''' # Check inputs, get defaults resample_wavespace = kwargs.pop('resample_wavespace', 'never') # default 'never' out_of_bounds = kwargs.pop('out_of_bounds', 'nan') # default 'nan' if len(kwargs) > 0: raise ValueError('Unexpected input: {0}'.format(list(kwargs.keys()))) 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('SerialSlabs takes an unfolded list of Spectrum as '+\ 'argument: *list (got {0})'.format(type(slabs[0]))) return slabs[0] else: # recursively calculate serial slabs slabs = list(slabs) # # Check all items are Spectrum for s in slabs: _check_valid(s) sn = slabs.pop(-1) # type: Spectrum s = SerialSlabs(*slabs, resample_wavespace=resample_wavespace, out_of_bounds=out_of_bounds) quantities = {} unitsn = sn.units # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1') # also make sure we use the same units waveunit = s.get_waveunit() w = s.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit'])[0] wn = sn.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit'])[0] # Make all our slabs copies with the same wavespace range # (note: wavespace range may be different for different quantities, but # equal for all slabs) s, sn = resample_slabs(waveunit, resample_wavespace, out_of_bounds, s, sn) w, I = s.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit']) wn, In = sn.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit']) _, Tn = sn.get('transmittance_noslit', wunit=waveunit, Iunit=unitsn['transmittance_noslit']) if 'radiance_noslit' in s._q and 'radiance_noslit' in sn._q: # case where we may use SerialSlabs just to compute the products of all transmittances quantities['radiance_noslit'] = (w, I * Tn + In) if 'transmittance_noslit' in s._q: # note that we dont need the transmittance in the inner # slabs to calculate the total radiance _, T = s.get('transmittance_noslit', wunit=waveunit, Iunit=unitsn['transmittance_noslit']) quantities['transmittance_noslit'] = (w, Tn * T) # Get conditions (if they're different, fill with 'N/A') conditions = intersect(s.conditions, sn.conditions) conditions['waveunit'] = waveunit cond_units = intersect(s.cond_units, sn.cond_units) # name name = _serial_slab_names(s, sn) return Spectrum(quantities=quantities, conditions=conditions, cond_units=cond_units, units=unitsn, name=name)
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)
def SerialSlabs(*slabs, **kwargs): # type: (*Spectrum, **dict) -> Spectrum ''' Adds several slabs along the line-of-sight. You can also use:: s1>s2>s3 Parameters ---------- slabs: list of Spectra, each representing a slab line-of-sight:: slabs [0] [1] ............... [n] : : : \==== light * -> * -> * -> )=== observer /==== 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: ``'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'`` Returns ------- Spectrum object representing total emission and total transmittance as observed at the output (slab[n+1]). 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 -------- Add s1 and s2 along the line of sight: s1 --> s2 :: s1 = calc_spectrum(...) s2 = calc_spectrum(...) s3 = SerialSlabs(s1, s2) The last line is equivalent to:: s3 = s1>s2 Notes ----- Todo: - rewrite with 'recompute' list like in MergeSlabs See Also -------- :func:`~radis.los.slabs.MergeSlabs` See more examples in :ref:`Line-of-Sight module <label_los_index>` ''' 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 resample_wavespace = kwargs.pop('resample', 'never') # default 'never' out_of_bounds = kwargs.pop('out', 'nan') # default 'nan' if len(kwargs) > 0: raise ValueError('Unexpected input: {0}'.format(list(kwargs.keys()))) 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( 'SerialSlabs takes an unfolded list of Spectrum as ' + 'argument: *list (got {0})'.format(type(slabs[0]))) return slabs[0] else: # recursively calculate serial slabs slabs = list(slabs) # # Check all items are Spectrum for s in slabs: _check_valid(s) # Recursively deal with the rest of Spectra --> call it s sn = slabs.pop(-1) # type: Spectrum s = SerialSlabs(*slabs, resample=resample_wavespace, out=out_of_bounds) # Now calculate sn and s in Serial quantities = {} unitsn = sn.units # make sure we use the same wavespace type (even if sn is in 'nm' and s in 'cm-1') # also make sure we use the same units waveunit = s.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) s, sn = resample_slabs(waveunit, resample_wavespace, out_of_bounds, s, sn) try: w = s._q['wavespace'] except KeyError: raise KeyError('Cannot calculate the RTE if non convoluted quantities '+\ 'are not defined. Got: {0}'.format(s.get_vars())) # Get all data # ------------- I, In, T, Tn = None, None, None, None # To make it easier, the radiative transfer equation is solved with 'radiance_noslit' and # 'transmittance_noslit' only. Here we first try to get these quantities: # ... get sn quantities try: sn.update('transmittance_noslit', verbose=False) except ValueError: pass else: Tn = sn.get('transmittance_noslit', wunit=waveunit, Iunit=unitsn['transmittance_noslit'])[1] try: sn.update('radiance_noslit', verbose=False) except ValueError: pass else: In = sn.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit'])[1] # ... get s quantities try: s.update('transmittance_noslit', verbose=False) except ValueError: pass else: T = s.get('transmittance_noslit', wunit=waveunit, Iunit=unitsn['transmittance_noslit'])[1] try: s.update('radiance_noslit', verbose=False) except ValueError: pass else: I = s.get('radiance_noslit', wunit=waveunit, Iunit=unitsn['radiance_noslit'])[1] # Solve radiative transfer equation # --------------------------------- if I is not None and In is not None: # case where we may use SerialSlabs just to compute the products of all transmittances quantities['radiance_noslit'] = (w, I * Tn + In) if T is not None: # note that we dont need the transmittance in the inner # slabs to calculate the total radiance quantities['transmittance_noslit'] = (w, Tn * T) # Get conditions (if they're different, fill with 'N/A') conditions = intersect(s.conditions, sn.conditions) conditions['waveunit'] = waveunit # sum path lengths if 'path_length' in s.conditions and 'path_length' in sn.conditions: conditions['path_length'] = s.conditions[ 'path_length'] + sn.conditions['path_length'] cond_units = intersect(s.cond_units, sn.cond_units) # name name = _serial_slab_names(s, sn) return Spectrum(quantities=quantities, conditions=conditions, cond_units=cond_units, units=unitsn, name=name)