Radiative equilibrium: WASP-69b

This tutorial shows how perform a radiative-equilibrium calculation to compute an exoplanet temperature profile. We will use WASP-69b as a test case.

Radiative-equilibrium estimations balance the incoming stellar irradiation from the host star and the outgoing planetary thermal emission. Thus, It’s important that the radiative-transfer calculation spans all the way from optical to infrared wavelengths, such that all relevant optical and infrared absorbers are included.

We can break the analysis into the following steps:


Cross sections

In this example we will include line-sampled cross sections for these molecules: H₂O, CO, CO₂, CH₄, SiO, H₂S, HCN, and NH₃. We will work with the latests opacity sources for these species from ExoMol, HITEMP, and Ames.

The Zenodo repository doi.org/10.5281/zenodo.16965391 provides the required cross-section files, ready to use. See the list below for direct links to the files for each molecule. These cross sections have been computed assuming an H₂/He-dominated atmosphere, and terrestrial isotopic ratios. The lines have Voigt profiles with a wing cut-off at 300 HWHM and at 25 cm-1. The grid sampling are:

  • Wavelength: \(0.15-33\) μm, at a constant resolution of \(R=25.000\)

  • Temperature: \(200-5000\) K, with \(\Delta T = 150\) K

  • Pressure: \(1.0^{-9}-1.0^{3}\) bar, equally sampled in log(p) with 4 samples per dex.

Tabulated cross-section files

Species (source)

References

H2O (exomol, pokazatel)

[Polyansky2018]

CO (HITEMP, li)

[Li2015]

CO2 (ames, ai3000k)

[Huang2023]

CH4 (exomol, mm)

[Yurchenko2024a]

H2S (exomol, ayt2)

[Azzam2016] [Chubb2018]

HCN (exomol, harris larner)

[Harris2008] [Barber2014]

NH3 (exomol, coyute)

[Coles2019] [Yurchenko2024b]

SiO (exomol, siouvenir)

[Yurchenko2022]

Note that these are mainly infrared abosrbers. Optical absorbers (e.g., Na, K, Rayleigh) will be defined later in the configuration file.

Stellar SED spectrum

The stellar spectral energy distribution (SED) sets the incident irradiation at the top of the atmosphere, as a function of pressure. Here we will make a stellar SED for WASP-69 from PHOENIX models [Husser2013] using the Gen TSO package. If you haven’t already, install this package with this shell command:

pip install gen_tso

Now, we can create the stellar SED spectrum with this Python script:

import gen_tso.pandeia_io as pandeia
import pyratbay.constants as pc
import pyratbay.spectrum as ps
import pyratbay.io as io
import matplotlib.pyplot as plt


# Use the Gen TSO package to get closest PHOENIX SED model for WASP-69
sed = pandeia.find_closest_sed(teff=4700, logg=4.5)
scene = pandeia.make_scene(
    sed_type='phoenix',
    sed_model=sed,
)
sed_wl, flux = pandeia.extract_sed(scene, wl_range=(0.15,35.0))

# Now convert flux from mJy to erg s-1 cm-2 cm
sed_flux = flux * pc.c / 1e26
# and lower the resolution
bin_wl = ps.constant_resolution_spectrum(0.15, 35.0, resolution=1500.0)
bin_sed_flux = ps.bin_spectrum(bin_wl, sed_wl, sed_flux, gaps='interpolate')

# Save to file
starspec_file = 'phoenix_WASP69b_4700K_logg4.5.dat'
io.write_spectrum(
    bin_wl,
    bin_sed_flux,
    starspec_file,
    type='emission',
)

# Lower the resolution to something more manageable
bin_wl = ps.constant_resolution_spectrum(0.15, 33.0, resolution=1500.0)
bin_sed_flux = ps.bin_spectrum(bin_wl, sed_wl, sed_flux, gaps='interpolate')

# Take a look
plt.figure(0, (7.5, 4.0))
plt.clf()
plt.subplots_adjust(0.09, 0.12, 0.98, 0.94)
ax = plt.subplot(111)
ax.plot(bin_wl, bin_sed_flux, color='royalblue', alpha=0.85)
ax.set_title('WASP-69 SED Spectrum')
ax.set_xscale('log')
ax.set_xlim(0.15, 30.5)
ax.set_xlabel(r'Wavelength ($\mathrm{\mu}$m)', fontsize=12)
ax.set_ylabel(r'$F_{\rm s}$ (erg s$^{-1}$ cm$^{-2}$ cm)', fontsize=12)
ax.tick_params(direction='in', which='both', labelsize=11)
../../_images/phoenix_sed_wasp69.png

