"""Provides a simplified API for calculating molecular line spectra."""
from collections import namedtuple
from numpy import sum as npsum
from numpy import unravel_index, zeros
from xarray import DataArray, Dataset
from .atmosphere import Atmosphere
from .database import AliasNotFoundError, CrossSectionNotFoundError, \
IsotopologuesNotFoundError, TipsDataNotFoundError, \
TransitionsNotFoundError
from .plugins import continua, cross_sections, molecular_lines
kb = 1.38064852e-23 # Boltzmann constant [J K-1].
[docs]def number_density(temperature, pressure, volume_mixing_ratio):
"""Calculates the number density using the ideal gas law.
Args:
temperature: Temperature [K].
pressure: Pressure [Pa].
volume_mixing_ratio: Volume-mixing ratio [mol mol-1].
Returns:
Number density [m-3].
"""
return pressure*volume_mixing_ratio/(kb*temperature)
[docs]class MoleculeCache(object):
"""Helper class that caches molecular data so it can be reused.
Attributes:
cross_section: Object controlling the current absorption cross section backend plugin.
gas: Object controlling the current lines backend plugin.
gas_continua: List of objects controlling the curret continua backend plugin.
"""
def __init__(self, name, grid, lines_database, lines_engine, continua_engine,
cross_sections_engine):
"""Initializes the internal cache.
Args:
name: String chemical formula.
grid: Numpy array defining the spectral grid [cm-1].
lines_database: Database object controlling the spectral database.
lines_engine: Gas object to use for the lines calculation.
continua_engine: Continuum object to use for the molecular continua.
cross_sections_engine: CrossSection object to use for the cross
section calculation.
"""
try:
self.gas = lines_engine(lines_database, name)
except (AliasNotFoundError, IsotopologuesNotFoundError,
TipsDataNotFoundError, TransitionsNotFoundError):
self.gas = None
if name == "H2O":
names = ["{}{}".format(name, x) for x in ["Foreign", "Self"]]
else:
names = [name, ]
try:
self.gas_continua = [continua_engine[x]() for x in names]
except KeyError:
self.gas_continua = None
try:
self.cross_section = cross_sections_engine(name, lines_database.arts_crossfit(name))
except (AliasNotFoundError, CrossSectionNotFoundError):
self.cross_section = None
[docs]class Spectroscopy(object):
"""Line-by-line gas optics.
Attributes:
atmosphere: Atmosphere object describing atmospheric conditions.
cache: Dictionary of MoleculeCache objects.
continua_backend: String name of model to use for the continua.
continua_engine: Object exposed by the current molecular continuum backed.
cross_section_backend: String name of model to use for the cross sections.
cross_section_engine: Object exposed by the current cross sections backend.
grid: Numpy array describing the spectral grid [cm-1].
lines_backend: String name of model to use for lines calculation.
lines_database: Database object controlling the spectral database.
lines_engine: Object exposed by the current molecular lines backend.
output: namedtuple object that stores some output dataset metadata.
"""
def __init__(self, atmosphere, grid, database, mapping=None,
lines_backend="pyLBL", continua_backend="mt_ckd",
cross_sections_backend="arts_crossfit"):
"""Initializes object.
Example:
mapping = {
"play": <name of pressure variable in dataset>,
"tlay": <name of temperature variable in dataset>,
"mole_fraction: {
"H2O" : <name of water vapor mole fraction variable in dataset>,
"CO2" : <name of carbon dioxided mole fraction variable in dataset>,
...
}
}
Args:
atmosphere: xarray Dataset describing atmospheric conditions.
grid: Wavenumber grid array [cm-1].
database: Database object to use.
mapping: Dictionary describing atmospheric dataset variable names.
lines_backend: String name of the model to use for the lines calculation.
continua_backend: String name of the model to use for the molecular continua.
cross_sections_backend: String name of the model to use for the cross
section calculation.
"""
self.atmosphere = Atmosphere(atmosphere, mapping=mapping)
self.grid = grid
self.lines_database = database
self.lines_backend = lines_backend
self.lines_engine = molecular_lines[lines_backend]
self.continua_backend = continua_backend
self.continua_engine = continua[continua_backend]
self.cross_sections_backend = cross_sections_backend
self.cross_sections_engine = cross_sections[cross_sections_backend]
self.cache = {}
# Prepare metadata for the ouput xarray Dataset.
Output = namedtuple("Output", ["dims", "dim_sizes", "mechanisms", "units"])
mechanisms = ["lines", "continuum", "cross_section"]
dims = list(self.atmosphere.temperature.dims) + ["mechanism", "wavenumber", ]
dim_sizes = \
[x for x in self.atmosphere.temperature.sizes.values()] + \
[len(mechanisms), self.grid.size]
units = {"units": "m-1"}
self.output = Output(dims=dims, dim_sizes=dim_sizes, mechanisms=mechanisms,
units=units)
[docs] def list_molecules(self):
"""Provides a list of molecules available in the specral lines database.
Returns:
List of string molecule formulae available in the specral lines database.
"""
return self.lines_database.molecules()
[docs] def compute_absorption(self, output_format="all", remove_pedestal=None):
"""Computes absorption coefficient [m-1] at specified wavenumbers given temperature,
pressure, and gas concentrations.
Args:
output_format: String describing how the absorption data should be output.
"all" - returns absorption spectra from all components (lines,
continuum, cross section) for all gases separately.
"gas" - returns total absorption spectra for all gases
separately.
"total" - returns the total spectra.
remove_pedestal: Flag that allows the user to not subtract off the
MT-CKD water vapor "pedestal" if desired.
Returns:
An xarray Dataset of absorption coefficients [m-1].
"""
pressure = self.atmosphere.pressure.data.flat
temperature = self.atmosphere.temperature.data.flat
if remove_pedestal is None:
remove_pedestal = self.continua_backend == "mt_ckd"
beta = {}
for name, mole_fraction in self.atmosphere.gases.items():
varname = "{}_absorption".format(name)
beta[varname] = DataArray(zeros(self.output.dim_sizes), dims=self.output.dims,
attrs=self.output.units)
try:
# Grab cached spectral database data.
data = self.cache[name]
except KeyError:
# If not already cached, then cache it.
data = MoleculeCache(name, self.grid, self.lines_database,
self.lines_engine, self.continua_engine,
self.cross_sections_engine)
self.cache[name] = data
for i in range(self.atmosphere.temperature.data.size):
vmr = {x: y.data.flat[i] for x, y in self.atmosphere.gases.items()}
n = number_density(temperature[i], pressure[i],
mole_fraction.data.flat[i])
j = unravel_index(i, self.atmosphere.temperature.data.shape)
# Calculate lines.
if data.gas is not None:
k = data.gas.absorption_coefficient(temperature[i], pressure[i],
mole_fraction.data.flat[i], self.grid,
remove_pedestal=remove_pedestal)
indices = tuple(list(j) + [0, slice(None)])
beta[varname].values[indices] = n*k[:self.grid.size]
# Calculate continua.
if data.gas_continua is not None:
indices = tuple(list(j) + [1, slice(None)])
for continuum in data.gas_continua:
k = continuum.spectra(temperature[i], pressure[i], vmr, self.grid)
beta[varname].values[indices] += k[:]
# Calculate the cross section.
if data.cross_section is not None:
k = data.cross_section.absorption_coefficient(self.grid, temperature[i],
pressure[i])
indices = tuple(list(j) + [2, slice(None)])
beta[varname].values[indices] = n*k[:]
return self._create_output_dataset(beta, output_format)
def _create_output_dataset(self, absorption, output_format):
"""Creates an xarray Dataset with the calculated absorption values.
Args:
absorption: Dictionary containing the absorptoin data.
output_format: String describing how the data should be output.
Returns:
xarray Dataset containing the absorption in the desired format.
"""
wavenumber = DataArray(self.grid, dims=("wavenumber",), attrs={"units": "cm-1"})
data_vars = {"wavenumber": wavenumber, }
dims = list(self.output.dims)
units = self.output.units
if output_format == "all":
data_vars["mechanism"] = DataArray(self.output.mechanisms, dims=("mechanism",))
data_vars.update(absorption)
elif output_format == "gas":
dims.pop(-2)
data_vars.update({x: DataArray(npsum(y.values, axis=-2), dims=dims, attrs=units)
for x, y in absorption.items()})
else:
dims.pop(-2)
data = [DataArray(npsum(x.values, axis=-2), dims=dims, attrs=units) for x in
absorption.values()]
data_vars["absorption"] = DataArray(sum([x.values for x in data]),
dims=dims, attrs=units)
return Dataset(data_vars=data_vars)