"""
Utility functions for atmospheric retrieval with ``petitRADTRANS``.
This module was put together many contributions by Paul Mollière
(MPIA).
"""
import copy
import sys
import warnings
from typing import Dict, List, Optional, Tuple, Union
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import interp1d, PchipInterpolator
from scipy.ndimage import gaussian_filter
from typeguard import typechecked
from species.core import constants
[docs]
@typechecked
def get_line_species() -> List[str]:
"""
Function to get the list of the molecular and atomic line species.
This function is not used anywhere so could be removed.
Returns
-------
list(str)
List with the line species.
"""
return [
"CH4",
"CO",
"CO_all_iso",
"CO_all_iso_HITEMP",
"CO_all_iso_Chubb",
"CO2",
"H2O",
"H2O_HITEMP",
"H2S",
"HCN",
"K",
"K_lor_cut",
"K_allard",
"K_burrows",
"NH3",
"Na",
"Na_lor_cut",
"Na_allard",
"Na_burrows",
"OH",
"PH3",
"TiO",
"TiO_all_Exomol",
"TiO_all_Plez",
"VO",
"VO_Plez",
"FeH",
"H2O_main_iso",
"CH4_main_iso",
]
[docs]
@typechecked
def pt_ret_model(
temp_3: Optional[np.ndarray],
delta: float,
alpha: float,
tint: float,
press: np.ndarray,
metallicity: float,
c_o_ratio: float,
conv: bool = True,
) -> Tuple[Optional[np.ndarray], Optional[float], Optional[float]]:
"""
Pressure-temperature profile for a self-luminous atmosphere (see
Mollière et al. 2020).
Parameters
----------
temp_3 : np.ndarray, None
Array with three temperature points that are added on top of
the radiative Eddington structure (i.e. above tau = 0.1). The
temperature nodes are connected with a spline interpolation
and a prior is used such that t1 < t2 < t3 < t_connect. The
three temperature points are not used if set to ``None``.
delta : float
Proportionality factor in tau = delta * press_cgs**alpha.
alpha : float
Power law index in
:math:`\\tau = \\delta * P_\\mathrm{cgs}**\\alpha`.
For the tau model: use the proximity to the
:math:`\\kappa_\\mathrm{rosseland}` photosphere as prior.
tint : float
Internal temperature for the Eddington model.
press : np.ndarray
Pressure profile (bar).
metallicity : float
Metallicity [Fe/H]. Required for the ``nabla_ad``
interpolation.
c_o_ratio : float
Carbon-to-oxygen ratio. Required for the ``nabla_ad``
interpolation.
conv : bool
Enforce a convective adiabat.
Returns
-------
np.ndarray
Temperature profile (K) for ``press``.
float
Pressure (bar) where the optical depth is 1.
float, None
Pressure (bar) at the radiative-convective boundary.
"""
# Convert pressures from bar to cgs units
press_cgs = press * 1e6
# Calculate the optical depth
tau = delta * press_cgs**alpha
# Calculate the Eddington temperature
tedd = (3.0 / 4.0 * tint**4.0 * (2.0 / 3.0 + tau)) ** 0.25
# Import interpol_abundances here because it slows down importing
# species otherwise. Importing interpol_abundances is only slow
# the first time, which occurs at the start of the run_multinest
# method of AtmosphericRetrieval
if "poor_mans_nonequ_chem" in sys.modules:
from poor_mans_nonequ_chem.poor_mans_nonequ_chem import interpol_abundances
else:
from petitRADTRANS.poor_mans_nonequ_chem.poor_mans_nonequ_chem import (
interpol_abundances,
)
ab = interpol_abundances(
np.full(tedd.shape[0], c_o_ratio),
np.full(tedd.shape[0], metallicity),
tedd,
press,
)
nabla_ad = ab["nabla_ad"]
# Enforce convective adiabat
if conv:
# Calculate the current, radiative temperature gradient
nab_rad = np.diff(np.log(tedd)) / np.diff(np.log(press_cgs))
# Extend to array of same length as pressure structure
nabla_rad = np.ones_like(tedd)
nabla_rad[0] = nab_rad[0]
nabla_rad[-1] = nab_rad[-1]
nabla_rad[1:-1] = (nab_rad[1:] + nab_rad[:-1]) / 2.0
# Where is the atmosphere convectively unstable?
conv_index = nabla_rad > nabla_ad
if np.argwhere(conv_index).size == 0:
conv_press = None
else:
conv_bound = np.amin(np.argwhere(conv_index))
conv_press = press[conv_bound]
tfinal = None
for i in range(10):
if i == 0:
t_take = copy.copy(tedd)
else:
t_take = copy.copy(tfinal)
ab = interpol_abundances(
np.full(t_take.shape[0], c_o_ratio),
np.full(t_take.shape[0], metallicity),
t_take,
press,
)
nabla_ad = ab["nabla_ad"]
# Calculate the average nabla_ad between the layers
nabla_ad_mean = nabla_ad
nabla_ad_mean[1:] = (nabla_ad[1:] + nabla_ad[:-1]) / 2.0
# What are the increments in temperature due to convection
tnew = nabla_ad_mean[conv_index] * np.mean(np.diff(np.log(press_cgs)))
# What is the last radiative temperature?
tstart = np.log(t_take[~conv_index][-1])
# Integrate and translate to temperature
# from log(temperature)
tnew = np.exp(np.cumsum(tnew) + tstart)
# Add upper radiative and lower covective
# part into one single array
tfinal = copy.copy(t_take)
tfinal[conv_index] = tnew
if np.max(np.abs(t_take - tfinal) / t_take) < 0.01:
# print('n_ad', 1./(1.-nabla_ad[conv_index]))
break
else:
tfinal = tedd
conv_press = None
# Add the three temperature-point P-T description above tau = 0.1
@typechecked
def press_tau(tau: float) -> float:
"""
Function to return the pressure in cgs units at a given
optical depth.
Parameters
----------
tau : float
Optical depth.
Returns
-------
float
Pressure (cgs) at optical depth ``tau``.
"""
return (tau / delta) ** (1.0 / alpha)
# Where is the uppermost pressure of the
# Eddington radiative structure?
p_bot_spline = press_tau(0.1)
if temp_3 is None:
tret = tfinal
else:
for i_intp in range(2):
if i_intp == 0:
# Create the pressure coordinates for the spline
# support nodes at low pressure
support_points_low = np.logspace(
np.log10(press_cgs[0]), np.log10(p_bot_spline), 4
)
# Create the pressure coordinates for the spline
# support nodes at high pressure, the corresponding
# temperatures for these nodes will be taken from
# the radiative-convective solution
support_points_high = 10.0 ** np.arange(
np.log10(p_bot_spline),
np.log10(press_cgs[-1]),
np.diff(np.log10(support_points_low))[0],
)
# Combine into one support node array, don't add
# the p_bot_spline point twice.
support_points = np.zeros(
len(support_points_low) + len(support_points_high) - 1
)
support_points[:4] = support_points_low
support_points[4:] = support_points_high[1:]
else:
# Create the pressure coordinates for the spline
# support nodes at low pressure
support_points_low = np.logspace(
np.log10(press_cgs[0]), np.log10(p_bot_spline), 7
)
# Create the pressure coordinates for the spline
# support nodes at high pressure, the corresponding
# temperatures for these nodes will be taken from
# the radiative-convective solution
support_points_high = np.logspace(
np.log10(p_bot_spline), np.log10(press_cgs[-1]), 7
)
# Combine into one support node array, don't add
# the p_bot_spline point twice.
support_points = np.zeros(
len(support_points_low) + len(support_points_high) - 1
)
support_points[:7] = support_points_low
support_points[7:] = support_points_high[1:]
# Define the temperature values at the node points
t_support = np.zeros_like(support_points)
if i_intp == 0:
tfintp = interp1d(press_cgs, tfinal)
# The temperature at p_bot_spline (from the
# radiative-convective solution)
t_support[len(support_points_low) - 1] = tfintp(p_bot_spline)
# if temp_3 is not None:
# The temperature at pressures below
# p_bot_spline (free parameters)
t_support[: len(support_points_low) - 1] = temp_3
# else:
# t_support[:3] = tfintp(support_points_low[:3])
# The temperature at pressures above p_bot_spline
# (from the radiative-convective solution)
t_support[len(support_points_low) :] = tfintp(
support_points[len(support_points_low) :]
)
else:
tfintp1 = interp1d(press_cgs, tret)
t_support[: len(support_points_low) - 1] = tfintp1(
support_points[: len(support_points_low) - 1]
)
tfintp = interp1d(press_cgs, tfinal)
# The temperature at p_bot_spline (from
# the radiative-convective solution)
t_support[len(support_points_low) - 1] = tfintp(p_bot_spline)
try:
t_support[len(support_points_low) :] = tfintp(
support_points[len(support_points_low) :]
)
except ValueError:
return None, None, None
# Make the temperature spline interpolation to be returned
# to the user tret = spline(np.log10(support_points),
# t_support, np.log10(press_cgs), order = 3)
cs = PchipInterpolator(np.log10(support_points), t_support)
tret = cs(np.log10(press_cgs))
# Return the temperature, the pressure at tau = 1
# The temperature at the connection point: tfintp(p_bot_spline)
# The last two are needed for the priors on the P-T profile.
return tret, press_tau(1.0) / 1e6, conv_press
[docs]
@typechecked
def pt_spline_interp(
knot_press: np.ndarray,
knot_temp: np.ndarray,
pressure: np.ndarray,
pt_smooth: Optional[float] = 0.3,
) -> np.ndarray:
"""
Function for interpolating the P-T nodes with a PCHIP 1-D monotonic
cubic interpolation. The interpolated temperature is smoothed with
a Gaussian kernel of width 0.3 dex in pressure (see Piette &
Madhusudhan 2020).
Parameters
----------
knot_press : np.ndarray
Pressure knots (bar).
knot_temp : np.ndarray
Temperature knots (K).
pressure : np.ndarray
Pressure points (bar) at which the temperatures is
interpolated.
pt_smooth : float, dict, None
Standard deviation of the Gaussian kernel that is used for
smoothing the P-T profile, after the temperature nodes
have been interpolated to a higher pressure resolution.
The argument should be given as
:math:`\\log10{P/\\mathrm{bar}}`, with the default value
set to 0.3 dex. No smoothing is applied if the argument
is set to ``None``.
Returns
-------
np.ndarray
Interpolated, smoothed temperature points (K).
"""
pt_interp = PchipInterpolator(np.log10(knot_press), knot_temp)
temp_interp = pt_interp(np.log10(pressure))
log_press = np.log10(pressure)
log_diff = np.mean(np.diff(log_press))
if np.std(np.diff(log_press)) / log_diff > 1e-6:
raise ValueError("Expecting equally spaced pressures in log space.")
if pt_smooth is not None:
temp_interp = gaussian_filter(
temp_interp, sigma=pt_smooth / log_diff, mode="nearest"
)
return temp_interp
[docs]
@typechecked
def create_pt_profile(
cube,
cube_index: Dict[str, float],
pt_profile: str,
pressure: np.ndarray,
knot_press: Optional[np.ndarray],
metallicity: float,
c_o_ratio: float,
pt_smooth: Optional[Union[float, Dict[str, float]]] = 0.3,
) -> Tuple[
Optional[np.ndarray], Optional[np.ndarray], Optional[float], Optional[float]
]:
"""
Function for creating a pressure-temperature profile.
Parameters
----------
cube : LP_c_double
Unit cube.
cube_index : dict
Dictionary with the index of each parameter in the ``cube``.
pt_profile : str
The parametrization for the pressure-temperature profile
('molliere', 'free', 'monotonic', 'eddington', 'gradient').
pressure : np.ndarray
Pressure points (bar) at which the temperatures is
interpolated.
knot_press : np.ndarray, None
Pressure knots (bar), which are required when the argument of
``pt_profile`` is either 'free' or 'monotonic'.
metallicity : float
Metallicity [Fe/H].
c_o_ratio : float, None
Carbon-to-oxgen ratio.
pt_smooth : float, dict, None
Standard deviation of the Gaussian kernel that is used for
smoothing the P-T profile, after the temperature nodes
have been interpolated to a higher pressure resolution.
The argument should be given as
:math:`\\log10{P/\\mathrm{bar}}`, with the default value
set to 0.3 dex. No smoothing is applied if the argument
is set to ``None``.
Returns
-------
np.ndarray
Temperatures (K).
np.ndarray, None
Temperature at the knots (K). A ``None`` is returned if
``pt_profile`` is set to 'molliere' or 'eddington'.
float, None
Pressure (bar) where the optical depth is 1.
float, None
Pressure (bar) at the radiative-convective boundary.
"""
knot_temp = None
if pt_profile == "molliere":
temp, phot_press, conv_press = pt_ret_model(
np.array(
[cube[cube_index["t1"]], cube[cube_index["t2"]], cube[cube_index["t3"]]]
),
10.0 ** cube[cube_index["log_delta"]],
cube[cube_index["alpha"]],
cube[cube_index["tint"]],
pressure,
metallicity,
c_o_ratio,
)
elif pt_profile == "mod-molliere":
temp, phot_press, conv_press = pt_ret_model(
None,
10.0 ** cube[cube_index["log_delta"]],
cube[cube_index["alpha"]],
cube[cube_index["tint"]],
pressure,
metallicity,
c_o_ratio,
)
elif pt_profile == "gradient":
num_layer = 6 # could make a variable in the future
layer_pt_slopes = np.ones(num_layer) * np.nan
for index in range(num_layer):
layer_pt_slopes[index] = cube[cube_index[f"PTslope_{num_layer - index}"]]
try:
from petitRADTRANS.physics import dTdP_temperature_profile
except ImportError as import_error:
raise ImportError(
"Can't import the dTdP profile function from "
"petitRADTRANS, check that the version of pRT "
"includes this function in petitRADTRANS.physics",
import_error,
)
temp = dTdP_temperature_profile(
pressure,
num_layer, # could change in the future
layer_pt_slopes,
cube[cube_index["T_bottom"]],
)
phot_press = None
conv_press = None
elif pt_profile in ["free", "monotonic"]:
knot_temp = []
for i in range(knot_press.shape[0]):
knot_temp.append(cube[cube_index[f"t{i}"]])
knot_temp = np.asarray(knot_temp)
nan_count = np.sum(np.isnan(knot_temp))
if nan_count > 0:
# TODO Not clear why this can happen
warnings.warn(f"Found {nan_count} NaN values in sampled temperature nodes.")
return None, None, None, None
temp = pt_spline_interp(knot_press, knot_temp, pressure, pt_smooth)
phot_press = None
conv_press = None
elif pt_profile == "eddington":
# Eddington approximation
# delta = kappa_ir/gravity
tau = pressure * 1e6 * 10.0 ** cube[cube_index["log_delta"]]
temp = (0.75 * cube[cube_index["tint"]] ** 4.0 * (2.0 / 3.0 + tau)) ** 0.25
phot_press = None
conv_press = None
return temp, knot_temp, phot_press, conv_press
[docs]
@typechecked
def make_half_pressure_better(
p_base: Dict[str, float], pressure: np.ndarray
) -> Tuple[np.ndarray, np.ndarray]:
"""
Function for reducing the number of pressure layers from 1440 to
~100 (depending on the number of cloud species) with a refinement
around the cloud decks.
Parameters
----------
p_base : dict
Dictionary with the base of the cloud deck for all cloud
species. The keys in the dictionary are included for example
as MgSiO3(c).
pressure : np.ndarray
Pressures (bar) at high resolution (1440 points).
Returns
-------
np.ndarray
Pressures (bar) at lower resolution (60 points) but with a
refinement around the position of the cloud decks.
np.ndarray, None
The indices of the pressures that have been selected from
the input array ``pressure``.
"""
press_plus_index = np.zeros(len(pressure) * 2).reshape(len(pressure), 2)
press_plus_index[:, 0] = pressure
press_plus_index[:, 1] = range(len(pressure))
press_small = press_plus_index[::24, :]
press_plus_index = press_plus_index[::2, :]
indexes_small = press_small[:, 0] > 0.0
indexes = press_plus_index[:, 0] > 0.0
for key, P_cloud in p_base.items():
indexes_small = indexes_small & (
(np.log10(press_small[:, 0] / P_cloud) > 0.05)
| (np.log10(press_small[:, 0] / P_cloud) < -0.3)
)
indexes = indexes & (
(np.log10(press_plus_index[:, 0] / P_cloud) > 0.05)
| (np.log10(press_plus_index[:, 0] / P_cloud) < -0.3)
)
press_cut = press_plus_index[~indexes, :]
press_small_cut = press_small[indexes_small, :]
press_out = np.zeros((len(press_cut) + len(press_small_cut)) * 2).reshape(
(len(press_cut) + len(press_small_cut)), 2
)
press_out[: len(press_small_cut), :] = press_small_cut
press_out[len(press_small_cut) :, :] = press_cut
press_out = np.sort(press_out, axis=0)
return press_out[:, 0], press_out[:, 1].astype("int")
[docs]
@typechecked
def create_abund_dict(
abund_in: Dict[str, np.ndarray],
line_species: List[str],
chemistry: str,
pressure_grid: str = "smaller",
indices: Optional[np.ndarray] = None,
) -> Dict[str, np.ndarray]:
"""
Function to update the names in the abundance dictionary.
Parameters
----------
abund_in : dict
Dictionary with the mass fractions.
line_species : list
List with the line species.
chemistry : str
Chemistry type ('equilibrium' or 'free').
pressure_grid : str
The type of pressure grid that is used for the radiative
transfer. Either 'standard', to use 180 layers both for the
atmospheric structure (e.g. when interpolating the abundances)
and 180 layers with the radiative transfer, or 'smaller' to
use 60 (instead of 180) with the radiative transfer, or 'clouds'
to start with 1440 layers but resample to ~100 layers (depending
on the number of cloud species) with a refinement around the
cloud decks. For cloudless atmospheres it is recommended to use
'smaller', which runs faster than 'standard' and provides
sufficient accuracy. For cloudy atmosphere, one can test with
'smaller' but it is recommended to use 'clouds' for improved
accuracy fluxes.
indices : np.ndarray, None
Pressure indices from the adaptive refinement in a cloudy
atmosphere. Only required with ``pressure_grid='clouds'``.
Otherwise, the argument can be set to ``None``.
Returns
-------
dict
Dictionary with the updated names of the abundances.
"""
# Create a dictionary with the updated abundance names
abund_out = {}
if indices is not None:
for item in line_species:
if chemistry == "equilibrium":
item_replace = item.replace("_R_10", "")
item_replace = item_replace.replace("_R_30", "")
item_replace = item_replace.replace("_all_iso_HITEMP", "")
item_replace = item_replace.replace("_all_iso_Chubb", "")
item_replace = item_replace.replace("_all_iso", "")
item_replace = item_replace.replace("_HITEMP", "")
item_replace = item_replace.replace("_main_iso", "")
item_replace = item_replace.replace("_lor_cut", "")
item_replace = item_replace.replace("_allard", "")
item_replace = item_replace.replace("_burrows", "")
item_replace = item_replace.replace("_all_Plez", "")
item_replace = item_replace.replace("_all_Exomol", "")
item_replace = item_replace.replace("_Plez", "")
abund_out[item] = abund_in[item_replace][indices]
elif chemistry == "free":
abund_out[item] = abund_in[item][indices]
if "Fe(c)" in abund_in:
abund_out["Fe(c)"] = abund_in["Fe(c)"][indices]
if "MgSiO3(c)" in abund_in:
abund_out["MgSiO3(c)"] = abund_in["MgSiO3(c)"][indices]
if "Mg2SiO4(c)" in abund_in:
abund_out["Mg2SiO4(c)"] = abund_in["Mg2SiO4(c)"][indices]
if "Al2O3(c)" in abund_in:
abund_out["Al2O3(c)"] = abund_in["Al2O3(c)"][indices]
if "Na2S(c)" in abund_in:
abund_out["Na2S(c)"] = abund_in["Na2S(c)"][indices]
if "KCL(c)" in abund_in:
abund_out["KCL(c)"] = abund_in["KCL(c)"][indices]
abund_out["H2"] = abund_in["H2"][indices]
abund_out["He"] = abund_in["He"][indices]
elif pressure_grid == "smaller":
for item in line_species:
if chemistry == "equilibrium":
item_replace = item.replace("_R_10", "")
item_replace = item_replace.replace("_R_30", "")
item_replace = item_replace.replace("_all_iso_HITEMP", "")
item_replace = item_replace.replace("_all_iso_Chubb", "")
item_replace = item_replace.replace("_all_iso", "")
item_replace = item_replace.replace("_HITEMP", "")
item_replace = item_replace.replace("_main_iso", "")
item_replace = item_replace.replace("_lor_cut", "")
item_replace = item_replace.replace("_allard", "")
item_replace = item_replace.replace("_burrows", "")
item_replace = item_replace.replace("_all_Plez", "")
item_replace = item_replace.replace("_all_Exomol", "")
item_replace = item_replace.replace("_Plez", "")
abund_out[item] = abund_in[item_replace][::3]
elif chemistry == "free":
abund_out[item] = abund_in[item][::3]
if "Fe(c)" in abund_in:
abund_out["Fe(c)"] = abund_in["Fe(c)"][::3]
if "MgSiO3(c)" in abund_in:
abund_out["MgSiO3(c)"] = abund_in["MgSiO3(c)"][::3]
if "Mg2SiO4(c)" in abund_in:
abund_out["Mg2SiO4(c)"] = abund_in["Mg2SiO4(c)"][::3]
if "Al2O3(c)" in abund_in:
abund_out["Al2O3(c)"] = abund_in["Al2O3(c)"][::3]
if "Na2S(c)" in abund_in:
abund_out["Na2S(c)"] = abund_in["Na2S(c)"][::3]
if "KCL(c)" in abund_in:
abund_out["KCL(c)"] = abund_in["KCL(c)"][::3]
abund_out["H2"] = abund_in["H2"][::3]
abund_out["He"] = abund_in["He"][::3]
else:
for item in line_species:
if chemistry == "equilibrium":
item_replace = item.replace("_R_10", "")
item_replace = item_replace.replace("_R_30", "")
item_replace = item_replace.replace("_all_iso_HITEMP", "")
item_replace = item_replace.replace("_all_iso_Chubb", "")
item_replace = item_replace.replace("_all_iso", "")
item_replace = item_replace.replace("_HITEMP", "")
item_replace = item_replace.replace("_main_iso", "")
item_replace = item_replace.replace("_lor_cut", "")
item_replace = item_replace.replace("_allard", "")
item_replace = item_replace.replace("_burrows", "")
item_replace = item_replace.replace("_all_Plez", "")
item_replace = item_replace.replace("_all_Exomol", "")
item_replace = item_replace.replace("_Plez", "")
abund_out[item] = abund_in[item_replace]
elif chemistry == "free":
abund_out[item] = abund_in[item]
if "Fe(c)" in abund_in:
abund_out["Fe(c)"] = abund_in["Fe(c)"]
if "MgSiO3(c)" in abund_in:
abund_out["MgSiO3(c)"] = abund_in["MgSiO3(c)"]
if "Mg2SiO4(c)" in abund_in:
abund_out["Mg2SiO4(c)"] = abund_in["Mg2SiO4(c)"]
if "Al2O3(c)" in abund_in:
abund_out["Al2O3(c)"] = abund_in["Al2O3(c)"]
if "Na2S(c)" in abund_in:
abund_out["Na2S(c)"] = abund_in["Na2S(c)"]
if "KCL(c)" in abund_in:
abund_out["KCL(c)"] = abund_in["KCL(c)"]
abund_out["H2"] = abund_in["H2"]
abund_out["He"] = abund_in["He"]
# Correction for the nuclear spin degeneracy that was not included
# in the partition function. See Charnay et al. (2018)
if "FeH" in abund_out:
abund_out["FeH"] = abund_out["FeH"] / 2.0
return abund_out
[docs]
@typechecked
def calc_spectrum_clear(
rt_object,
pressure: np.ndarray,
temperature: np.ndarray,
log_g: float,
c_o_ratio: Optional[float],
metallicity: Optional[float],
p_quench: Optional[float],
log_x_abund: Optional[dict],
chemistry: str,
knot_press_abund: Optional[np.ndarray],
abund_smooth: Optional[float],
pressure_grid: str = "smaller",
contribution: bool = False,
) -> Tuple[np.ndarray, np.ndarray, Optional[np.ndarray]]:
"""
Function to simulate an emission spectrum of a clear atmosphere.
The function supports both equilibrium chemistry
(``chemistry='equilibrium'``) and free abundances
(``chemistry='free'``).
rt_object : petitRADTRANS.radtrans.Radtrans
Instance of ``Radtrans``.
pressure : np.ndarray
Array with the pressure points (bar).
temperature : np.ndarray
Array with the temperature points (K) corresponding to
``pressure``.
log_g : float
Log10 of the surface gravity (cm s-2).
c_o_ratio : float, None
Carbon-to-oxygen ratio.
metallicity : float, None
Metallicity.
p_quench : float, None
Quenching pressure (bar).
log_x_abund : dict, None
Dictionary with the log10 of the abundances. Only required when
``chemistry='free'``.
chemistry : str
Chemistry type ('equilibrium' or 'free').
knot_press_abund : np.ndarray, None
Pressure nodes at which the abundances are sampled. Only
required when ``chemistry='free'``.
abund_smooth : float, None
Standard deviation of the Gaussian kernel that is used for
smoothing the abundance profiles, after the abundance nodes
have been interpolated to a higher pressure resolution.
Only required with ```chemistry='free'``` and
``knot_press_abund`` is not set to ``None``. The argument
should be given as :math:`\\log10{P/\\mathrm{bar}}`. No
smoothing is applied if the argument if set to 0 or ``None``.
pressure_grid : str
The type of pressure grid that is used for the radiative
transfer. Either 'standard', to use 180 layers both for the
atmospheric structure (e.g. when interpolating the abundances)
and 180 layers with the radiative transfer, or 'smaller' to use
60 (instead of 180) with the radiative transfer, or 'clouds' to
start with 1440 layers but resample to ~100 layers (depending
on the number of cloud species) with a refinement around the
cloud decks. For cloudless atmospheres it is recommended to use
'smaller', which runs faster than 'standard' and provides
sufficient accuracy. For cloudy atmosphere, one can test with
'smaller' but it is recommended to use 'clouds' for improved
accuracy fluxes.
contribution : bool
Calculate the emission contribution.
Returns
-------
np.ndarray
Wavelength (um).
np.ndarray
Flux (W m-2 um-1).
np.ndarray, None
Emission contribution.
"""
if knot_press_abund is None:
abund_nodes = None
else:
abund_nodes = knot_press_abund.size
if chemistry == "equilibrium":
# Import interpol_abundances here because it slows down
# importing species otherwise. Importing interpol_abundances
# is only slow the first time, which occurs at the start of
# the run_multinest method of AtmosphericRetrieval
if "poor_mans_nonequ_chem" in sys.modules:
from poor_mans_nonequ_chem.poor_mans_nonequ_chem import interpol_abundances
else:
from petitRADTRANS.poor_mans_nonequ_chem.poor_mans_nonequ_chem import (
interpol_abundances,
)
# Chemical equilibrium
abund_in = interpol_abundances(
np.full(pressure.shape, c_o_ratio),
np.full(pressure.shape, metallicity),
temperature,
pressure,
Pquench_carbon=p_quench,
)
# Extract the mean molecular weight
mmw = abund_in["MMW"]
elif chemistry == "free":
# Free abundances
# Create a dictionary with all mass fractions
abund_in = mass_fractions(log_x_abund, rt_object.line_species, abund_nodes)
# Create list of all species
abund_species = rt_object.line_species.copy()
abund_species.append("H2")
abund_species.append("He")
if abund_nodes is None:
# Mean molecular weight
mmw_float = mean_molecular_weight(abund_in)
mmw = np.full(pressure.size, mmw_float)
# Create arrays of constant abundances with pressure
for abund_item in abund_species:
abund_in[abund_item] = np.full(pressure.size, abund_in[abund_item])
else:
# Create arrays of pressure-dependent abundances
for abund_item in abund_species:
knot_abund = np.zeros(knot_press_abund.shape)
for node_idx in range(abund_nodes):
knot_abund[node_idx] = abund_in[f"{abund_item}_{node_idx}"]
del abund_in[f"{abund_item}_{node_idx}"]
nan_count = np.sum(np.isnan(knot_abund))
if nan_count > 0:
warnings.warn(
f"Found {nan_count} NaN values in sampled abundance nodes."
)
return None, None, None
abund_in[abund_item] = pt_spline_interp(
knot_press_abund, knot_abund, pressure, pt_smooth=abund_smooth
)
# Mean molecular weight
mmw = np.zeros(pressure.size)
for press_idx in range(pressure.size):
abund_dict = {}
for abund_item, abund_value in abund_in.items():
abund_dict[abund_item] = abund_value[press_idx]
mmw[press_idx] = mean_molecular_weight(abund_dict)
# Extract every three levels when pressure_grid is set to 'smaller'
if pressure_grid == "smaller":
temperature = temperature[::3]
pressure = pressure[::3]
mmw = mmw[::3]
abundances = create_abund_dict(
abund_in,
rt_object.line_species,
chemistry,
pressure_grid=pressure_grid,
indices=None,
)
# calculate the emission spectrum
rt_object.calc_flux(
temperature, abundances, 10.0**log_g, mmw, contribution=contribution
)
# convert frequency (Hz) to wavelength (cm)
wavel = constants.LIGHT * 1e2 / rt_object.freq
# optionally return the emission contribution
if contribution:
contr_em = rt_object.contr_em
else:
contr_em = None
# return wavelength (micron), flux (W m-2 um-1),
# and emission contribution
return (
1e4 * wavel,
1e-7 * rt_object.flux * constants.LIGHT * 1e2 / wavel**2.0,
contr_em,
)
[docs]
@typechecked
def calc_spectrum_clouds(
rt_object,
pressure: np.ndarray,
temperature: np.ndarray,
c_o_ratio: float,
metallicity: float,
p_quench: Optional[float],
log_x_abund: Optional[dict],
log_x_base: Optional[dict],
cloud_dict: Dict[str, Optional[float]],
log_g: float,
chemistry: str,
knot_press_abund: Optional[np.ndarray],
abund_smooth: Optional[float],
pressure_grid: str = "smaller",
plotting: bool = False,
contribution: bool = False,
tau_cloud: Optional[float] = None,
cloud_wavel: Optional[Tuple[float, float]] = None,
) -> Tuple[
Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], np.ndarray
]:
"""
Function to simulate an emission spectrum of a cloudy atmosphere.
Parameters
----------
rt_object : petitRADTRANS.radtrans.Radtrans
Instance of ``Radtrans``.
pressure : np.ndarray
Array with the pressure points (bar).
temperature : np.ndarray
Array with the temperature points (K) corresponding to
``pressure``.
c_o_ratio : float
Carbon-to-oxygen ratio.
metallicity : float
Metallicity.
p_quench : float, None
Quenching pressure (bar).
log_x_abund : dict, None
Dictionary with the log10 of the abundances. Only required
when ``chemistry='free'``.
log_x_base : dict, None
Dictionary with the log10 of the mass fractions at the cloud
base. Only required when the ``cloud_dict`` contains ``fsed``,
``log_kzz``, and ``sigma_lnorm``.
cloud_dict : dict
Dictionary with the cloud parameters.
log_g : float
Log10 of the surface gravity (cm s-2).
chemistry : str
Chemistry type ('equilibrium' or 'free').
knot_press_abund : np.ndarray, None
Pressure nodes at which the abundances are sampled. Only
required when ``chemistry='free'``.
abund_smooth : float, None
Standard deviation of the Gaussian kernel that is used for
smoothing the abundance profiles, after the abundance nodes
have been interpolated to a higher pressure resolution.
Only required with ```chemistry='free'``` and
``knot_press_abund`` is not set to ``None``. The argument
should be given as :math:`\\log10{P/\\mathrm{bar}}`. No
smoothing is applied if the argument if set to 0 or ``None``.
pressure_grid : str
The type of pressure grid that is used for the radiative
transfer. Either 'standard', to use 180 layers both for the
atmospheric structure (e.g. when interpolating the abundances)
and 180 layers with the radiative transfer, or 'smaller' to
use 60 (instead of 180) with the radiative transfer, or
'clouds' to start with 1440 layers but resample to ~100 layers
(depending on the number of cloud species) with a refinement
around the cloud decks. For cloudless atmospheres it is
recommended to use 'smaller', which runs faster than 'standard'
and provides sufficient accuracy. For cloudy atmosphere, one
can test with 'smaller' but it is recommended to use 'clouds'
for improved accuracy fluxes.
plotting : bool
Create plots.
contribution : bool
Calculate the emission contribution.
tau_cloud : float, None
Total cloud optical that will be used for scaling the cloud
mass fractions. The mass fractions will not be scaled if the
parameter is set to ``None``.
cloud_wavel : tuple(float, float), None
Tuple with the wavelength range (um) that is used for
calculating the median optical depth of the clouds at the
gas-only photosphere and then scaling the cloud optical
depth to the value of ``log_tau_cloud``. The range of
``cloud_wavel`` should be encompassed by the range of
``wavel_range``. The full wavelength range (i.e.
``wavel_range``) is used if the argument is set to ``None``.
Returns
-------
np.ndarray, None
Wavelength (um).
np.ndarray, None
Flux (W m-2 um-1).
np.ndarray, None
Emission contribution.
np.ndarray
Array with mean molecular weight.
"""
if knot_press_abund is None:
abund_nodes = None
else:
abund_nodes = knot_press_abund.size
if chemistry == "equilibrium":
# Import interpol_abundances here because it slows down
# importing species otherwise. Importing interpol_abundances
# is only slow the first time, which occurs at the start
# of the run_multinest method of AtmosphericRetrieval
if "poor_mans_nonequ_chem" in sys.modules:
from poor_mans_nonequ_chem.poor_mans_nonequ_chem import interpol_abundances
else:
from petitRADTRANS.poor_mans_nonequ_chem.poor_mans_nonequ_chem import (
interpol_abundances,
)
# Interpolate the abundances, following chemical equilibrium
abund_in = interpol_abundances(
np.full(pressure.shape, c_o_ratio),
np.full(pressure.shape, metallicity),
temperature,
pressure,
Pquench_carbon=p_quench,
)
# Extract the mean molecular weight
mmw = abund_in["MMW"]
elif chemistry == "free":
# Free abundances
# Create a dictionary with all mass fractions
abund_in = mass_fractions(log_x_abund, rt_object.line_species, abund_nodes)
# Create list of all species
abund_species = rt_object.line_species.copy()
abund_species.append("H2")
abund_species.append("He")
if abund_nodes is None:
# Mean molecular weight
mmw_float = mean_molecular_weight(abund_in)
mmw = np.full(pressure.size, mmw_float)
# Create arrays of constant abundances with pressure
for abund_item in abund_species:
abund_in[abund_item] = np.full(pressure.size, abund_in[abund_item])
else:
# Create arrays of pressure-dependent abundances
for abund_item in abund_species:
knot_abund = np.zeros(knot_press_abund.shape)
for node_idx in range(abund_nodes):
knot_abund[node_idx] = abund_in[f"{abund_item}_{node_idx}"]
del abund_in[f"{abund_item}_{node_idx}"]
nan_count = np.sum(np.isnan(knot_abund))
if nan_count > 0:
warnings.warn(
f"Found {nan_count} NaN values in sampled abundance nodes."
)
return None, None, None, np.zeros(1)
abund_in[abund_item] = pt_spline_interp(
knot_press_abund, knot_abund, pressure, pt_smooth=abund_smooth
)
# Mean molecular weight
mmw = np.zeros(pressure.size)
for press_idx in range(pressure.size):
abund_dict = {}
for abund_item, abund_value in abund_in.items():
abund_dict[abund_item] = abund_value[press_idx]
mmw[press_idx] = mean_molecular_weight(abund_dict)
if log_x_base is not None:
p_base = {}
for cloud_item in log_x_base:
if f"log_p_base_{cloud_item}(c)" in cloud_dict:
p_base_item = 10.0 ** cloud_dict[f"log_p_base_{cloud_item}(c)"]
else:
p_base_item = find_cloud_deck(
cloud_item,
pressure,
temperature,
metallicity,
c_o_ratio,
mmw=np.mean(mmw),
plotting=plotting,
)
p_base[f"{cloud_item}(c)"] = p_base_item
abund_in[f"{cloud_item}(c)"] = np.zeros_like(temperature)
if "fsed" in cloud_dict:
f_sed = cloud_dict["fsed"]
else:
f_sed = cloud_dict[f"fsed_{cloud_item}(c)"]
abund_in[f"{cloud_item}(c)"][pressure < p_base_item] = (
10.0 ** log_x_base[cloud_item]
* (pressure[pressure <= p_base_item] / p_base_item) ** f_sed
)
# Adaptive pressure refinement around the cloud base
if pressure_grid == "clouds":
_, indices = make_half_pressure_better(p_base, pressure)
else:
indices = None
# Update the abundance dictionary
abundances = create_abund_dict(
abund_in,
rt_object.line_species,
chemistry,
pressure_grid=pressure_grid,
indices=indices,
)
# Create dictionary with sedimentation parameters
# Use the same value for all cloud species
fseds = {}
for cloud_item in rt_object.cloud_species:
# The cloud_item has the form of e.g. MgSiO3(c). For
# parametrized cloud opacities, then the number of
# cloud_species is zero so the fseds dictionary is empty
if "fsed" in cloud_dict:
fseds[cloud_item] = cloud_dict["fsed"]
else:
fseds[cloud_item] = cloud_dict[f"fsed_{cloud_item}"]
# Create an array with a constant eddy diffusion coefficient (cm2 s-1)
if "log_kzz" in cloud_dict:
Kzz_use = np.full(pressure.shape, 10.0 ** cloud_dict["log_kzz"])
else:
Kzz_use = None
# Adjust number of atmospheric levels
if pressure_grid == "smaller":
temperature = temperature[::3]
pressure = pressure[::3]
mmw = mmw[::3]
if "log_kzz" in cloud_dict:
Kzz_use = Kzz_use[::3]
elif pressure_grid == "clouds":
temperature = temperature[indices]
pressure = pressure[indices]
mmw = mmw[indices]
if "log_kzz" in cloud_dict:
Kzz_use = Kzz_use[indices]
# Optionally plot the cloud properties
if (
plotting
and Kzz_use is not None
and (
rt_object.wlen_bords_micron[0] != 0.5
and rt_object.wlen_bords_micron[1] != 30.0
)
):
if "CO_all_iso" in abundances:
plt.plot(abundances["CO_all_iso"], pressure, label="CO")
if "CO_all_iso_HITEMP" in abundances:
plt.plot(abundances["CO_all_iso_HITEMP"], pressure, label="CO")
if "CO_all_iso_Chubb" in abundances:
plt.plot(abundances["CO_all_iso_Chubb"], pressure, label="CO")
if "CH4" in abundances:
plt.plot(abundances["CH4"], pressure, label="CH4")
if "H2O" in abundances:
plt.plot(abundances["H2O"], pressure, label="H2O")
if "H2O_HITEMP" in abundances:
plt.plot(abundances["H2O_HITEMP"], pressure, label="H2O")
if "Na" in abundances:
plt.plot(abundances["Na"], pressure, label="Na")
if "K" in abundances:
plt.plot(abundances["K"], pressure, label="K")
if "Na_allard" in abundances:
plt.plot(abundances["Na_allard"], pressure, label="Na")
if "K_allard" in abundances:
plt.plot(abundances["K_allard"], pressure, label="K")
if "Na_burrows" in abundances:
plt.plot(abundances["Na_burrows"], pressure, label="Na")
if "K_burrows" in abundances:
plt.plot(abundances["K_burrows"], pressure, label="K")
plt.xlim(1e-10, 1.0)
plt.ylim(pressure[-1], pressure[0])
plt.yscale("log")
plt.xscale("log")
plt.xlabel("Mass fraction")
plt.ylabel("Pressure (bar)")
if p_quench is not None:
plt.axhline(p_quench, ls="--", color="black")
plt.legend(loc="best")
plt.savefig("abundances.png", bbox_inches="tight")
plt.clf()
plt.plot(temperature, pressure, "o", ls="none", ms=2.0)
for item in log_x_base:
plt.axhline(
p_base[f"{item}(c)"], label=f"Cloud deck {item}", ls="--", color="black"
)
plt.yscale("log")
plt.ylim(1e3, 1e-6)
plt.xlim(0.0, 4000.0)
plt.savefig("pt_cloud_deck.png", bbox_inches="tight")
plt.clf()
for item in log_x_base:
plt.plot(abundances[f"{item}(c)"], pressure)
plt.axhline(p_base[f"{item}(c)"])
plt.yscale("log")
if np.count_nonzero(abundances[f"{item}(c)"]) > 0:
plt.xscale("log")
plt.ylim(1e3, 1e-6)
plt.xlim(1e-10, 1.0)
log_x_base_item = log_x_base[item]
if "fsed" in cloud_dict:
fsed = cloud_dict["fsed"]
else:
fsed = cloud_dict[f"fsed_{item}(c)"]
log_kzz = cloud_dict["log_kzz"]
plt.title(
f"fsed = {fsed:.2f}, log(Kzz) = {log_kzz:.2f}, "
+ f"X_b = {log_x_base_item:.2f}"
)
plt.savefig(f"{item.lower()}_clouds.png", bbox_inches="tight")
plt.clf()
# Turn clouds off
# abundances['MgSiO3(c)'] = np.zeros_like(pressure)
# abundances['Fe(c)'] = np.zeros_like(pressure)
# Reinitiate the pressure layers after make_half_pressure_better
if pressure_grid == "clouds":
rt_object.setup_opa_structure(pressure)
# Width of cloud particle distribution
if "sigma_lnorm" in cloud_dict:
sigma_lnorm = cloud_dict["sigma_lnorm"]
else:
sigma_lnorm = None
if "log_kappa_0" in cloud_dict:
# Cloud model 2
@typechecked
def kappa_abs(wavel_micron: np.ndarray, press_bar: np.ndarray) -> np.ndarray:
p_base = 10.0 ** cloud_dict["log_p_base"] # (bar)
kappa_0 = 10.0 ** cloud_dict["log_kappa_0"] # (cm2 g-1)
# Opacity at 1 um (cm2 g-1) as function of pressure (bar)
# See Eq. 5 in Mollière et al. 2020
kappa_p = kappa_0 * (press_bar / p_base) ** cloud_dict["fsed"]
# Opacity (cm2 g-1) as function of wavelength (um)
# See Eq. 4 in Mollière et al. 2020
kappa_grid, wavel_grid = np.meshgrid(kappa_p, wavel_micron, sparse=True)
kappa_tot = kappa_grid * wavel_grid ** cloud_dict["opa_index"]
kappa_tot[:, press_bar > p_base] = 0.0
# if (
# cloud_dict["opa_knee"] > wavel_micron[0]
# and cloud_dict["opa_knee"] < wavel_micron[-1]
# ):
# indices = np.where(wavel_micron > cloud_dict["opa_knee"])[0]
# for i in range(press_bar.size):
# kappa_tot[indices, i] = (
# kappa_tot[indices[0], i]
# * (wavel_micron[indices] / wavel_micron[indices[0]]) ** -4.0
# )
return (1.0 - cloud_dict["albedo"]) * kappa_tot
@typechecked
def kappa_scat(wavel_micron: np.ndarray, press_bar: np.ndarray):
p_base = 10.0 ** cloud_dict["log_p_base"] # (bar)
kappa_0 = 10.0 ** cloud_dict["log_kappa_0"] # (cm2 g-1)
# Opacity at 1 um (cm2 g-1) as function of pressure (bar)
# See Eq. 5 in Mollière et al. 2020
kappa_p = kappa_0 * (press_bar / p_base) ** cloud_dict["fsed"]
# Opacity (cm2 g-1) as function of wavelength (um)
# See Eq. 4 in Mollière et al. 2020
kappa_grid, wavel_grid = np.meshgrid(kappa_p, wavel_micron, sparse=True)
kappa_tot = kappa_grid * wavel_grid ** cloud_dict["opa_index"]
kappa_tot[:, press_bar > p_base] = 0.0
# if (
# cloud_dict["opa_knee"] > wavel_micron[0]
# and cloud_dict["opa_knee"] < wavel_micron[-1]
# ):
# indices = np.where(wavel_micron > cloud_dict["opa_knee"])[0]
# for i in range(press_bar.size):
# kappa_tot[indices, i] = (
# kappa_tot[indices[0], i]
# * (wavel_micron[indices] / wavel_micron[indices[0]]) ** -4.0
# )
return cloud_dict["albedo"] * kappa_tot
elif "log_kappa_abs" in cloud_dict:
# Powerlaw absorption and scattering opacities
@typechecked
def kappa_abs(wavel_micron: np.ndarray, press_bar: np.ndarray) -> np.ndarray:
p_base = 10.0 ** cloud_dict["log_p_base"] # (bar)
kappa_0 = 10.0 ** cloud_dict["log_kappa_abs"] # (cm2 g-1)
# Opacity at 1 um (cm2 g-1) as function of pressure (bar)
kappa_p = kappa_0 * (press_bar / p_base) ** cloud_dict["fsed"]
# Opacity (cm2 g-1) as function of wavelength (um)
kappa_grid, wavel_grid = np.meshgrid(kappa_p, wavel_micron, sparse=True)
kappa_abs = kappa_grid * wavel_grid ** cloud_dict["opa_abs_index"]
kappa_abs[:, press_bar > p_base] = 0.0
return kappa_abs
if "log_kappa_sca" in cloud_dict:
@typechecked
def kappa_scat(wavel_micron: np.ndarray, press_bar: np.ndarray):
p_base = 10.0 ** cloud_dict["log_p_base"] # (bar)
kappa_0 = 10.0 ** cloud_dict["log_kappa_sca"] # (cm2 g-1)
# Opacity at 1 um (cm2 g-1) as function of pressure (bar)
kappa_p = kappa_0 * (press_bar / p_base) ** cloud_dict["fsed"]
# Opacity (cm2 g-1) as function of wavelength (um)
kappa_grid, wavel_grid = np.meshgrid(kappa_p, wavel_micron, sparse=True)
kappa_sca = kappa_grid * wavel_grid ** cloud_dict["opa_sca_index"]
kappa_sca[:, press_bar > p_base] = 0.0
if (
cloud_dict["lambda_ray"] > wavel_micron[0]
and cloud_dict["lambda_ray"] < wavel_micron[-1]
):
indices = np.where(wavel_micron > cloud_dict["lambda_ray"])[0]
for i in range(press_bar.size):
kappa_sca[indices, i] = (
kappa_sca[indices[0], i]
* (wavel_micron[indices] / wavel_micron[indices[0]]) ** -4.0
)
return kappa_sca
else:
kappa_scat = None
elif "log_kappa_gray" in cloud_dict:
# Gray clouds with cloud top
@typechecked
def kappa_abs(wavel_micron: np.ndarray, press_bar: np.ndarray) -> np.ndarray:
p_top = 10.0 ** cloud_dict["log_cloud_top"] # (bar)
kappa_gray = 10.0 ** cloud_dict["log_kappa_gray"] # (cm2 g-1)
opa_abs = np.full((wavel_micron.size, press_bar.size), kappa_gray)
opa_abs[:, press_bar < p_top] = 0.0
return opa_abs
# Add optional scattering opacity
if "albedo" in cloud_dict:
@typechecked
def kappa_scat(
wavel_micron: np.ndarray, press_bar: np.ndarray
) -> np.ndarray:
# Absorption opacity (cm2 g-1)
opa_abs = kappa_abs(wavel_micron, press_bar)
# Scattering opacity (cm2 g-1)
opa_scat = cloud_dict["albedo"] * opa_abs / (1.0 - cloud_dict["albedo"])
return opa_scat
else:
kappa_scat = None
else:
kappa_abs = None
kappa_scat = None
# Calculate the emission spectrum
rt_object.calc_flux(
temperature,
abundances,
10.0**log_g,
mmw,
sigma_lnorm=sigma_lnorm,
Kzz=Kzz_use,
fsed=fseds,
radius=None,
contribution=contribution,
gray_opacity=None,
Pcloud=None,
kappa_zero=None,
gamma_scat=None,
add_cloud_scat_as_abs=False,
hack_cloud_photospheric_tau=tau_cloud,
give_absorption_opacity=kappa_abs,
give_scattering_opacity=kappa_scat,
cloud_wlen=cloud_wavel,
)
# if (
# hasattr(rt_object, "scaling_physicality")
# and rt_object.scaling_physicality > 1.0
# ):
# # cloud_scaling_factor > 2 * (fsed + 1)
# # Set to None such that -inf will be returned as ln_like
# wavel = None
# f_lambda = None
# contr_em = None
#
# else:
# wavel = 1e6 * constants.LIGHT / rt_object.freq # (um)
#
# # (erg s-1 cm-2 Hz-1) -> (erg s-1 m-2 Hz-1)
# f_lambda = 1e4 * rt_object.flux
#
# # (erg s-1 m-2 Hz-1) -> (erg s-1 m-2 m-1)
# f_lambda *= constants.LIGHT / (1e-6 * wavel) ** 2.0
#
# # (erg s-1 m-2 m-1) -> (erg s-1 m-2 um-1)
# f_lambda *= 1e-6
#
# # (erg s-1 m-2 um-1) -> (W m-2 um-1)
# f_lambda *= 1e-7
#
# # Optionally return the emission contribution
# if contribution:
# contr_em = rt_object.contr_em
# else:
# contr_em = None
if rt_object.flux is None:
wavel = None
f_lambda = None
contr_em = None
else:
wavel = 1e6 * constants.LIGHT / rt_object.freq # (um)
# (erg s-1 cm-2 Hz-1) -> (erg s-1 m-2 Hz-1)
f_lambda = 1e4 * rt_object.flux
# (erg s-1 m-2 Hz-1) -> (erg s-1 m-2 m-1)
f_lambda *= constants.LIGHT / (1e-6 * wavel) ** 2.0
# (erg s-1 m-2 m-1) -> (erg s-1 m-2 um-1)
f_lambda *= 1e-6
# (erg s-1 m-2 um-1) -> (W m-2 um-1)
f_lambda *= 1e-7
# Optionally return the emission contribution
if contribution:
contr_em = rt_object.contr_em
else:
contr_em = None
# if plotting and Kzz_use is None and hasattr(rt_object, "continuum_opa"):
# plt.plot(wavel, rt_object.continuum_opa[:, 0], label="Total continuum opacity")
# plt.plot(
# wavel,
# rt_object.continuum_opa[:, 0] - rt_object.continuum_opa_scat[:, 0],
# label="Absorption continuum opacity",
# )
# plt.plot(
# wavel,
# rt_object.continuum_opa_scat[:, 0],
# label="Scattering continuum opacity",
# )
# plt.xlabel(r"Wavelength ($\mu$m)")
# plt.ylabel("Opacity at smallest pressure")
# plt.yscale("log")
# plt.legend(loc="best")
# plt.savefig("continuum_opacity.png", bbox_inches="tight")
# plt.clf()
return wavel, f_lambda, contr_em, mmw
[docs]
@typechecked
def mass_fractions(
log_x_abund: Dict[str, float],
line_species: List[str],
abund_nodes: Optional[int] = None,
) -> Dict[str, float]:
"""
Function to return a dictionary with the mass fractions of
all species, including molecular hydrogen and helium.
Parameters
----------
log_x_abund : dict
Dictionary with the log10 of the mass fractions of metals.
line_species : list(str)
List with the line species.
abund_nodes : int, None
Number of abundance nodes. The argument can be set to
``None`` if abundances are constant with pressure.
Returns
-------
dict
Dictionary with the mass fractions of all species.
"""
if abund_nodes is None:
# Initiate abundance dictionary
abund = {}
# Initiate the total mass fraction of the metals
metal_sum = 0.0
for line_item in line_species:
if line_item in log_x_abund:
# Add the mass fraction to the dictionary
abund[line_item] = 10.0 ** log_x_abund[line_item]
# Update the total mass fraction of the metals
metal_sum += abund[line_item]
# Mass fraction of H2 and He
ab_h2_he = 1.0 - metal_sum
# Add H2 and He mass fraction to the dictionary
abund["H2"] = ab_h2_he * 0.75
abund["He"] = ab_h2_he * 0.25
else:
# Initiate abundance dictionary
abund = {}
for node_idx in range(abund_nodes):
# Initiate the total mass fraction of the metals
metal_sum = 0.0
for line_item in line_species:
if f"{line_item}_{node_idx}" in log_x_abund:
# Add the mass fraction to the dictionary
abund[f"{line_item}_{node_idx}"] = (
10.0 ** log_x_abund[f"{line_item}_{node_idx}"]
)
# Update the total mass fraction of the metals
metal_sum += abund[f"{line_item}_{node_idx}"]
# Mass fraction of H2 and He
ab_h2_he = 1.0 - metal_sum
# Add H2 and He mass fraction to the dictionary
abund[f"H2_{node_idx}"] = ab_h2_he * 0.75
abund[f"He_{node_idx}"] = ab_h2_he * 0.25
return abund
[docs]
@typechecked
def mean_molecular_weight(mass_frac: Dict[str, float]) -> float:
"""
Function to calculate the mean molecular weight from a
dictionary with mass fractions.
Parameters
----------
mass_frac : dict
Dictionary with the mass fraction of each species.
Returns
-------
float
Mean molecular weight in atomic mass units.
"""
masses = atomic_masses()
mmw_sum = 0.0
for abund_item in mass_frac:
if "_" in abund_item:
mmw_sum += mass_frac[abund_item] / masses[abund_item.split("_")[0]]
else:
mmw_sum += mass_frac[abund_item] / masses[abund_item]
return 1.0 / mmw_sum
[docs]
@typechecked
def potassium_abundance(
log_x_abund: Dict[str, float],
line_species: List[str],
abund_nodes: Optional[int] = None,
) -> Union[float, List[float]]:
"""
Function to calculate the mass fraction of potassium at a solar
ratio of the sodium and potassium abundances.
Parameters
----------
log_x_abund : dict
Dictionary with the log10 of the mass fractions.
line_species : list(str)
List with the line species.
abund_nodes : int, None
Number of abundance nodes. The argument can be set to
``None`` if abundances are constant with pressure.
Returns
-------
float, list(float)
Log10 of the mass fraction of potassium or a list with the
log10 mass fraction of potassium in case ``abund_nodes`` is
not ``None``. The length of the list is equal to the argument
of ``abund_nodes`` in that case.
"""
# Solar volume mixing ratios of Na and K (Asplund et al. 2009)
n_na_solar = 1.60008694353205e-06
n_k_solar = 9.86605611925677e-08
# Get the atomic masses
masses = atomic_masses()
# Create a dictionary with all mass fractions
x_abund = mass_fractions(log_x_abund, line_species, abund_nodes)
if abund_nodes is None:
# Calculate mean molecular weight from the mass fractions
mmw = mean_molecular_weight(x_abund)
# Volume mixing ratio of sodium
if "Na" in log_x_abund:
n_na_abund = x_abund["Na"] * mmw / masses["Na"]
elif "Na_lor_cut" in log_x_abund:
n_na_abund = x_abund["Na_lor_cut"] * mmw / masses["Na"]
elif "Na_allard" in log_x_abund:
n_na_abund = x_abund["Na_allard"] * mmw / masses["Na"]
elif "Na_burrows" in log_x_abund:
n_na_abund = x_abund["Na_burrows"] * mmw / masses["Na"]
# Volume mixing ratio of potassium
n_k_abund = n_na_abund * n_k_solar / n_na_solar
# Mass fraction of potassium
log_x_k = np.log10(n_k_abund * masses["K"] / mmw)
else:
log_x_k = []
for node_idx in range(abund_nodes):
# Calculate mean molecular weight from the mass fractions
abund_dict = {}
for abund_item in line_species:
abund_dict[abund_item] = x_abund[f"{abund_item}_{node_idx}"]
mmw = mean_molecular_weight(abund_dict)
# Volume mixing ratio of sodium
if f"Na_{node_idx}" in log_x_abund:
n_na_abund = x_abund[f"Na_{node_idx}"] * mmw / masses["Na"]
elif f"Na_lor_cut_{node_idx}" in log_x_abund:
n_na_abund = x_abund[f"Na_lor_cut_{node_idx}"] * mmw / masses["Na"]
elif f"Na_allard_{node_idx}" in log_x_abund:
n_na_abund = x_abund[f"Na_allard_{node_idx}"] * mmw / masses["Na"]
elif f"Na_burrows_{node_idx}" in log_x_abund:
n_na_abund = x_abund[f"Na_burrows_{node_idx}"] * mmw / masses["Na"]
# Volume mixing ratio of potassium
n_k_abund = n_na_abund * n_k_solar / n_na_solar
# Mass fraction of potassium
log_x_k.append(np.log10(n_k_abund * masses["K"] / mmw))
return log_x_k
[docs]
@typechecked
def log_x_cloud_base(
c_o_ratio: float, metallicity: float, cloud_fractions: Dict[str, float]
) -> Dict[str, float]:
"""
Function for returning a dictionary with the log10 mass fractions
at the cloud base.
Parameters
----------
c_o_ratio : float
C/O ratio.
metallicity : float
Metallicity, [Fe/H].
cloud_fractions : dict
Dictionary with the log10 mass fractions at the cloud base,
relative to the maximum values allowed from elemental
abundances. The dictionary keys are the cloud species without
the structure and shape index (e.g. Na2S(c) instead of
Na2S(c)_cd).
Returns
-------
dict
Dictionary with the log10 mass fractions at the cloud base.
Compared to the keys of ``cloud_fractions``, the keys in the
returned dictionary are provided without ``(c)`` (e.g. Na2S
instead of Na2S(c)).
"""
log_x_base = {}
for item in cloud_fractions:
# Mass fraction
x_cloud = cloud_mass_fraction(f"{item[:-3]}", metallicity, c_o_ratio)
# Log10 of the mass fraction at the cloud base
log_x_base[f"{item[:-3]}"] = np.log10(10.0 ** cloud_fractions[item] * x_cloud)
return log_x_base
[docs]
@typechecked
def solar_mixing_ratios() -> dict:
"""
Function which returns the volume mixing ratios for solar elemental
abundances (i.e. [Fe/H] = 0); adopted from Asplund et al. (2009).
Returns
-------
dict
Dictionary with the solar number fractions (i.e. volume
mixing ratios).
"""
n_fracs = {}
n_fracs["H"] = 0.9207539305
n_fracs["He"] = 0.0783688694
n_fracs["C"] = 0.0002478241
n_fracs["N"] = 6.22506056949881e-05
n_fracs["O"] = 0.0004509658
n_fracs["Na"] = 1.60008694353205e-06
n_fracs["Mg"] = 3.66558742055362e-05
n_fracs["Al"] = 2.595e-06
n_fracs["Si"] = 2.9795e-05
n_fracs["P"] = 2.36670201997668e-07
n_fracs["S"] = 1.2137900734604e-05
n_fracs["Cl"] = 2.91167958499589e-07
n_fracs["K"] = 9.86605611925677e-08
n_fracs["Ca"] = 2.01439011429255e-06
n_fracs["Ti"] = 8.20622804366359e-08
n_fracs["V"] = 7.83688694089992e-09
n_fracs["Fe"] = 2.91167958499589e-05
n_fracs["Ni"] = 1.52807116806281e-06
return n_fracs
[docs]
@typechecked
def atomic_masses() -> dict:
"""
Function which returns the atomic and molecular masses.
Returns
-------
dict
Dictionary with the atomic and molecular masses.
"""
masses = {}
# Atoms
masses["H"] = 1.0
masses["He"] = 4.0
masses["C"] = 12.0
masses["N"] = 14.0
masses["O"] = 16.0
masses["Na"] = 23.0
masses["Na_lor_cur"] = 23.0
masses["Na_allard"] = 23.0
masses["Na_burrows"] = 23.0
masses["Mg"] = 24.3
masses["Al"] = 27.0
masses["Si"] = 28.0
masses["P"] = 31.0
masses["S"] = 32.0
masses["Cl"] = 35.45
masses["K"] = 39.1
masses["K_lor_cut"] = 39.1
masses["K_allard"] = 39.1
masses["K_burrows"] = 39.1
masses["Ca"] = 40.0
masses["Ti"] = 47.9
masses["V"] = 51.0
masses["Fe"] = 55.8
masses["Ni"] = 58.7
# Molecules
masses["H2"] = 2.0
masses["H2O"] = 18.0
masses["H2O_HITEMP"] = 18.0
masses["H2O_main_iso"] = 18.0
masses["CH4"] = 16.0
masses["CH4_main_iso"] = 16.0
masses["CO2"] = 44.0
masses["CO2_main_iso"] = 44.0
masses["CO"] = 28.0
masses["CO_all_iso"] = 28.0
masses["CO_all_iso_Chubb"] = 28.0
masses["CO_all_iso_HITEMP"] = 28.0
masses["NH3"] = 17.0
masses["NH3_main_iso"] = 17.0
masses["HCN"] = 27.0
masses["C2H2,acetylene"] = 26.0
masses["PH3"] = 34.0
masses["PH3_main_iso"] = 34.0
masses["H2S"] = 34.0
masses["H2S_main_iso"] = 34.0
masses["VO"] = 67.0
masses["VO_Plez"] = 67.0
masses["TiO"] = 64.0
masses["TiO_all_Exomol"] = 64.0
masses["TiO_all_Plez"] = 64.0
masses["FeH"] = 57.0
masses["FeH_main_iso"] = 57.0
masses["OH"] = 17.0
return masses
[docs]
@typechecked
def cloud_mass_fraction(
composition: str, metallicity: float, c_o_ratio: float
) -> float:
"""
Function to calculate the mass fraction for a cloud species.
Parameters
----------
composition : str
Cloud composition ('Fe', 'MgSiO3', 'Mg2SiO4', 'Al2O3',
'Na2S', or 'KCL').
metallicity : float
Metallicity [Fe/H].
c_o_ratio : float
Carbon-to-oxygen ratio.
Returns
-------
float
Mass fraction.
"""
# Solar fractional number densities (i.e. volume mixing ratios; VMR)
nfracs = solar_mixing_ratios()
# Atomic masses
masses = atomic_masses()
# Make a copy of the dictionary with the solar number densities
nfracs_use = copy.copy(nfracs)
# Scale the solar number densities by the [Fe/H], except H and He
for item in nfracs:
if item != "H" and item != "He":
nfracs_use[item] = nfracs[item] * 10.0**metallicity
# Adjust the VMR of O with the C/O ratio
nfracs_use["O"] = nfracs_use["C"] / c_o_ratio
if composition == "Fe":
nfrac_cloud = nfracs_use["Fe"]
mass_cloud = masses["Fe"]
elif composition == "MgSiO3":
nfrac_cloud = np.min(
[nfracs_use["Mg"], nfracs_use["Si"], nfracs_use["O"] / 3.0]
)
mass_cloud = masses["Mg"] + masses["Si"] + 3.0 * masses["O"]
elif composition == "Mg2SiO4":
nfrac_cloud = np.min(
[nfracs_use["Mg"] / 2.0, nfracs_use["Si"], nfracs_use["O"] / 4.0]
)
mass_cloud = 2.0 * masses["Mg"] + masses["Si"] + 4.0 * masses["O"]
elif composition == "Al2O3":
nfrac_cloud = np.min([nfracs_use["Al"] / 2.0, nfracs_use["O"] / 3.0])
mass_cloud = 2.0 * masses["Al"] + 3.0 * masses["O"]
elif composition == "Na2S":
nfrac_cloud = np.min([nfracs_use["Na"] / 2.0, nfracs_use["S"]])
mass_cloud = 2.0 * masses["Na"] + masses["S"]
elif composition == "KCL":
nfrac_cloud = np.min([nfracs_use["K"], nfracs_use["Cl"]])
mass_cloud = masses["K"] + masses["Cl"]
# Cloud mass fraction
x_cloud = mass_cloud * nfrac_cloud
mass_norm = 0.0
for item in nfracs_use:
# Sum up the mass fractions of all species
mass_norm += masses[item] * nfracs_use[item]
# Normalize the cloud mass fraction by the total mass fraction
return x_cloud / mass_norm
[docs]
@typechecked
def get_condensation_curve(
composition: str,
press: np.ndarray,
metallicity: float,
c_o_ratio: float,
mmw: float = 2.33,
) -> np.ndarray:
"""
Function to find the base of the cloud deck by intersecting the
P-T profile with the saturation vapor pressure.
Parameters
----------
composition : str
Cloud composition ('Fe', 'MgSiO3', 'Mg2SiO4', 'Al2O3',
'Na2S', or 'KCL').
press : np.ndarray
Pressures (bar).
metallicity : float
Metallicity [Fe/H].
c_o_ratio : float
Carbon-to-oxygen ratio.
mmw : float
Mean molecular weight.
Returns
-------
np.array
Condensation temperatures (K) for the provided input pressures.
"""
if composition == "Fe":
Pc, Tc = return_T_cond_Fe_comb(metallicity, c_o_ratio, mmw)
elif composition == "MgSiO3":
Pc, Tc = return_T_cond_MgSiO3(metallicity, c_o_ratio, mmw)
elif composition == "Mg2SiO4":
Pc, Tc = return_T_cond_Mg2SiO4(metallicity)
elif composition == "Al2O3":
Pc, Tc = return_T_cond_Al2O3(metallicity)
elif composition == "Na2S":
Pc, Tc = return_T_cond_Na2S(metallicity, c_o_ratio, mmw)
elif composition == "KCL":
Pc, Tc = return_T_cond_KCl(metallicity, c_o_ratio, mmw)
else:
raise ValueError(
f"The '{composition}' composition is not "
"supported by get_condensation_curve."
)
index = (Pc > 1e-8) & (Pc < 1e5)
Pc, Tc = Pc[index], Tc[index]
tcond_p = interp1d(Pc, Tc)
return tcond_p(press)
[docs]
@typechecked
def find_cloud_deck(
composition: str,
press: np.ndarray,
temp: np.ndarray,
metallicity: float,
c_o_ratio: float,
mmw: float = 2.33,
plotting: bool = False,
) -> float:
"""
Function to find the base of the cloud deck by intersecting the
P-T profile with the saturation vapor pressure.
Parameters
----------
composition : str
Cloud composition ('Fe', 'MgSiO3', 'Mg2SiO4', 'Al2O3',
'Na2S', or 'KCL').
press : np.ndarray
Pressures (bar).
temp : np.ndarray
Temperatures (K).
metallicity : float
Metallicity [Fe/H].
c_o_ratio : float
Carbon-to-oxygen ratio.
mmw : float
Mean molecular weight.
plotting : bool
Create a plot.
Returns
-------
float
Pressure (bar) at the base of the cloud deck.
"""
Tcond_on_input_grid = get_condensation_curve(
composition=composition,
press=press,
metallicity=metallicity,
c_o_ratio=c_o_ratio,
mmw=mmw,
)
Tdiff = Tcond_on_input_grid - temp
diff_vec = Tdiff[1:] * Tdiff[:-1]
ind_cdf = diff_vec < 0.0
if len(diff_vec[ind_cdf]) > 0:
P_clouds = (press[1:] + press[:-1])[ind_cdf] / 2.0
P_cloud = float(P_clouds[-1])
else:
P_cloud = 1e-8
if plotting:
plt.plot(temp, press)
plt.plot(Tcond_on_input_grid, press)
plt.axhline(P_cloud, color="red", linestyle="--")
plt.yscale("log")
plt.xlim(0.0, 3000.0)
plt.ylim(1e2, 1e-6)
plt.savefig(f"{composition.lower()}_clouds_cdf.png", bbox_inches="tight")
plt.clf()
return P_cloud
[docs]
@typechecked
def scale_cloud_abund(
params: Dict[str, float],
rt_object,
pressure: np.ndarray,
temperature: np.ndarray,
mmw: np.ndarray,
chemistry: str,
abund_in: Dict[str, np.ndarray],
composition: str,
tau_cloud: float,
pressure_grid: str,
) -> float:
"""
Function to scale the mass fraction of a cloud species to the
requested optical depth.
Parameters
----------
params : dict
Dictionary with the model parameters.
rt_object : petitRADTRANS.radtrans.Radtrans
Instance of ``Radtrans``.
pressure : np.ndarray
Array with the pressure points (bar).
temperature : np.ndarray
Array with the temperature points (K) corresponding
to ``pressure``.
mmw : np.ndarray
Array with the mean molecular weights corresponding
to ``pressure``.
chemistry : str
Chemistry type (only ``'equilibrium'`` is supported).
abund_in : dict
Dictionary with arrays that contain the pressure-dependent,
equilibrium mass fractions of the line species.
composition : sr
Cloud composition ('Fe(c)', 'MgSiO3(c)', 'Mg2SiO4(c)',
'Al2O3(c)', 'Na2S(c)', 'KCl(c)').
tau_cloud : float
Optical depth of the clouds. The returned mass fraction is
scaled such that the optical depth at the shortest wavelength
is equal to ``tau_cloud``.
pressure_grid : str
The type of pressure grid that is used for the radiative
transfer. Either 'standard', to use 180 layers both for the
atmospheric structure (e.g. when interpolating the abundances)
and 180 layers with the radiative transfer, or 'smaller' to
use 60 (instead of 180) with the radiative transfer, or
'clouds' to start with 1440 layers but resample to ~100 layers
(depending on the number of cloud species) with a refinement
around the cloud decks. For cloudless atmospheres it is
recommended to use 'smaller', which runs faster than 'standard'
and provides sufficient accuracy. For cloudy atmosphere, one
can test with 'smaller' but it is recommended to use 'clouds' for
improved accuracy fluxes.
Returns
-------
float
Mass fraction relative to the maximum value allowed from
elemental abundances. The value has been scaled to the
requested optical depth ``tau_cloud`` (at the shortest
wavelength).
"""
# Dictionary with the requested cloud composition and setting the
# log10 of the mass fraction (relative to the maximum value
# allowed from elemental abundances) equal to zero
cloud_fractions = {composition: 0.0}
# Create a dictionary with the log10 of
# the mass fraction at the cloud base
log_x_base = log_x_cloud_base(
params["c_o_ratio"], params["metallicity"], cloud_fractions
)
# Get the pressure (bar) of the cloud base
p_base = find_cloud_deck(
composition[:-3],
pressure,
temperature,
params["metallicity"],
params["c_o_ratio"],
mmw=np.mean(mmw),
plotting=False,
)
# Initialize the cloud abundance in
# the dictionary with mass fractions
abund_in[composition] = np.zeros_like(temperature)
# Set the cloud abundances by scaling
# from the base with the f_sed parameter
abund_in[composition][pressure < p_base] = (
10.0 ** log_x_base[composition[:-3]]
* (pressure[pressure <= p_base] / p_base) ** params["fsed"]
)
# Adaptive pressure refinement around the cloud base
if pressure_grid == "clouds":
_, indices = make_half_pressure_better({composition: p_base}, pressure)
else:
indices = None
# Update the abundance dictionary
abundances = create_abund_dict(
abund_in,
rt_object.line_species,
chemistry,
pressure_grid=pressure_grid,
indices=indices,
)
# Interpolate the line opacities to the temperature structure
if pressure_grid == "standard":
rt_object.interpolate_species_opa(temperature)
mmw_select = mmw.copy()
if "log_kzz" in params:
kzz_select = np.full(pressure.size, 10.0 ** params["log_kzz"])
else:
# Backward compatibility
kzz_select = np.full(pressure.size, 10.0 ** params["kzz"])
elif pressure_grid == "smaller":
rt_object.interpolate_species_opa(temperature[::3])
mmw_select = mmw[::3]
if "log_kzz" in params:
kzz_select = np.full(pressure[::3].size, 10.0 ** params["log_kzz"])
else:
# Backward compatibility
kzz_select = np.full(pressure[::3].size, 10.0 ** params["kzz"])
elif pressure_grid == "clouds":
# Reinitiate the pressure structure
# after make_half_pressure_better
rt_object.setup_opa_structure(pressure[indices])
rt_object.interpolate_species_opa(temperature[indices])
mmw_select = mmw[indices]
if "log_kzz" in params:
kzz_select = np.full(pressure[indices].size, 10.0 ** params["log_kzz"])
else:
# Backward compatibility
kzz_select = np.full(pressure[indices].size, 10.0 ** params["kzz"])
# Set the continuum opacities to zero because
# calc_cloud_opacity adds to existing opacities
rt_object.continuum_opa = np.zeros_like(rt_object.continuum_opa)
rt_object.continuum_opa_scat = np.zeros_like(rt_object.continuum_opa_scat)
rt_object.continuum_opa_scat_emis = np.zeros_like(rt_object.continuum_opa_scat_emis)
# Calculate the cloud opacities for
# the defined atmospheric structure
rt_object.calc_cloud_opacity(
abundances,
mmw_select,
10.0 ** params["logg"],
params["sigma_lnorm"],
fsed=params["fsed"],
Kzz=kzz_select,
radius=None,
add_cloud_scat_as_abs=False,
)
# Calculate the cloud optical depth and set the tau_cloud attribute
rt_object.calc_tau_cloud(10.0 ** params["logg"])
# Extract the wavelength-averaged optical
# depth at the largest pressure
tau_current = np.mean(rt_object.tau_cloud[0, :, 0, -1])
# Set the continuum opacities again to zero
rt_object.continuum_opa = np.zeros_like(rt_object.continuum_opa)
rt_object.continuum_opa_scat = np.zeros_like(rt_object.continuum_opa_scat)
rt_object.continuum_opa_scat_emis = np.zeros_like(rt_object.continuum_opa_scat_emis)
if tau_current > 0.0:
# Scale the mass fraction
log_x_scaled = np.log10(tau_cloud / tau_current)
else:
log_x_scaled = 100.0
return log_x_scaled
[docs]
@typechecked
def cube_to_dict(cube, cube_index: Dict[str, float]) -> Dict[str, float]:
"""
Function to convert the parameter cube into a dictionary.
Parameters
----------
cube : LP_c_double
Cube with the parameters.
cube_index : dict
Dictionary with the index of each parameter in the ``cube``.
Returns
-------
dict
Dictionary with the parameters.
"""
params = {}
for key, value in cube_index.items():
params[key] = cube[value]
return params
[docs]
@typechecked
def list_to_dict(param_list: List[str], sample_val: np.ndarray) -> Dict[str, float]:
"""
Function to convert the parameter cube into a dictionary.
Parameters
----------
param_list : list(str)
List with the parameter labels.
sample_val : np.ndarray
Array with the parameter values, in the same order as
``param_list``.
Returns
-------
dict
Dictionary with the parameters.
"""
sample_dict = {}
for item in param_list:
sample_dict[item] = sample_val[param_list.index(item)]
return sample_dict
[docs]
@typechecked
def return_T_cond_Fe(
FeH: float, CO: float, MMW: float = 2.33
) -> Tuple[np.ndarray, np.ndarray]:
"""
Function for calculating the saturation pressure for solid Fe.
Parameters
----------
FeH : float
Metallicity.
CO : float
Carbon-to-oxygen ratio.
MMW : float
Mean molecular weight.
Returns
-------
np.ndarray
Saturation pressure (bar).
np.ndarray
Temperature (K).
"""
masses = atomic_masses()
T = np.linspace(100.0, 10000.0, 1000)
# Taken from Ackerman & Marley (2001)
# including erratum (P_vap is in bar, not cgs!)
P_vap = lambda x: np.exp(15.71 - 47664.0 / x)
x_cloud = cloud_mass_fraction("Fe", FeH, CO)
return P_vap(T) / (x_cloud * MMW / masses["Fe"]), T
[docs]
@typechecked
def return_T_cond_Fe_l(
FeH: float, CO: float, MMW: float = 2.33
) -> Tuple[np.ndarray, np.ndarray]:
"""
Function for calculating the saturation pressure for liquid Fe.
Parameters
----------
FeH : float
Metallicity.
CO : float
Carbon-to-oxygen ratio.
MMW : float
Mean molecular weight.
Returns
-------
np.ndarray
Saturation pressure (bar).
np.ndarray
Temperature (K).
"""
masses = atomic_masses()
T = np.linspace(100.0, 10000.0, 1000)
# Taken from Ackerman & Marley (2001)
# including erratum (P_vap is in bar, not cgs!)
P_vap = lambda x: np.exp(9.86 - 37120.0 / x)
x_cloud = cloud_mass_fraction("Fe", FeH, CO)
return P_vap(T) / (x_cloud * MMW / masses["Fe"]), T
[docs]
@typechecked
def return_T_cond_Fe_comb(
FeH: float, CO: float, MMW: float = 2.33
) -> Tuple[np.ndarray, np.ndarray]:
"""
Function for calculating the saturation pressure for Fe.
Parameters
----------
FeH : float
Metallicity.
CO : float
Carbon-to-oxygen ratio.
MMW : float
Mean molecular weight.
Returns
-------
np.ndarray
Saturation pressure (bar).
np.ndarray
Temperature (K).
"""
P1, T1 = return_T_cond_Fe(FeH, CO, MMW)
P2, T2 = return_T_cond_Fe_l(FeH, CO, MMW)
retP = np.zeros_like(P1)
index = P1 < P2
retP[index] = P1[index]
retP[~index] = P2[~index]
return retP, T2
[docs]
@typechecked
def return_T_cond_MgSiO3(
FeH: float, CO: float, MMW: float = 2.33
) -> Tuple[np.ndarray, np.ndarray]:
"""
Function for calculating the saturation pressure for MgSiO3.
Parameters
----------
FeH : float
Metallicity.
CO : float
Carbon-to-oxygen ratio.
MMW : float
Mean molecular weight.
Returns
-------
np.ndarray
Saturation pressure (bar).
np.ndarray
Temperature (K).
"""
masses = atomic_masses()
T = np.linspace(100.0, 10000.0, 1000)
# Taken from Ackerman & Marley (2001)
# including erratum (P_vap is in bar, not cgs!)
P_vap = lambda x: np.exp(25.37 - 58663.0 / x)
x_cloud = cloud_mass_fraction("MgSiO3", FeH, CO)
m_mgsio3 = masses["Mg"] + masses["Si"] + 3.0 * masses["O"]
return P_vap(T) / (x_cloud * MMW / m_mgsio3), T
[docs]
@typechecked
def return_T_cond_Mg2SiO4(FeH: float) -> Tuple[np.ndarray, np.ndarray]:
"""
Function for calculating the saturation pressure for Mg2SiO4.
Parameters
----------
FeH : float
Metallicity.
Returns
-------
np.ndarray
Saturation pressure (bar).
np.ndarray
Temperature (K).
"""
# Condensation temperature of Mg2SiO4
# See Eq. 18 in Visscher et al. 2010
temp = np.linspace(100.0, 10000.0, 1000)
P_vap = lambda x: 10.0 ** ((5.89 - 1e4 / temp - 0.73 * FeH) / 0.37)
return P_vap(temp), temp
[docs]
@typechecked
def return_T_cond_Al2O3(FeH: float) -> Tuple[np.ndarray, np.ndarray]:
"""
Function for calculating the condensation temperature for Al2O3.
Parameters
----------
FeH : float
Metallicity.
CO : float
Carbon-to-oxygen ratio.
MMW : float
Mean molecular weight.
Returns
-------
np.ndarray
Saturation pressure (bar).
np.ndarray
Temperature (K).
"""
# Return dictionary with atomic masses
# masses = atomic_masses()
# Create pressures (bar)
pressure = np.logspace(-6, 3, 1000)
# Equilibrium mass fraction of Al2O3
# x_cloud = cloud_mass_fraction('Al2O3', FeH, CO)
# Molecular mass of Al2O3
# m_al2o3 = 3. * masses['Al'] + 2. * masses['O']
# Partial pressure of Al2O3
# part_press = pressure/(x_cloud*MMW/m_al2o3)
# Condensation temperature of Al2O3
# (see Eq. 4 in Wakeford et al. 2017)
t_cond = 1e4 / (
5.014
- 0.2179 * np.log10(pressure)
+ 2.264e-3 * np.log10(pressure) ** 2
- 0.580 * FeH
)
return pressure, t_cond
[docs]
@typechecked
def return_T_cond_Na2S(
FeH: float, CO: float, MMW: float = 2.33
) -> Tuple[np.ndarray, np.ndarray]:
"""
Function for calculating the saturation pressure for Na2S.
Parameters
----------
FeH : float
Metallicity.
CO : float
Carbon-to-oxygen ratio.
MMW : float
Mean molecular weight.
Returns
-------
np.ndarray
Saturation pressure (bar).
np.ndarray
Temperature (K).
"""
masses = atomic_masses()
# Taken from Charnay+2018
T = np.linspace(100.0, 10000.0, 1000)
# This is the partial pressure of Na, so divide by factor 2 to get
# the partial pressure of the hypothetical Na2S gas particles, this
# is OK: there are more S than Na atoms at solar abundance ratios.
P_vap = lambda x: 1e1 ** (8.55 - 13889.0 / x - 0.5 * FeH) / 2.0
x_cloud = cloud_mass_fraction("Na2S", FeH, CO)
m_na2s = 2.0 * masses["Na"] + masses["S"]
return P_vap(T) / (x_cloud * MMW / m_na2s), T
[docs]
@typechecked
def return_T_cond_KCl(
FeH: float, CO: float, MMW: float = 2.33
) -> Tuple[np.ndarray, np.ndarray]:
"""
Function for calculating the saturation pressure for KCl.
Parameters
----------
FeH : float
Metallicity.
CO : float
Carbon-to-oxygen ratio.
MMW : float
Mean molecular weight.
Returns
-------
np.ndarray
Saturation pressure (bar).
np.ndarray
Temperature (K).
"""
masses = atomic_masses()
T = np.linspace(100.0, 10000.0, 1000)
# Taken from Charnay+2018
P_vap = lambda x: 1e1 ** (7.611 - 11382.0 / T)
x_cloud = cloud_mass_fraction("KCL", FeH, CO)
m_kcl = masses["K"] + masses["Cl"]
return P_vap(T) / (x_cloud * MMW / m_kcl), T
[docs]
@typechecked
def convolve_spectrum(
input_wavel: np.ndarray, input_flux: np.ndarray, spec_res: float
) -> np.ndarray:
"""
Function to convolve a spectrum with a Gaussian filter.
Parameters
----------
input_wavel : np.ndarray
Input wavelengths.
input_flux : np.ndarray
Input flux
spec_res : float
Spectral resolution of the Gaussian filter.
Returns
-------
np.ndarray
Convolved spectrum.
"""
# delta_lambda of resolution element is
# FWHM of the LSF's standard deviation
sigma_lsf = 1.0 / spec_res / (2.0 * np.sqrt(2.0 * np.log(2.0)))
# The input spacing of petitRADTRANS is 1e3, but just compute
# it to be sure, or more versatile in the future.
# Also, we have a log-spaced grid, so the spacing is constant
# as a function of wavelength
spacing = np.mean(2.0 * np.diff(input_wavel) / (input_wavel[1:] + input_wavel[:-1]))
# Calculate the sigma to be used with the Gaussian filter
# in units of the input wavelength bins
sigma_filter = sigma_lsf / spacing
return gaussian_filter(input_flux, sigma=sigma_filter, mode="nearest")
[docs]
@typechecked
def quench_pressure(
pressure: np.ndarray,
temperature: np.ndarray,
metallicity: float,
c_o_ratio: float,
log_g: float,
log_kzz: float,
) -> Optional[float]:
"""
Function to determine the CO/CH$_4$ quenching pressure by
intersecting the pressure-dependent timescales of the
vertical mixing and the CO/CH$_4$ reaction rates.
Parameters
----------
pressure : np.ndarray
Array with the pressures (bar).
temperature : np.ndarray
Array with the temperatures (K) corresponding to ``pressure``.
metallicity : float
Metallicity [Fe/H].
c_o_ratio : float
Carbon-to-oxygen ratio.
log_g : float
Log10 of the surface gravity (cm s-2).
log_kzz : float
Log10 of the eddy diffusion coefficient (cm2 s-1).
Returns
-------
float, None
Quenching pressure (bar).
"""
# Interpolate the equilibbrium abundances
co_array = np.full(pressure.shape[0], c_o_ratio)
feh_array = np.full(pressure.shape[0], metallicity)
if "poor_mans_nonequ_chem" in sys.modules:
from poor_mans_nonequ_chem.poor_mans_nonequ_chem import interpol_abundances
else:
from petitRADTRANS.poor_mans_nonequ_chem.poor_mans_nonequ_chem import (
interpol_abundances,
)
abund_eq = interpol_abundances(
co_array, feh_array, temperature, pressure, Pquench_carbon=None
)
# Surface gravity (m s-2)
gravity = 1e-2 * 10.0**log_g
# Mean molecular weight (kg)
mmw = abund_eq["MMW"] * constants.ATOMIC_MASS
# Pressure scale height (m)
h_scale = constants.BOLTZMANN * temperature / (mmw * gravity)
# Diffusion coefficient (m2 s-1)
kzz = 1e-4 * 10.0**log_kzz
# Mixing timescale (s)
t_mix = h_scale**2 / kzz
# Chemical timescale (see Eq. 12 from Zahnle & Marley 2014)
metal = 10.0**metallicity
t_chem = 1.5e-6 * pressure**-1.0 * metal**-0.7 * np.exp(42000.0 / temperature)
# Determine pressure at which t_mix = t_chem
t_diff = t_mix - t_chem
diff_product = t_diff[1:] * t_diff[:-1]
# If t_mix and t_chem intersect then there
# is 1 negative value in diff_product
indices = diff_product < 0.0
if np.sum(indices) == 1:
p_quench = (pressure[1:] + pressure[:-1])[indices] / 2.0
p_quench = p_quench[0]
elif np.sum(indices) == 0:
p_quench = None
else:
raise ValueError(
f"Encountered unexpected number of indices "
f"({np.sum(indices)}) when determining the "
f"intersection of t_mix and t_chem."
)
return p_quench
[docs]
def convective_flux(
press: np.ndarray,
temp: np.ndarray,
mmw: np.ndarray,
nabla_ad: np.ndarray,
kappa_r: np.ndarray,
density: np.ndarray,
c_p: np.ndarray,
gravity: float,
f_bol: float,
mix_length: float = 1.0,
) -> np.ndarray:
"""
Function for calculating the convective flux with mixing-length
theory. This function has been adopted from petitCODE (Paul
Mollière, MPIA) and was converted from Fortran to Python.
Parameters
----------
press : np.ndarray
Array with the pressures (Pa).
temp : np.ndarray
Array with the temperatures (K) at ``pressure``.
mmw : np.ndarray
Array with the mean molecular weights at ``pressure``.
nabla_ad : np.ndarray
Array with the adiabatic temperature gradient at ``pressure``.
kappa_r : np.ndarray
Array with the Rosseland mean opacity (m2 kg-1) at
``pressure``.
density : np.ndarray
Array with the density (kg m-3) at ``pressure``.
c_p : np.ndarray
Array with the specific heat capacity (J kg-1 K-1) at
constant pressure, ``pressure``.
gravity : float
Surface gravity (m s-2).
f_bol : float
Bolometric flux (W m-2) at the top of the atmosphere,
calculated from the low-resolution spectrum.
mix_length : float
Mixing length for the convection in units of the pressure
scale height (default: 1.0).
Returns
-------
np.ndarray
Convective flux (W m-2) at each pressure.
"""
t_transp = (f_bol / constants.SIGMA_SB) ** 0.25 # (K)
nabla_rad = (
3.0 * kappa_r * press * t_transp**4.0 / 16.0 / gravity / temp**4.0
) # (dimensionless)
h_press = (
constants.BOLTZMANN * temp / (mmw * constants.ATOMIC_MASS * gravity)
) # (m)
l_mix = mix_length * h_press # (m)
U = (
(12.0 * constants.SIGMA_SB * temp**3.0)
/ (c_p * density**2.0 * kappa_r * l_mix**2.0)
* np.sqrt(8.0 * h_press / gravity)
)
W = nabla_rad - nabla_ad
# FIXME thesis: 2336U^4W
A = (
1168.0 * U**3.0
+ 2187 * U * W
+ 27.0
* np.sqrt(
3.0
* (2048.0 * U**6.0 + 2236.0 * U**4.0 * W + 2187.0 * U**2.0 * W**2.0)
)
) ** (1.0 / 3.0)
xi = (
19.0 / 27.0 * U
- 184.0 / 27.0 * 2.0 ** (1.0 / 3.0) * U**2.0 / A
+ 2.0 ** (2.0 / 3.0) / 27.0 * A
)
nabla = xi**2.0 + nabla_ad - U**2.0
nabla_e = nabla_ad + 2.0 * U * xi - 2.0 * U**2.0
f_conv = (
density
* c_p
* temp
* np.sqrt(gravity)
* (mix_length * h_press) ** 2.0
/ (4.0 * np.sqrt(2.0))
* h_press**-1.5
* (nabla - nabla_e) ** 1.5
)
f_conv[np.isnan(f_conv)] = 0.0
return f_conv # (W m-2)