# Copyright (c) 2021-2022 Patricio Cubillos
# Pyrat Bay is open-source software under the GPL-2.0 license (see LICENSE)
__all__ = [
'pressure',
'temperature',
'chemistry',
'uniform',
'abundance',
'hydro_g',
'hydro_m',
'rhill',
'stoich',
'mean_weight',
'ideal_gas_density',
'equilibrium_temp',
'transit_path',
'temperature_posterior',
]
import warnings
import numpy as np
import scipy.integrate as si
import scipy.constants as sc
import scipy.interpolate as sip
import mc3.utils as mu
import chemcat as cat
from .. import tools as pt
from .. import constants as pc
from .. import io as io
from . import tmodels
[docs]def pressure(ptop, pbottom, nlayers, units="bar", log=None, verb=0):
"""
Compute a log-scale pressure profile.
Parameters
----------
ptop: String or Float
Pressure at the top of the atmosphere. If string, may contain units.
pbottom: String or Float
Pressure at the bottom of the atmosphere. If string, may contain units.
nlayers: Integer
Number of pressure layers.
units: String
Pressure input units (if not defined in ptop, pbottom).
Available units are: barye, mbar, pascal, bar (default), and atm.
log: Log object
Screen-output log handler.
verb: Integer
Verbosity level (when log is None). Print out when verb > 0.
Returns
-------
press: 1D float ndarray
The pressure profile (in barye units).
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> import pyratbay.constants as pc
>>> nlayers = 9
>>> # These are all equivalent:
>>> p1 = pa.pressure(ptop=1e-6, pbottom=1e2, nlayers=nlayers)
>>> p2 = pa.pressure(1e-6, 1e2, nlayers, 'bar')
>>> p3 = pa.pressure('1e-6 bar', '1e2 bar', nlayers)
>>> p4 = pa.pressure(1e-6*pc.bar, 1e2*pc.bar, nlayers, 'barye')
>>> print(p1/pc.bar)
[1.e-06 1.e-05 1.e-04 1.e-03 1.e-02 1.e-01 1.e+00 1.e+01 1.e+02]
"""
if log is None:
log = mu.Log(verb=verb)
# Unpack pressure input variables:
ptop = pt.get_param(ptop, units, gt=0.0)
pbottom = pt.get_param(pbottom, units, gt=0.0)
ptop_txt = ptop/pt.u(units)
pbot_txt = pbottom/pt.u(units)
if ptop >= pbottom:
log.error(
f'Bottom-layer pressure ({pbot_txt:.2e} {units}) must be '
f'higher than the top-layer pressure ({ptop_txt:.2e} {units}).'
)
# Create pressure array in barye (CGS) units:
press = np.logspace(np.log10(ptop), np.log10(pbottom), nlayers)
log.head(
f'Creating {nlayers}-layer atmospheric model between '
f'{pbot_txt:.1e} and {ptop_txt:.1e} {units}.'
)
return press
[docs]def temperature(tmodel, pressure=None, nlayers=None, log=None, params=None):
"""
Temperature profile wrapper.
Parameters
----------
tmodel: String
Name of the temperature model.
pressure: 1D float ndarray
Atmospheric pressure profile in barye units.
nlayers: Integer
Number of pressure layers.
log: Log object
Screen-output log handler.
params: 1D float ndarray
Temperature model parameters. If None, return a tuple with the
temperature model, its arguments, and the number or required parameters.
Returns
-------
If params is not None:
temperature: 1D float ndarray
The evaluated atmospheric temperature profile.
If params is None:
temp_model: Callable
The atmospheric temperature model.
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> nlayers = 11
>>> # Isothermal profile:
>>> temp_iso = pa.temperature("isothermal", params=1500.0, nlayers=nlayers)
>>> print(temp_iso)
[1500. 1500. 1500. 1500. 1500. 1500. 1500. 1500. 1500. 1500. 1500.]
>>> # Three-channel Eddington-approximation profile:
>>> pressure = pa.pressure(1e-8, 1e2, nlayers, "bar")
>>> params = np.array([-4.84, -0.8, -0.8, 0.5, 1200.0, 100.0])
>>> temp = pa.temperature('guillot', pressure, params=params)
>>> print(temp)
[1046.89057381 1046.89090056 1046.89433798 1046.93040895 1047.30779086
1051.21739055 1088.76131307 1312.57904127 1640.18896334 1659.78818839
1665.09706555]
"""
if log is None:
log = mu.Log()
if pressure is not None and nlayers is None:
nlayers = len(pressure)
if tmodel in pc.tmodels:
temp_model = tmodels.get_model(tmodel, pressure=pressure)
else:
log.error(
f"Invalid temperature model '{tmodel}'. Select from: {pc.tmodels}"
)
if params is None:
return temp_model
else:
if np.size(params) != temp_model.npars:
log.error(
f"Wrong number of parameters ({np.size(params)}) for the "
f"{temp_model.name} temperature model ({temp_model.npars})."
)
temperature = temp_model(params)
log.head(f'\nComputed {tmodel} temperature model.')
return temperature
[docs]def chemistry(
chem_model, pressure, temperature, species,
metallicity=0.0, e_abundances={}, e_scale={}, e_ratio={},
q_uniform=None,
solar_file=None, log=None, verb=1,
atmfile=None, punits='bar',
):
"""
Compute atmospheric abundaces for given pressure and
temperature profiles with either uniform abundances or TEA.
Parameters
----------
chem_model: String
Name of chemistry model.
pressure: 1D float ndarray
Atmospheric pressure profile (barye).
temperature: 1D float ndarray
Atmospheric temperature profile (Kelvin).
species: 1D string list
Output atmospheric composition.
metallicity: Float
Metallicity enhancement factor in dex units relative to solar.
e_abundances: Dictionary
Custom elemental abundances.
The dict contains the name of the element and their custom
abundance in dex units relative to H=12.0.
These values override metallicity.
e_scale: Dictionary
Scaling abundance factor for specified atoms by the respective
values (in dex units, in addition to metallicity scaling).
E.g. (3x solar): e_scale = {'C': np.log10(3.0)}
e_ratio: Dictionary
Custom elemental abundances scaled relative to another element.
The dict contains the pair of elements joined by an underscore
and their ratio in dex units, e.g., for a C/O ratio of 0.8 set
e_ratio = {'C_O': 0.8}.
These values modify the abundances after metallicity and
e_scale have been applied.
solar_file: String
Input solar elemental abundances file (default Asplund et al. 2021).
log: Log object
Screen-output log handler.
verb: Integer
Verbosity level.
atmfile: String
If not None, output file where to save the atmospheric model.
punits: String
Output pressure units.
Returns
-------
model: Callable
The atmospheric chemistry-network model.
Example
-------
>>> import pyratbay.atmosphere as pa
>>> import matplotlib.pyplot as plt
>>> import pyratbay.plots as pp
>>> nlayers = 100
>>> T0 = 1500.0
>>> pressure = pa.pressure('1e-8 bar', '1e3 bar', nlayers)
>>> temperature = pa.temperature('isothermal', pressure, nlayers, params=T0)
>>> species = 'H2O CH4 CO CO2 NH3 C2H2 C2H4 HCN N2 H2 H He H+ e-'.split()
>>> # Equilibrium abundances model:
>>> chem_model = 'tea'
>>> chem_network = pa.chemistry(
>>> chem_model, pressure, temperature, species,)
>>> q_uniform = np.array([
>>> 5e-4, 3e-5, 2e-4, 1e-8, 1e-6, 1e-14, 1e-13, 5e-10,
>>> 1e-4, 0.85, 5e-3, 0.14, 3e-23, 1e-23])
>>> chem_model = 'uniform'
>>> chem_network_unif = pa.chemistry(
>>> chem_model, pressure, temperature, species, q_uniform=q_uniform)
>>> # Plot the results:
>>> ax1 = pp.abundance(
>>> chem_network.vmr, pressure, chem_network.species,
>>> colors='default', xlim=[1e-30, 3.0])
>>> ax2 = pp.abundance(
>>> chem_network_unif.vmr, pressure, chem_network_unif.species,
>>> colors='default', xlim=[1e-30, 3.0], fignum=506)
"""
if solar_file is None:
solar_file = 'asplund_2021'
if log is None:
log = mu.Log(verb=verb)
log.head("\nCompute chemical abundances.")
chem_network = cat.Network(
pressure/pc.bar, temperature, species,
metallicity=metallicity,
e_abundances=e_abundances,
e_scale=e_scale,
e_ratio=e_ratio,
e_source=solar_file,
)
if chem_model == 'uniform':
abundances = [
q_uniform[list(species).index(spec)]
for spec in chem_network.species
]
chem_network.vmr = uniform(
pressure, temperature, chem_network.species, abundances=abundances)
elif chem_model == 'tea':
chem_network.thermochemical_equilibrium()
if atmfile is not None:
header = "# TEA atmospheric file\n\n"
io.write_atm(
atmfile, pressure, temperature,
chem_network.species, chem_network.vmr,
punits=punits, header=header)
return chem_network
[docs]def abundance(
pressure, temperature, species, elements=None,
quniform=None, atmfile=None, punits='bar',
metallicity=0.0, e_abundances={}, e_scale={}, e_ratio={},
solar_file=None, log=None, verb=1,
# To be deprecated:
ncpu=None, xsolar=None, escale=None,
):
"""
Compute atmospheric abundaces for given pressure and
temperature profiles with either uniform abundances or TEA.
Parameters
----------
pressure: 1D float ndarray
Atmospheric pressure profile (barye).
temperature: 1D float ndarray
Atmospheric temperature profile (Kelvin).
species: 1D string list
Output atmospheric composition.
elements: 1D strings list
Input elemental composition (default to minimum list of elements
required to form species).
quniform: 1D float ndarray
If not None, the output species abundances (isobaric).
atmfile: String
If not None, output file where to save the atmospheric model.
punits: String
Output pressure units.
metallicity: Float
Metallicity enhancement factor in dex units relative to solar.
e_abundances: Dictionary
Custom elemental abundances.
The dict contains the name of the element and their custom
abundance in dex units relative to H=12.0.
These values override metallicity.
e_scale: Dict
Scaling abundance factor for specified atoms by the respective
values (in dex units, in addition to metallicity scaling).
E.g. (3x solar): e_scale = {'C': np.log10(3.0)}
solar_file: String
Input solar elemental abundances file (default Asplund et al. 2021).
log: Log object
Screen-output log handler.
verb: Integer
Verbosity level.
ncpu: Integer [DEPRECATED]
Number of parallel CPUs to use in TEA calculation.
xsolar: Float [DEPRECATED]
Metallicity enhancement factor. Deprecated, use metallicity instead.
escale: Dict [DEPRECATED]
Multiplication factor for specified atoms (dict's keys)
by the respective values (on top of the xsolar scaling).
Deprecated, use e_scale instead.
Returns
-------
vmr: 2D float ndarray
Atmospheric volume mixing fraction abundances of shape
[nlayers, nspecies].
Example
-------
>>> import pyratbay.atmosphere as pa
>>> import pyratbay.constants as pc
>>> nlayers = 100
>>> press = np.logspace(-8, 3, nlayers) * pc.bar
>>> temp = np.tile(900.0, nlayers)
>>> species = 'H2O CH4 CO CO2 NH3 C2H2 C2H4 HCN N2 H2 H He'.split()
>>> # Thermochemical equilibrium abundances for requested species:
>>> vmr = pa.abundance(press, temp, species)
"""
if solar_file is None:
solar_file = 'asplund_2021'
if log is None:
log = mu.Log(verb=verb)
# Uniform-abundances profile:
if quniform is not None:
log.head("\nCompute uniform-abundances profile.")
vmr = uniform(
pressure, temperature, species, quniform, punits, log, atmfile)
return vmr
# Deprecated arguments:
if ncpu is not None:
warning_msg = (
"The 'ncpu' argument is deprecated and no longer has any "
"effect, it will be removed in the near future"
)
warnings.warn(warning_msg, category=DeprecationWarning)
if not e_scale and escale is not None:
e_scale = {key: np.log10(val) for key,val in escale.items()}
warning_msg = (
"The 'escale' argument is deprecated and will be removed in "
"the near future, use 'e_scale' instead"
)
warnings.warn(warning_msg, category=DeprecationWarning)
if xsolar is not None:
metallicity = np.log10(xsolar)
warning_msg = (
"The 'xsolar' argument is deprecated and will be removed in "
"the near future, use 'metallicity' instead"
)
warnings.warn(warning_msg, category=DeprecationWarning)
# TEA abundances:
log.head("\nCompute TEA thermochemical-equilibrium abundances profile.")
chem_network = chemistry(
'tea',
pressure, temperature, species,
metallicity=metallicity,
e_abundances=e_abundances,
e_scale=e_scale,
e_ratio=e_ratio,
solar_file=solar_file,
log=log,
verb=verb,
)
vmr = chem_network.vmr
if atmfile is not None:
header = "# TEA atmospheric file\n\n"
io.write_atm(
atmfile, pressure, temperature, chem_network.species, vmr,
punits=punits, header=header,
)
return vmr
[docs]def hydro_g(pressure, temperature, mu, g, p0=None, r0=None):
"""
Calculate radii using the hydrostatic-equilibrium equation considering
a constant gravity.
Parameters
----------
pressure: 1D float ndarray
Atmospheric pressure for each layer (in barye).
temperature: 1D float ndarray
Atmospheric temperature for each layer (in K).
mu: 1D float ndarray
Mean molecular mass for each layer (in g mol-1).
g: Float
Atmospheric gravity (in cm s-2).
p0: Float
Reference pressure level (in barye) where radius(p0) = r0.
r0: Float
Reference radius level (in cm) corresponding to p0.
Returns
-------
radius: 1D float ndarray
Radius for each layer (in cm).
Notes
-----
If the reference values (p0 and r0) are not given, set radius = 0.0
at the bottom of the atmosphere.
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> import pyratbay.constants as pc
>>> nlayers = 11
>>> pressure = pa.pressure(1e-8, 1e2, nlayers, units='bar')
>>> temperature = pa.tmodels.Isothermal(nlayers)(1500.0)
>>> mu = np.tile(2.3, nlayers)
>>> g = pc.G * pc.mjup / pc.rjup**2
>>> r0 = 1.0 * pc.rjup
>>> p0 = 1.0 * pc.bar
>>> # Radius profile in Jupiter radii:
>>> radius = pa.hydro_g(pressure, temperature, mu, g, p0, r0) / pc.rjup
>>> print(radius)
[1.0563673 1.04932138 1.04227547 1.03522956 1.02818365 1.02113774
1.01409182 1.00704591 1. 0.99295409 0.98590818]
"""
# Apply the HE equation:
radius = si.cumtrapz(-pc.k*sc.N_A*temperature / (mu*g), np.log(pressure))
radius = np.concatenate(([0.0], radius))
# Set absolute radii values if p0 and r0 are provided:
if p0 is not None and r0 is not None:
# Find current radius at p0:
radinterp = sip.interp1d(pressure, radius, kind='slinear')
r0_interp = radinterp(p0)
# Set: radius(p0) = r0
radius += r0 - r0_interp
# Set radius = 0 at the bottom of the atmosphere:
else:
radius -= radius[-1]
return radius
[docs]def hydro_m(pressure, temperature, mu, mass, p0, r0):
"""
Calculate radii using the hydrostatic-equilibrium equation considering
a variable gravity: g(r) = G*mass/r**2
Parameters
----------
pressure: 1D float ndarray
Atmospheric pressure for each layer (in barye).
temperature: 1D float ndarray
Atmospheric temperature for each layer (in K).
mu: 1D float ndarray
Mean molecular mass for each layer (in g mol-1).
mass: Float
Object's mass (in g).
p0: Float
Reference pressure level (in barye) where radius(p0) = r0.
r0: Float
Reference radius level (in cm) corresponding to p0.
Returns
-------
radius: 1D float ndarray
Radius for each layer (in cm).
Notes
-----
It is possible that this hydrostatic solution diverges when an
atmosphere is too puffy. In such cases, some returned radii
will have np.inf values (at the top layers).
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> import pyratbay.constants as pc
>>> nlayers = 11
>>> pressure = pa.pressure(1e-8, 1e2, nlayers, units='bar')
>>> temperature = pa.tmodels.Isothermal(nlayers)(1500.0)
>>> mu = np.tile(2.3, nlayers)
>>> mplanet = 1.0 * pc.mjup
>>> r0 = 1.0 * pc.rjup
>>> p0 = 1.0 * pc.bar
>>> # Radius profile in Jupiter radii:
>>> radius = pa.hydro_m(pressure, temperature, mu, mplanet, p0, r0)/pc.rjup
>>> print(radius)
[1.05973436 1.05188019 1.04414158 1.036516 1.029001 1.02159419
1.01429324 1.00709591 1. 0.99300339 0.986104 ]
"""
# Apply the HE equation:
I = si.cumtrapz((pc.k*sc.N_A*temperature)/(pc.G*mu*mass), np.log(pressure))
I = np.concatenate(([0.0], I))
# Find current radius at p0:
radinterp = sip.interp1d(pressure, I, kind='slinear')
I0 = radinterp(p0)
# Set: radius(p0) = r0
radius = 1.0/(I - I0 + 1/r0)
# Divergent profile (radius should be monotonically decreasing):
for j in range(len(radius)-1):
if radius[j] <= radius[j+1]:
radius[0:j+1] = np.inf
return radius
[docs]def rhill(smaxis, mplanet, mstar):
"""
Compute the Hill radius. If any argument is None, return inf.
Parameters
----------
smaxis: Float
Orbital semi-major axis (in cm).
mplanet: Float
Planetary mass (in g).
mstar: Float
Stellar mass (in g).
Returns
-------
rhill: Float
Hill radius of planet.
"""
if smaxis is None or mplanet is None or mstar is None:
return np.inf
rhill = smaxis * (mplanet/(3*mstar))**(1.0/3.0)
return rhill
[docs]def stoich(species):
"""
Extract the elemental composition from a list of species.
Parameters
----------
species: 1D string list
List of species.
Returns
-------
elements: 1D string list
List of elements contained in species list.
stoich: 2D integer ndarray
Stoichiometric elemental values for each species (number of elements).
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> species = ['H2', 'He', 'H2O', 'CO', 'CO2', 'CH4']
>>> elements, stoichs = pa.stoich(species)
>>> print(f'{elements}\n{stoichs}')
['C', 'H', 'He', 'O']
[[0 2 0 0]
[0 0 1 0]
[0 2 0 1]
[1 0 0 1]
[1 0 0 2]
[1 4 0 0]]
"""
# Elemental composition and quantity for each species:
comp, n = [], []
for spec in species:
comp.append([])
n.append([])
for char in spec:
# Special case for electrons:
if spec == 'e-':
comp[-1].append(spec)
n[-1].append(1)
# New element:
elif char.isupper():
comp[-1].append(char)
n[-1].append(1)
# Same element:
elif char.islower():
comp[-1][-1] += char
# Quantity:
elif char.isdigit():
n[-1][-1] = int(char)
# Flatten nested list, and get unique set of elements:
elements = sorted(set([
element
for spec in comp
for element in spec
]))
stoich = np.zeros((len(species), len(elements)), int)
# Count how many elements in each species:
for i in range(len(species)):
for j in range(len(comp[i])):
idx = elements.index(comp[i][j])
stoich[i,idx] = n[i][j]
return elements, stoich
[docs]def mean_weight(abundances, species=None, molfile=None, mass=None):
"""
Calculate the mean molecular weight (a.k.a. mean molecular mass)
for the given abundances composition.
Parameters
----------
abundances: 2D float iterable
Species volume mixing fraction, of shape [nlayers,nmol].
species: 1D string iterable
Species names.
molfile: String
A molecules file with the species info. If None, use
pyratbay's default molecules.dat file.
mass: 1D float ndarray
The mass for each one of the species (g mol-1).
If not None, this variable takes precedence over molfile.
Returns
-------
mu: 1D float ndarray
Mean molecular weight at each layer for the input abundances.
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> species = 'H2 He H2O CO CO2 CH4'.split()
>>> abundances = [[0.8496, 0.15, 1e-4, 1e-4, 1e-8, 1e-4]]
>>> mu = pa.mean_weight(abundances, species)
>>> print(mu)
[2.31939114]
"""
if mass is None and species is None:
raise ValueError('Either species or mass arguments must be specified')
if mass is None:
if molfile is None:
molfile = pc.ROOT + 'pyratbay/data/molecules.dat'
names, mass, radius = io.read_molecs(molfile)
if np.any(~np.isin(species, names)):
with pt.log_error():
missing = species[~np.isin(species, names)]
raise ValueError(f"Missing species masses for {missing}")
mass = np.array([mass[names==spec][0] for spec in species])
return np.sum(np.atleast_2d(abundances)*mass, axis=1)
[docs]def ideal_gas_density(abundances, pressure, temperature):
"""
Use the Ideal gas law to calculate number density in molecules cm-3.
Parameters
----------
abundances: 2D float ndarray
Species volume mixing fraction, of shape [nlayers,nmol].
pressure: 1D ndarray
Atmospheric pressure profile (in barye units).
temperature: 1D ndarray
Atmospheric temperature profile (in kelvin).
Returns
-------
density: 2D float ndarray
Atmospheric density in molecules cm-3.
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> atmfile = "uniform_test.atm"
>>> nlayers = 11
>>> pressure = pa.pressure(1e-8, 1e2, nlayers, units='bar')
>>> temperature = np.tile(1500.0, nlayers)
>>> species = ["H2", "He", "H2O", "CO", "CO2", "CH4"]
>>> abundances = [0.8496, 0.15, 1e-4, 1e-4, 1e-8, 1e-4]
>>> qprofiles = pa.uniform(pressure, temperature, species, abundances)
>>> dens = pa.ideal_gas_density(qprofiles, pressure, temperature)
>>> print(dens[0])
[4.10241993e+10 7.24297303e+09 4.82864869e+06 4.82864869e+06
4.82864869e+02 4.82864869e+06]
"""
return abundances * np.expand_dims(pressure/temperature, axis=1) / pc.k
[docs]def equilibrium_temp(
tstar, rstar, smaxis,
A=0.0, f=1.0,
tstar_unc=0.0, rstar_unc=0.0, smaxis_unc=0.0,
):
r"""
Calculate equilibrium temperature and uncertainty.
Parameters
----------
tstar: Scalar
Effective temperature of host star (in kelvin degrees).
rstar: Scalar
Radius of host star (in cm).
smaxis: Scalar
Orbital semi-major axis (in cm).
A: Scalar
Planetary bond albedo.
f: Scalar
Planetary energy redistribution factor:
f=0.5 no redistribution (total dayside reemission)
f=1.0 good redistribution (4pi reemission)
tstar_unc: Scalar
Effective temperature uncertainty (in kelvin degrees).
rstar_unc: Scalar
Stellar radius uncertainty (in cm).
smaxis_unc: Scalar
Semi-major axis uncertainty (in cm).
Returns
-------
teq: 1D ndarray
Planet equilibrium temperature in kelvin degrees.
teq_unc: 1D ndarray
Equilibrium temperature uncertainty.
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> import pyratbay.constants as pc
>>> import numpy as np
>>> # HD 209458b (Stassun et al. 2017):
>>> tstar = 6091.0
>>> rstar = 1.19 * pc.rsun
>>> smaxis = 0.04747 * pc.au
>>> tstar_unc = 10.0
>>> rstar_unc = 0.02 * pc.rsun
>>> smaxis_unc = 0.00046 * pc.au
>>> A = 0.3
>>> f = np.array([1.0, 0.5])
>>> teq, teq_unc = pa.equilibrium_temp(tstar, rstar, smaxis, A, f,
>>> tstar_unc, rstar_unc, smaxis_unc)
>>> print(f'HD 209458b T_eq =\n '
>>> f'{teq[0]:.1f} +/- {teq_unc[0]:.1f} K (day--night redistributed)\n'
>>> f' {teq[1]:.1f} +/- {teq_unc[1]:.1f} K (instant re-emission)')
HD 209458b T_eq =
1345.1 +/- 13.2 K (day--night redistribution)
1599.6 +/- 15.7 K (instant re-emission)
"""
teq = ((1.0-A)/f)**0.25 * (0.5*rstar/smaxis)**0.5 * tstar
teq_unc = teq * np.sqrt(
( tstar_unc/tstar)**2.0
+ (0.5*smaxis_unc/smaxis)**2.0
+ (0.5*rstar_unc/rstar)**2.0
)
return teq, teq_unc
[docs]def transit_path(radius, nskip=0):
"""
Calculate the distances between layers for a set of rays grazing
an atmosphere defined by concentric spheres at radii radius.
Assume that the grazing rays have an impact parameter = radius.
See for example, Fig 1 of Molliere et al. (2019), AA, 627, 67.
Parameters
----------
radius: 1D float ndarray
Atmospheric radius profile array (from top to bottom).
nskip: Integer
Number of layers to skip from the top of the atmosphere.
Returns
-------
path: List of 1D float ndarrays
List where each element is a 1D array of the paths at an
impact parameter defined.
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> nlayers = 5
>>> radius = np.linspace(5.0, 1.0, nlayers)
>>> path = pa.transit_path(radius)
>>> # First path grazes the outer layer (thus, empty path)
>>> # Second path is sqrt(5.0**2 - 4.0**2), and so on
>>> for p in path:
>>> print(p)
[]
[3.]
[1.35424869 2.64575131]
[1.11847408 1.22803364 2.23606798]
[1.02599614 1.04455622 1.09637632 1.73205081]
>>> # Now, ignore the top layer:
>>> path = pa.transit_path(radius, nskip=1)
>>> for p in path:
>>> print(p)
[]
[]
[2.64575131]
[1.22803364 2.23606798]
[1.04455622 1.09637632 1.73205081]
"""
rad = radius[nskip:]
nlayers = len(rad)
# Empty-filling layers that don't contribute:
path = []
for r in range(nskip):
path.append(np.empty(0, np.double))
# Compute the path for each impact parameter:
r = 0
while r < nlayers:
raypath = np.empty(r, np.double)
for i in range(r):
raypath[i] = (
np.sqrt(rad[i ]**2 - rad[r]**2) -
np.sqrt(rad[i+1]**2 - rad[r]**2)
)
path.append(raypath)
r += 1
return path
[docs]def temperature_posterior(posterior, tmodel, tpars, ifree, pressure):
"""
Compute the median and inter-quantiles regions (68% and 95%)
of a temperature profile posterior.
Parameters
----------
posterior: 2D float ndarray [nsamples, nfree]
A posterior distribution for tmodel.
tmodel: Callable
Temperature-profile model.
tpars: 1D float iterable [npars]
Temperature-profile parameters (including fixed parameters).
ifree: 1D bool iterable [npars]
Mask of free (True) and fixed (False) parameters in tpars.
The number of free parameters must match nfree in posterior.
pressure: 1D float ndarray [nlayers]
The atmospheric pressure profile in barye.
Returns
-------
median: 1D float ndarray [nlayers]
The matplotlib Axes of the figure.
low1: 1D float ndarray [nlayers]
Lower temperature boundary of the 68%-interquantile range.
high1: 1D float ndarray [nlayers]
Upper temperature boundary of the 68%-interquantile range.
low2: 1D float ndarray [nlayers]
Lower temperature boundary of the 95%-interquantile range.
high2: 1D float ndarray [nlayers]
Upper temperature boundary of the 95%-interquantile range.
Examples
--------
>>> import pyratbay.atmosphere as pa
>>> import pyratbay.constants as pc
>>> import pyratbay.plots as pp
>>> import numpy as np
>>> # Non-inverted temperature profile:
>>> pressure = pa.pressure('1e-6 bar', '1e2 bar', nlayers=100)
>>> tmodel = pa.tmodels.Guillot(pressure)
>>> tpars = np.array([-4.0, -1.0, 0.0, 0.0, 1000.0, 0.0])
>>> # Simulate posterior where kappa' and gamma are variable:
>>> nsamples = 5000
>>> ifree = [True, True, False, False, False, False]
>>> posterior = np.array([
>>> np.random.normal(tpars[0], 0.5, nsamples),
>>> np.random.normal(tpars[1], 0.1, nsamples)]).T
>>> tpost = pa.temperature_posterior(
>>> posterior, tmodel, tpars, ifree, pressure)
>>> ax = pp.temperature(pressure, profiles=tpost[0], bounds=tpost[1:])
"""
nlayers = len(pressure)
u, uind, uinv = np.unique(
posterior[:,0], return_index=True, return_inverse=True,
)
nsamples = len(u)
# Evaluate posterior PT profiles:
profiles = np.zeros((nsamples, nlayers), np.double)
tpars = np.array(tpars)
ifree = np.array(ifree)
for i in range(nsamples):
tpars[ifree] = posterior[uind[i]]
profiles[i] = tmodel(tpars)
# Get median and inter-quantile ranges for 68% and 95% percentiles:
low1 = np.zeros(nlayers, np.double)
low2 = np.zeros(nlayers, np.double)
median = np.zeros(nlayers, np.double)
high1 = np.zeros(nlayers, np.double)
high2 = np.zeros(nlayers, np.double)
for i in range(nlayers):
tpost = profiles[uinv,i]
low2[i] = np.percentile(tpost, 2.275)
low1[i] = np.percentile(tpost, 15.865)
median[i] = np.percentile(tpost, 50.000)
high1[i] = np.percentile(tpost, 84.135)
high2[i] = np.percentile(tpost, 97.725)
return median, low1, high1, low2, high2