Configuration file

Lastly, the configuration file will put together the inputs, define system and atmospheric properties, and set the radiative-equilibrium configuration. Here is the file we will use for the WASP-69b simulation.

Click here to show/hide: wasp69b_radeq_1x_solar_metallicity.cfg
File: wasp69b_radeq_1x_solar_metallicity.cfg
[pyrat]

# Pyrat Bay run mode [tli atmosphere opacity spectrum radeq retrieval]
runmode = radeq
# Number of radiative-equilibrium iterations
nsamples = 250

# Output file names
logfile = radeq_WASP69b_1x_solar_metallicity.log

# Verbosity level [1--3]
verb = 2

# Observing geometry, select between: [transit eclipse emission_two_stream]
rt_path = emission_two_stream

# Wavelength sampling
wl_low = 0.15 um
wl_high = 33.0 um
wl_thinning = 2

# System parameters (Bonomo et al. 2017)
rstar = 0.813 rsun
mstar = 0.826 msun
starspec = inputs/phoenix_WASP69b_4700K_logg4.5.dat

rplanet = 1.057 rjup
mplanet = 0.250 mjup
smaxis = 0.04527 au
refpressure = 1e-2 bar

# Atmospheric model
ptop = 1.0e-9 bar
pbottom = 100 bar
nlayers = 81

# Initial thermal profile for radiative-equilibrium runs
tmodel = isothermal
tpars = 900.0

# Chemistry and composition
chemistry = equilibrium
species =
    H  He  C  N  O  Na  K  Mg  Si  P  S  Cl Ti  V Fe
    H2  H2O  CH4  CO  CO2  HCN  NH3  N2  OH  C2H2  C2H4
    S2  SH  H2S  SO2  SO  TiO  VO  TiO2  VO2   SiO  SiH  SiS  SiH4
    OCS PH PH3 PN PO KOH CS CS2 NaH  NaCl  HCl  KCl NaOH
    e-  H-  H+  H2+  He+  Na-  Na+  Mg+ K-  K+  Fe+  Ti+  V+  SiH+ Si- Si+
# Elemental composition
vmr_vars =
    [M/H] 0.0
    C/O 0.59

# Hydrostatic equilibrium
radmodel = hydro_m

# Stellar irradiation scaling factor, beta = (1-A)*f
beta_irr = 0.66

# Internal heat (K)
tint = 100.0

# Line-by-line sampled cross sections
sampled_cross_sec =
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_H2O_exomol_pokazatel.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_CO_hitemp_2019.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_CO2_ames_ai3000k.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_CH4_exomol_mm.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_H2S_exomol_ayt2.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_HCN_exomol_harris_larner.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_NH3_exomol_coyute.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_SiO_exomol_siouvenir.npz

# Alkali models, select from: [sodium_vdw potassium_vdw]
alkali =
    sodium_vdw
    potassium_vdw

# Continuum cross sections
continuum_cross_sec =
    {ROOT}/pyratbay/data/CIA/CIA_Borysow_H2H2_0060-7000K_0.6-500um.dat
    {ROOT}/pyratbay/data/CIA/CIA_Borysow_H2He_0050-7000K_0.5-031um.dat

# Rayleigh models, select from: [rayleigh_H rayleigh_He rayleigh_H2]
rayleigh =
    rayleigh_H2
    rayleigh_He
    rayleigh_H

# H- continous absorption
h_ion = h_ion_john1988

Lets break this down:

# Pyrat Bay run mode [tli atmosphere opacity spectrum radeq retrieval]
runmode = radeq
# Number of radiative-equilibrium iterations
nsamples = 250

# Output file names
logfile = radeq_WASP69b_1x_solar_metallicity.log

# Verbosity level [1--3]
verb = 2

This first section defines what we want to run. runmode = radeq indicates that we want a radiative-equilibrium calculation. nsamples set the number of radiative-equilibrium iterations to run. logfile sets the path to the output files (logs, temperature profile, and emission spectrum). Finally, verb sets the screen-output verbosity.

# Observing geometry, select between: [transit eclipse emission_two_stream]
rt_path = emission_two_stream

# Wavelength sampling
wl_low = 0.15 um
wl_high = 33.0 um
wl_thinning = 2

Here we define the observing path and radiative-transfer scheme. For radiative-equilibrium runs set rt_path = emission_two_stream to compute the upward/downward emission through the atmosphere.

