===================
Interior retrievals
===================
.. note::
Download the full notebook : :download:`here `
In this page we show a tutorial on how to obtain a grid of GASTLI models to run a retrieval on mass, radius, age, and atmospheric metallicity.
We first need to generate a grid of forward models. The next example is going to consist of more than 4000 models. Each model takes between 2-10 second, so this may take up to 12 hours, depending on the computer architecture and number of threads/cores available for parallelization. Hence, we recommend setting up this step of the calculation in a cluster. The following code saves each thermal evolution curve in a file:
.. code-block:: python
# Import coupling module
import gastli.Thermal_evolution as therm
# Other Python modules
import numpy as np
import os
Mjup = 318.
Rjup = 11.2
## Equilibrium temperature
Teqpl = 842.
# Input for interior
## mass in Mearth units
mass = np.arange(0.4*Mjup,1.05*Mjup,0.05*Mjup)
## CMF
CMFs = np.array([0.,0.1,0.2,0.25,0.3,0.35,0.4,0.45,0.5,0.6,0.8,0.99])
## log(Fe/H)
log_FeHs = np.array([-2.,0.,1.,1.99])
## Tint
Tint_array = np.array([50.,100.,200.,300.,400.,449.])
# Loop for forward models
n_mrel = len(CMFs)*len(log_FeHs)*len(mass)
counter = 0
for i_cmf, CMF in enumerate(CMFs):
for i_FeH, logFeH in enumerate(log_FeHs):
for i_mass, M_P in enumerate(mass):
counter = counter+1
# File with thermal evolution
file_name = "Output/thermal_sequence_CMF_" + "{:4.2f}".format(CMF) + "_logFeH_" + "{:4.2f}".format(logFeH) +\
"_mass_" + "{:4.2f}".format(M_P) + ".dat"
# Check that file does not exist to avoid overwrite
answer = os.path.isfile(file_name)
if answer == True:
continue
# Print message to monitor progress
print('---------------')
print('CMF = ', CMF)
print('log(Fe/H) = ', logFeH)
print('Mass [Mearth] = ', M_P)
print('Sequence = ', counter, ' out of ', n_mrel)
print('---------------')
# Thermal evolution class object
my_therm_obj = therm.thermal_evolution(pow_law_formass=0.315)
my_therm_obj.main(M_P, CMF, Teqpl, Tint_array, CO=0.55, log_FeH=logFeH)
my_therm_obj.solve_thermal_evol_eq()
# Save sequence of interior models
data = np.zeros((len(my_therm_obj.f_S), 11))
data[:, 0] = my_therm_obj.f_S
data[:, 1] = my_therm_obj.s_mean_TE
data[:, 2] = my_therm_obj.s_top_TE
data[:, 3] = my_therm_obj.Tint_array
data[:, 4] = my_therm_obj.Rtot_TE*Rjup
data[:, 5] = my_therm_obj.Rbulk_TE*Rjup
data[:, 6] = my_therm_obj.Tsurf_TE
data[:, 7] = my_therm_obj.age_points
data[:, 8] = my_therm_obj.Zenv_TE
data[:, 9] = my_therm_obj.Mtot_TE
tmm = CMF + (1-CMF)*my_therm_obj.Zenv_TE
data[:, 10] = tmm
header0 = 'f_S s_mean_TE s_top_TE Tint_K Rtot_earth Rbulk_earth Tsurf_K Age_Gyrs Zenv Mtot_earth Zplanet'
np.savetxt(file_name, data, header=header0, comments='',fmt = '%1.4e')
Then, we can read all these files in a loop to generate a single hdf5 grid with all the forward models:
.. code-block:: python
# Python modules
import numpy as np
import pandas as pd
import h5py
# Constants
Mjup = 318.
Rjup = 11.2
# Arrays of grid
## mass in Mearth units
masses = np.arange(0.4*Mjup,1.05*Mjup,0.05*Mjup)
## CMF
CMFs = np.array([0.,0.1,0.2,0.25,0.3,0.35,0.4,0.45,0.5,0.6,0.8,0.99])
## log(Fe/H)
log_FeHs = np.array([-2.,0.,1.,1.99])
## Tint
Tint_array = np.array([50.,100.,200.,300.,349.])
n_masses = len(masses)
n_logFeH = len(log_FeHs)
n_CMF= len(CMFs)
n_Tint = len(Tint_array)
# Create file
f = h5py.File("my_forward_model_grid.hdf5", "w")
# Data sets
data_set_Rtot = f.create_dataset("Rtot", (n_CMF, n_logFeH, n_masses, n_Tint), dtype='f')
data_set_Rbulk = f.create_dataset("Rbulk", (n_CMF, n_logFeH, n_masses, n_Tint), dtype='f')
data_set_age = f.create_dataset("age", (n_CMF, n_logFeH, n_masses, n_Tint), dtype='f')
data_set_Tsurf = f.create_dataset("Tsurf", (n_CMF, n_logFeH, n_masses, n_Tint), dtype='f')
data_set_Mtot = f.create_dataset("Mtot", (n_CMF, n_logFeH, n_masses, n_Tint), dtype='f')
data_set_Zplanet = f.create_dataset("Zplanet", (n_CMF, n_logFeH, n_masses, n_Tint), dtype='f')
data_set_Zenv = f.create_dataset("Zenv", (n_CMF, n_logFeH, n_masses, n_Tint), dtype='f')
# Assign arrays for grid
f['CMF'] = CMFs
f['log_FeH'] = log_FeHs
f['mass'] = masses/Mjup
f['Tint'] = Tint_array
# Prepare loop to read output files and fill in data sets
n_mrel = n_CMF*n_logFeH*n_masses
for i_cmf, CMF in enumerate(CMFs):
for i_FeH, logFeH in enumerate(log_FeHs):
for i_mass, M_P in enumerate(masses):
file_name = "Output/thermal_sequence_CMF_" + "{:4.2f}".format(CMF) +\
"_logFeH_" + "{:4.2f}".format(logFeH) + "_mass_" + "{:4.2f}".format(M_P) + ".dat"
# Read file
data = pd.read_csv(file_name, sep='\s+', header=0)
rtot = data['Rtot_earth']
rbulk = data['Rbulk_earth']
age = data['Age_Gyrs']
tsurf = data['Tsurf_K']
mtot = data['Mtot_earth']
zplanet = data['Zplanet']
zenv = data['Zenv']
# Fill data set
data_set_Rtot[i_cmf, i_FeH, i_mass, :] = rtot/Rjup
data_set_Rbulk[i_cmf, i_FeH, i_mass, :] = rbulk/Rjup
data_set_age[i_cmf, i_FeH, i_mass, :] = age
data_set_Tsurf[i_cmf, i_FeH, i_mass, :] = tsurf
data_set_Mtot[i_cmf, i_FeH, i_mass, :] = mtot/Mjup
data_set_Zplanet[i_cmf, i_FeH, i_mass, :] = zplanet
data_set_Zenv[i_cmf, i_FeH, i_mass, :] = zenv
# End of loop, now attach dimensions to grid data sets
## Total radius (Jupiter units)
f['Rtot'].dims[0].attach_scale(f['CMF'])
f['Rtot'].dims[1].attach_scale(f['log_FeH'])
f['Rtot'].dims[2].attach_scale(f['mass'])
f['Rtot'].dims[3].attach_scale(f['Tint'])
## Interior radius
f['Rbulk'].dims[0].attach_scale(f['CMF'])
f['Rbulk'].dims[1].attach_scale(f['log_FeH'])
f['Rbulk'].dims[2].attach_scale(f['mass'])
f['Rbulk'].dims[3].attach_scale(f['Tint'])
## Age (Gyrs)
f['age'].dims[0].attach_scale(f['CMF'])
f['age'].dims[1].attach_scale(f['log_FeH'])
f['age'].dims[2].attach_scale(f['mass'])
f['age'].dims[3].attach_scale(f['Tint'])
## Surface temperature (K)
f['Tsurf'].dims[0].attach_scale(f['CMF'])
f['Tsurf'].dims[1].attach_scale(f['log_FeH'])
f['Tsurf'].dims[2].attach_scale(f['mass'])
f['Tsurf'].dims[3].attach_scale(f['Tint'])
## Total mass (Jupiter units)
f['Mtot'].dims[0].attach_scale(f['CMF'])
f['Mtot'].dims[1].attach_scale(f['log_FeH'])
f['Mtot'].dims[2].attach_scale(f['mass'])
f['Mtot'].dims[3].attach_scale(f['Tint'])
## Total metal mass fraction
f['Zplanet'].dims[0].attach_scale(f['CMF'])
f['Zplanet'].dims[1].attach_scale(f['log_FeH'])
f['Zplanet'].dims[2].attach_scale(f['mass'])
f['Zplanet'].dims[3].attach_scale(f['Tint'])
## Envelope metal mass fraction
f['Zenv'].dims[0].attach_scale(f['CMF'])
f['Zenv'].dims[1].attach_scale(f['log_FeH'])
f['Zenv'].dims[2].attach_scale(f['mass'])
f['Zenv'].dims[3].attach_scale(f['Tint'])
# Close file
f.close()
We finally have our grid of forward models that we can interpolate. For the retrieval, you need to install the Markov chain Monte Carlo (MCMC) sampler package `emcee `_. The following snippet uses ``emcee`` and interpolates our grid to perform the retrieval.
.. code-block:: python
# import modules
import numpy as np
import h5py
from scipy.interpolate import RegularGridInterpolator
import matplotlib.pyplot as plt
import emcee
# Load data
file_name = "my_forward_model_grid.hdf5"
file = h5py.File(file_name, 'r')
## datasets
data_set_Rtot = file['Rtot'][()]
data_set_Rbulk = file['Rbulk'][()]
data_set_age = file['age'][()]
data_set_Tsurf = file['Tsurf'][()]
data_set_Mtot = file['Mtot'][()]
data_set_Zplanet = file['Zplanet'][()]
data_set_Zenv = file['Zenv'][()]
## arrays
CMFs = file['CMF'][()]
logFeHs = file['log_FeH'][()]
masses = file['mass'][()]
Tints = file['Tint'][()]
# Create functions for interpolation
rtot = RegularGridInterpolator((CMFs,logFeHs,masses,Tints), data_set_Rtot, bounds_error=False, fill_value=None)
rbulk = RegularGridInterpolator((CMFs,logFeHs,masses,Tints), data_set_Rbulk, bounds_error=False, fill_value=None)
age = RegularGridInterpolator((CMFs,logFeHs,masses,Tints), data_set_age, bounds_error=False, fill_value=None)
tsurf = RegularGridInterpolator((CMFs,logFeHs,masses,Tints), data_set_Tsurf, bounds_error=False, fill_value=None)
mtot = RegularGridInterpolator((CMFs,logFeHs,masses,Tints), data_set_Mtot, bounds_error=False, fill_value=None)
zplanet = RegularGridInterpolator((CMFs,logFeHs,masses,Tints), data_set_Zplanet, bounds_error=False, fill_value=None)
zenv = RegularGridInterpolator((CMFs,logFeHs,masses,Tints), data_set_Zenv, bounds_error=False, fill_value=None)
# Forward model function
def forward_model(CMF, logFeH, bulk_mass, Tint_mod):
'''
Forward model
'''
pts = np.zeros((1, 4))
pts[:, 0] = CMF
pts[:, 1] = logFeH
pts[:, 2] = bulk_mass
pts[:, 3] = Tint_mod
model_R = rtot(pts)
R_mod = model_R[0]
model_Mtot = mtot(pts)
Mtot_mod = model_Mtot[0]
model_age = age(pts)
age_mod = model_age[0]
return R_mod, Mtot_mod, age_mod
# Example of use
output_forward = forward_model(0.1, 0., 0.74, 70.)
print(output_forward)
# Log-likelihood
## Mass, radius and age: mean and uncertainties
mean_mass = 0.74
e_M_minus = 0.07
e_M_plus = 0.06
age_planet = 7.3
e_age_minus = 2.5
e_age_plus = 2.4
mean_rad = 0.98
e_rad_minus = 0.05
e_rad_plus = 0.05
x = np.asarray([])
y = np.asarray([mean_mass,mean_rad,age_planet])
yerr = np.asarray([e_M_plus,e_M_minus,e_rad_plus,e_rad_minus,e_age_plus,e_age_minus])
'''
Format:
y = (Mp,Rp,age)
yerr = (Mp_e+,Mp_e-,Rp_e+,Rp_e-,age_e+,age_e-)
'''
## Function
def log_likelihood(theta, x, y, yerr):
CMF_mod, log_FeH_mod, mass_mod, Tint_mod = theta
R_mod, Mtot_mod, age_mod = forward_model(CMF_mod, log_FeH_mod, mass_mod, Tint_mod)
Mdata = y[0]
Rdata = y[1]
age_data = y[2]
sigma_Mplus = yerr[0]
sigma_Mminus = yerr[1]
sigma_Rplus = yerr[2]
sigma_Rminus = yerr[3]
sigma_age_plus = yerr[4]
sigma_age_minus = yerr[5]
if Mtot_mod > Mdata:
sigma_M = sigma_Mplus
else:
sigma_M = sigma_Mminus
if R_mod > Rdata:
sigma_R = sigma_Rplus
else:
sigma_R = sigma_Rminus
if age_mod > age_data:
sigma_age = sigma_age_plus
else:
sigma_age = sigma_age_minus
# Likelihood
L = -0.5 * ( ((Mtot_mod - Mdata) / sigma_M) ** 2 + \
((R_mod - Rdata) / sigma_R) ** 2+\
((age_mod - age_planet) / sigma_age)**2 )
return L
# Example of use
theta_test = np.array([0.1, 0., 0.74, 70.])
a = log_likelihood(theta_test, x, y, yerr)
print(a)
# Priors
## Max and min limits
CMF_min = 0.01
CMF_max = 0.99
logFeH_min = -2.
logFeH_max = 1.99
mass_min = 0.4
mass_max = 1.05
Tint_min = 50.
Tint_max = 349.
# Function
def log_prior(theta):
CMF_mod, log_FeH_mod, mass_mod, Tint_mod = theta
if min(CMFs) < CMF_mod < max(CMFs) and \
min(logFeHs) < log_FeH_mod < max(logFeHs) and \
min(masses) < mass_mod < max(masses) and \
Tint_min < Tint_mod < Tint_max:
return 0.0
return -np.inf
# Probability function
def log_probability(theta, x, y, yerr):
lp = log_prior(theta)
if not np.isfinite(lp):
return -np.inf
return lp + log_likelihood(theta, x, y, yerr)
# Define walkers
nwlk = 32
pos = np.zeros((nwlk, 4))
pos[:,0] = np.random.uniform(CMF_min, CMF_max, nwlk) # CMF: uniform
pos[:,1] = np.random.uniform(logFeH_min, logFeH_max, nwlk) # log(Fe/H): uniform
pos[:,2] = np.random.normal(mean_mass, max(e_M_plus,e_M_minus), nwlk) # mass: uniform
pos[:,3] = np.random.uniform(Tint_min, Tint_max, nwlk) # Tint: log-uniform
nwalkers, ndim = pos.shape
# Define steps
nsteps = int(100000)
# emcee main functions
sampler = emcee.EnsembleSampler(
nwalkers, ndim, log_probability, args=(x, y, yerr), backend=backend
)
sampler.run_mcmc(pos, nsteps, progress=True)
A retrieval with this number of steps takes around 30 min. To check convergence, we can plot the evolution of the chains:
.. code-block:: python
fig, axes = plt.subplots(ndim, figsize=(10, 11), sharex=True)
samples = sampler.get_chain()
labels = ["CMF", "log(FeH) [x solar]", "Mass [MJup]", "Tint [K]"]
for i in range(ndim):
ax = axes[i]
ax.plot(samples[:, :, i], "k", alpha=0.3)
ax.set_xlim(0, len(samples))
ax.set_ylabel(labels[i])
ax.yaxis.set_label_coords(-0.1, 0.5)
axes[-1].set_xlabel("step number")
fig.savefig('Output/emcee_convergence.pdf',bbox_inches='tight',format='pdf', dpi=1000)
plt.close(fig)
.. figure:: emcee_convergence.png
:align: center
Positions of each walker as a function of the number of steps in the chain.
You can also check that the MCMC chains converged by looking at the autocorrelation time
.. code-block:: python
tau = sampler.get_autocorr_time()
print('tau = ', tau)
The autocorrelation time should not be larger than the number of steps divided by 50.
We can obtain the samples and save them with:
.. code-block:: python
# Obtain input parameter samples
ndiscard = int(2 * max(tau))
nthin = int(max(tau)/2)
flat_samples = sampler.get_chain(discard=ndiscard, thin=nthin, flat=True)
n = flat_samples.shape[0]
CMF_sample = flat_samples[:, 0]
logFeH_sample = flat_samples[:, 1]
mass_sample = flat_samples[:, 2]
tint_sample = flat_samples[:, 3]
# Output parameter
## Initialise arrays
rtot_sample = np.zeros_like(CMF_sample)
mtot_sample = np.zeros_like(CMF_sample)
rbulk_sample = np.zeros_like(CMF_sample)
age_sample = np.zeros_like(CMF_sample)
tsurf_sample = np.zeros_like(CMF_sample)
zplanet_sample = np.zeros_like(CMF_sample)
zenv_sample = np.zeros_like(CMF_sample)
## Interpolate
pts = np.zeros((n, 4))
pts[:, 0] = CMF_sample
pts[:, 1] = logFeH_sample
pts[:, 2] = mass_sample
pts[:, 3] = tint_sample
rtot_sample = rtot(pts)
mtot_sample = mtot(pts)
rbulk_sample = rbulk(pts)
age_sample = age(pts)
tsurf_sample = tsurf(pts)
zplanet_sample = zplanet(pts)
zenv_sample = zenv(pts)
# Save all samples in output file
data = np.zeros((n,11))
data[:,0] = CMF_sample
data[:,1] = logFeH_sample
data[:,2] = mass_sample
data[:,3] = age_sample
data[:,4] = rtot_sample
data[:,5] = mtot_sample
data[:,6] = rbulk_sample
data[:,7] = tint_sample
data[:,8] = tsurf_sample
data[:,9] = zplanet_sample
data[:,10] = zenv_sample
np.savetxt('samples.dat', data,\
header='CMF logFeH Mbulk[M_J] Age[Gyr] Rtot[R_J] Mtot[M_J] Rbulk[R_J] Tint[K] Tsurf[K] Zpl Zenv',\
comments='',fmt='%1.4e')
Finally, we can generate a corner plot with the samples with the module ``corner`` (see how to install it `here `_). Here is a snippet to plot the samples obtained above:
.. code-block:: python
# python modules
import pandas as pd
import numpy as np
import corner
# Read samples file
data = pd.read_csv('samples.dat', sep='\s+')
CMF = data['CMF']
log_FeH = data['logFeH']
M = data['Mtot[M_J]']
R = data['Rtot[R_J]']
Mbulk = data['Mbulk[M_J]']
Zenv = data['Zenv']
age = data['Age[Gyr]']
Tint = data['Tint[K]']
# Account for atmospheric mass (this is usually negligible)
Matm = M - Mbulk
Menv_int = Mbulk * (1-CMF)
EMF_recalc = (Menv_int + Matm)/M
x_core = 1. - EMF_recalc
Zplanet = x_core + EMF_recalc*Zenv
# Corner input
flat_samples = np.zeros((len(CMF),8))
flat_samples[:,0] = CMF
flat_samples[:,1] = log_FeH
flat_samples[:,2] = M
flat_samples[:,3] = R
flat_samples[:,4] = Zenv
flat_samples[:,5] = Zplanet
flat_samples[:,6] = age
flat_samples[:,7] = Tint
# Plot it.
figure = corner.corner(flat_samples, labels=[r"CMF", r"log(Fe/H)", r"M [$M_{Jup}$]",\
r"R [$R_{Jup}$]", "$Z_{env}$", r"$Z_{planet}$", r"Age [Gyr]",\
r"$T_{int}$ [K] "],
quantiles=[0.16, 0.5, 0.84],\
truths=[np.nan,np.nan,0.74,0.98,np.nan,np.nan,7.5,np.nan],\
show_titles=True, title_kwargs={"fontsize": 14},label_kwargs={"fontsize": 16})
figure.savefig('corner_plot.pdf',bbox_inches='tight',format='pdf', dpi=1000)
.. figure:: corner_plot.png
:align: center
Corner plot of our retrieval with GASTLI