The wavelength sampling rate and coverage is set by the sampled_cross_section files. The wl_low and wl_high variables help to trim down the wavelength range (though in this case we keep the wavelength range as wide as possible). Setting a wl_thinning = n parameter lowers the resolution by taking every n-th spectral sample (with n an integer). Here we reduce the sampling resolution by a factor of two to speed up the runs.

# System parameters (Bonomo et al. 2017)
rstar = 0.813 rsun
mstar = 0.826 msun
starspec = inputs/phoenix_WASP69b_4700K_logg4.5.dat

rplanet = 1.057 rjup
mplanet = 0.250 mjup
smaxis = 0.04527 au
refpressure = 1e-2 bar

And this next section defines the system parameters. For a radiative-equilibrium run, the most relevant properties will be the stellar radius, the orbital semi-major axis, and the stellar SED. These parameters set the incident irradiation at the top of the atmosphere.

The other relevant planetary parameters set the planet mass, the planet radius, and the pressure at the given planet radius (refpressure).

# Atmospheric model
ptop = 1.0e-9 bar
pbottom = 100 bar
nlayers = 81

# Initial thermal profile for radiative-equilibrium runs
tmodel = isothermal
tpars = 900.0

# Chemistry and composition
chemistry = equilibrium
species =
    H  He  C  N  O  Na  K  Mg  Si  P  S  Cl Ti  V Fe
    H2  H2O  CH4  CO  CO2  HCN  NH3  N2  OH  C2H2  C2H4
    S2  SH  H2S  SO2  SO  TiO  VO  TiO2  VO2   SiO  SiH  SiS  SiH4
    OCS PH PH3 PN PO KOH CS CS2 NaH  NaCl  HCl  KCl NaOH
    e-  H-  H+  H2+  He+  Na-  Na+  Mg+ K-  K+  Fe+  Ti+  V+  SiH+ Si- Si+
# Elemental composition
vmr_vars =
    [M/H] 0.0
    C/O 0.59

# Hydrostatic equilibrium
radmodel = hydro_m

These parameters define the atmospheric profile models. For the pressure range, the only constraint is that the bottom boundary (i.e., max pressure) must be covered by the sampled_cross_section files.

Note that the temperature profile parameters define the starting point for the radiative-equilibrium calculation. The recommendation is to start with an isothermal profile at the equilibrium temperature.

For the composition, thermochemical equilibrium is the recommendation. Here it’s important to include all species that can be chemically relevant across the range of temperatures and pressures. vmr_vars allows one to customize elemental composition and ratios that differ from the solar values if needed.

radmodel tells the code to compute the atmospheric layers’ altitude under hydrostatic equilibrium assuming \(g(r) = GM/r^2\) and boundary condition from rplanet and refpressure (\(r(p_{\rm ref}) = r_{\rm p}\)).

# Stellar irradiation scaling factor, beta = (1-A)*f
beta_irr = 0.66

# Internal heat (K)
tint = 100.0

Finally, these pair of variables define the radiative boundary conditions at the top of the atmosphere. tint is the internal heat from the planet at the bottom of the atmosphere. beta_irr sets how much stellar irradiation is absorbed at the top of the atmosphere:

\[F_{\rm inc}(\lambda) = \beta_{\rm irr} \left(\frac{R_*}{a}\right)^2F_{*}(\lambda)\]

where \(a\) is the semi-major axis, \(R_*\) the stellar radius, and \(F_*\) the stellar SED.

# Line-by-line sampled cross sections
sampled_cross_sec =
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_H2O_exomol_pokazatel.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_CO_hitemp_2019.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_CO2_ames_ai3000k.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_CH4_exomol_mm.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_H2S_exomol_ayt2.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_HCN_exomol_harris_larner.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_NH3_exomol_coyute.npz
    inputs/cross_section_0.15-33.0um_0200-5000K_R025K_SiO_exomol_siouvenir.npz

# Alkali models, select from: [sodium_vdw potassium_vdw]
alkali =
    sodium_vdw
    potassium_vdw

# Continuum cross sections
continuum_cross_sec =
    {ROOT}/pyratbay/data/CIA/CIA_Borysow_H2H2_0060-7000K_0.6-500um.dat
    {ROOT}/pyratbay/data/CIA/CIA_Borysow_H2He_0050-7000K_0.5-031um.dat

# Rayleigh models, select from: [rayleigh_H rayleigh_He rayleigh_H2]
rayleigh =
    rayleigh_H2
    rayleigh_He
    rayleigh_H

# H- continous absorption
h_ion = h_ion_john1988

This section defines the atmospheric absorbers. Make sure that all absorber species are defined in the atmospheric composition. Note that some of these set constraints to the domain to be explored. The sampled_cross_sec files set the maximum resolution, spectral range, temperature range, and pressure. The continuum_cross_sec files set temperature range constraints, but one can span beyond their wavelength ranges.

On top of the line-sampled opacities, we add Na and K alkali models from [Burrows2000]; H₂-H₂ and H₂-He CIA; and H, H₂, and He Rayleigh opacities.

Radiative-equilibrium run

To launch the radiative-equilibrium run, use the following prompt command:

# Run radiative-equilibrium calculation
pbay -c wasp69b_radeq_1x_solar_metallicity.cfg

That’s it. This should take a couple of minutes to complete the iterations. The progress will be displayed on screen. Once finished, this run will produce four files:

  • radeq_WASP69b_1x_solar_metallicity.atm: Atmospheric profile at last iteration (p, T, VMRs)

  • radeq_WASP69b_1x_solar_metallicity.dat: Planetary emission spectrum at final iteration

  • radeq_WASP69b_1x_solar_metallicity.npz: Temperature profile at each iteration

  • radeq_WASP69b_1x_solar_metallicity.log: Screen-output log

Radiative equilibrium profiles will typically settle after a couple hundred iterations. These Python scripts show how the temperature converges toward radiative equilibrium (purple to yellow to green profiles), and the resulting abundances corresponding to the final iteration:

import pyratbay.io as io
import pyratbay.plots as pp
import numpy as np
import matplotlib.pyplot as plt
plt.ion()

# Temperature profiles
file = 'radeq_WASP69b_1x_solar_metallicity.npz'
data = np.load(file)
press = data['pressure']
temps = data['temps']
niter, nlay = np.shape(temps)

# Abundances
a_file = 'radeq_WASP69b_1x_solar_metallicity.atm'
units, species, press, temp, vmr, rad = io.read_atm(a_file)

fig = plt.figure(1)
plt.clf()
fig.set_size_inches(5.25, 4)
ax = plt.subplots_adjust(0.13, 0.12, 0.98, 0.97)
ax = plt.subplot(111)
for i in range(0, niter, 3):
    c = i / niter
    ax.plot(temps[i], press, c=plt.cm.plasma(c), alpha=0.75)
ax.plot(temps[niter-1], press, label='WASP-69b', c='xkcd:green', lw=2)
ax.set_xlabel('Temperature (K)', fontsize=12)
ax.set_ylabel('Pressure (bar)', fontsize=12)
ax.legend(loc='upper right')
ax.tick_params(direction='in', which='both', labelsize=11)
ax.set_yscale('log')
ax.set_ylim(100, 1e-9)
ax.set_xlim(600, 1750)

# Only show molecules of interest
mol_show = ['H2', 'He', 'H', 'H2O', 'CO', 'CO2', 'CH4', 'H2S', 'HCN', 'NH3', 'SiO']
imol = np.isin(species, mol_show)
fig = plt.figure(2)
plt.clf()
fig.set_size_inches(6, 4)
ax = plt.subplot(111)
plt.subplots_adjust(0.13, 0.12, 0.98, 0.97)
ax = pp.abundance(
    vmr[:,imol], press, species[imol],
    colors='default', xlim=[1e-14, 2.0], ax=ax,
)

And we also have the planetary emission spectrum from the top of the atmosphere:

import numpy as np
import pyratbay.spectrum as ps
import matplotlib.pyplot as plt
plt.ion()

s_file = 'radeq_WASP69b_1x_solar_metallicity.dat'
wl, p_flux = np.loadtxt(s_file, unpack=True)
bin_wl = ps.constant_resolution_spectrum(0.5, 30.0, resolution=500.0)
bin_pflux = ps.bin_spectrum(bin_wl, wl, p_flux)

fig = plt.figure(3)
plt.clf()
fig.set_size_inches(6.5, 3.25)
ax = plt.subplots_adjust(0.14, 0.15, 0.98, 0.98)
ax = plt.subplot(111)
ax.plot(wl, p_flux, label='WASP-69b', c='royalblue', lw=1.0)
ax.plot(bin_wl, bin_pflux, c='salmon', lw=1.25)
ax.legend(loc='upper right')
ax.set_xscale('log')
ax.set_xlim(0.5, 30.0)
ax.set_xlabel(r'Wavelength ($\mathrm{\mu}$m)', fontsize=12)
ax.set_ylabel(r'$F_{\rm p}$ (erg s$^{-1}$ cm$^{-2}$ cm)', fontsize=12)
ax.tick_params(direction='in', which='both', labelsize=11)
../../_images/WASP69b_radiative_equilibrium_flux.png