Tutorial 5: Using the LDTk limb darkening model#

The TSModel transmission spectroscopy transit model in ExoIris is based on PyTransit’s RRModel and includes its ability to model stellar limb darkening using an arbitrary rotationally symmetric function. The LDTk limb darkening model (exoiris.LDTkLD) takes advantage of this and models stellar limb darkening using theoretical PHOENIX limb darkening profiles directly instead of representing it as simple analytical functions.

for all the passbands parameterised by three stellar parameters: the effective temperature, log g, and metallicity.

The LDTk limb darkening is parameterised by (\(T_\mathrm{Eff}\), \(\log g\), \(z\)) model calculates the limb darkening profiles automatically for all passbands defined by a set of filters for a set of stellar parameters. This is very useful for transmission spectroscopy since the number of limb darkening parameters stays constant (three) no matter how many passbands are modelled simultaneously.

Author: Hannu Parviainen Edited: 7 November 2024

[1]:
%run ../setup_multiprocessing.py
[2]:
%matplotlib inline
[3]:
from multiprocessing import Pool
from xarray import load_dataset
from scipy.interpolate import splev, splrep
from numpy import array, geomspace, linspace
from matplotlib.pyplot import subplots, setp

from exoiris import ExoIris, TSData, LDTkLD
from ldtk import BoxcarFilter

Data preparation#

[4]:
def read_data(fname, name=""):
    with load_dataset(fname) as ds:
        return TSData(time=ds.time.values, wavelength=ds.wavelength.values, fluxes=ds.flux.values, errors=ds.error.values, name=name)

d1 = read_data('data/nirHiss_order_1.h5', "WASP-39b JWST NIRISS Order 1")
d1.remove_outliers()
d1.mask_transit(t0=2459783.5015, p=4.0552842, t14=0.13)
d1.normalize_to_poly()

d2 = read_data('data/nirHiss_order_2.h5', "WASP-39b JWST NIRISS Order 2")
d2.remove_outliers()
d2.crop_wavelength(d2.bbox_wl[0], 1.0)
d2.mask_transit(ephemeris=d1.ephemeris)
d2.normalize_to_poly()
[4]:
TSData Name:'WASP-39b JWST NIRISS Order 2' [0.57 - 1.00] nwl=957 npt=518
[5]:
db = d2.bin_wavelength(r = 100) + d1.bin_wavelength(r = 100)
fig = db.plot()
fig.tight_layout()
../../_images/examples_e01_05a_ldtkldm_6_0.png

Setup the LDTk limb darkening model#

The way LDTkLD works is familiar to anyone who has used LDTk before. You initialise the model by giving it the stellar effective temperature, surface gravity, and metallicity estimates as (mean, \(1\sigma\) uncertainty) tuples and a set of filters. You can also optionally tell the model which theoretical stellar spectrum dataset it should use (vis, vis-lowres, visir, or visir-lowres). For JWST transmission spectroscopy, one should choose either visir, or visir-lowres.

[49]:
ldm = LDTkLD(db, teff=(5327, 139), logg=(4.38, 0.09), metal=(-0.01, 0.1), dataset='visir-lowres')
[50]:
ts = ExoIris('05a_ldtk_limb_darkening', ldmodel=ldm, data=db, nk=50, nldc=10, nthreads=1)

Now, when we look at the model parameterisation, we notice the limb darkening is parameterised only by the three stellar parameters teff, logg, and metal. The priors are taken automatically from LDTkLD.

[52]:
ts.ps[:10]
[52]:
[  0 |G| rho            U(a = 0.1, b = 25.0)                     [    0.00 ..      inf],
   1 |G| tc             N(μ = 0.0, σ = 0.1)                      [    -inf ..      inf],
   2 |G| p              N(μ = 1.0, σ = 1e-05)                    [    0.00 ..      inf],
   3 |G| b              U(a = 0.0, b = 1.0)                      [    0.00 ..      inf],
   4 |G| secw           N(μ = 0.0, σ = 1e-05)                    [   -1.00 ..     1.00],
   5 |G| sesw           N(μ = 0.0, σ = 1e-05)                    [   -1.00 ..     1.00],
   6 |G| teff           N(μ = 5327.0, σ = 139.0)                 [    0.00 ..      inf],
   7 |G| logg           N(μ = 4.38, σ = 0.09)                    [    0.00 ..      inf],
   8 |G| metal          N(μ = -0.01, σ = 0.1)                    [    -inf ..      inf],
   9 |G| k_00.57318     U(a = 0.02, b = 0.2)                     [    0.00 ..      inf]]
[22]:
ts.set_prior('tc', 'NP', 2459694.286, 0.003)
ts.set_prior('p', 'NP', 4.05487, 1e-5)
[23]:
ts.set_radius_ratio_prior('UP', 0.14, 0.15)

Customise the radius ratio knot locations#

[24]:
ts.set_radius_ratio_knots(geomspace(ts.data[0].wavelength[5], ts.data[1].wavelength[-5], 50))
ts.add_radius_ratio_knots(linspace(0.768-0.015, 0.768+0.015, 5))
ts.set_radius_ratio_prior('UP', 0.14, 0.15)
ts.plot_setup();
../../_images/examples_e01_05a_ldtkldm_15_0.png
[25]:
ts.fit_white()
fig = ts.plot_white(figsize=(13,4))
fig.tight_layout()
../../_images/examples_e01_05a_ldtkldm_16_0.png
[26]:
ts.save(overwrite=True)

Set up multiprocessing#

Now we’re almost ready for fitting and MCMC sampling, but since we’re using multiprocessing to parallelise the process, we first need to take some extra steps to make sure everything works the way it’s supposed to.

First, we need to define a log-posterior function that calls the ExoIris log-posterior method. This must be done so that Python can pickle the method for parallelisation (if you know a better way, please let me know). Next, we also create a multiprocessing pool that will be used by the global optimiser and the MCMC sampler.

[29]:
def lnpostf(pv):
    return ts.lnposterior(pv)

pool = Pool(8)

Fit the transmission spectrum#

[37]:
ts.fit(niter=2500, npop=250, pool=pool, lnpost=lnpostf)
../../_images/examples_e01_05a_ldtkldm_21_1.png
[38]:
ts.plot_fit(result='fit', figsize=(13,10), height_ratios=(0.3, 0.7),
            res_args=dict(pmin=5, pmax=95),
            trs_args=dict(xscale='log', ylim=(2.0,2.25), xticks=[0.6, 0.88, 1.16, 1.44, 1.72, 2.0, 2.30, 2.8]));
../../_images/examples_e01_05a_ldtkldm_22_0.png

We can immediately see from the residual plot that the current best-fit solution is bad. This is something we could have guessed already by looking at the log posterior distribution in the optimiser plot that shows a spread of tens of thousands. Let’s continue the fitting for another 25 iterations. Each successive ExoIris.fit call continues optimisation from the solution of the previous call, and it also plots the old and new posterior and parameter populations at the end of the fit to visualise how the population is changing.

So, it is clear at this stage that running the optimiser for 25 iterations is not doing much. Let’s continue the optimisation for 5000 iterations and see if this does the trick. This should take 2-3 min (or less if you initialise the pool with more processes) and the optimisation should finish before it reaches 5000 iterations (the progress bar turns red).

[30]:
ts.fit(niter=5000, npop=250, pool=pool, lnpost=lnpostf)
../../_images/examples_e01_05a_ldtkldm_25_1.png

Let’s plot everything once again. This time, the residuals look good (there’s no trace of a transit signal there), the transmission spectrum looks something else than just noise, and the limb darkening parameters also look good, so we’re ready to move to the final step: the MCMC sampling.

[32]:
ts.plot_fit(result='fit', figsize=(13,7),
            res_args=dict(pmin=5, pmax=95),
            trs_args=dict(xscale='log', ylim=(2.0,2.25), xticks=[0.6, 0.88, 1.16, 1.44, 1.72, 2.0, 2.30, 2.8]))
[32]:
../../_images/examples_e01_05a_ldtkldm_27_0.png

Save the optimisation results#

[39]:
ts.save(overwrite=True)

MCMC sampling#

Next comes the final part, obtaining a posterior sample using Markov Chain Monte Carlo (MCMC) sampling. This is done using the ExoIris.sample method, that starts with the parameter vector population from the global optimisation.

As a first step, let’s still inflate the limb darkening parameter prior widths by ten to make sure we’re not constraining them too much in the sampling phase. So, now we set uncertainty_multiplier=100 instead of uncertainty_multiplier=10 that we used in fitting.

[40]:
ts.sample(1000, thin=100, repeats=3, pool=pool, lnpost=lnpostf)
[41]:
df = ts._tsa.posterior_samples()
[47]:
import ldtk
[48]:
ldtk.__version__
[48]:
Version('1.8.4')
[43]:
ts.plot_fit(result='mcmc', figsize=(13,7),
            res_args=dict(pmin=5, pmax=95),
            trs_args=dict(xscale='log', ylim=(2.0,2.25), xticks=[0.6, 0.88, 1.16, 1.44, 1.72, 2.0, 2.30, 2.8]));
../../_images/examples_e01_05a_ldtkldm_35_0.png

Save the results and export the transmission spectrum#

Congratulations, you now have a low-resolution transmission spectrum for your observations! Let’s save the model one more time to also store the posterior samples inside the fits file.

[44]:
ts.save(overwrite=True)
[46]:
ts.transmission_spectrum
[46]:
Table length=175
wavelengthradius_ratioradius_ratio_earea_ratioarea_ratio_e
um
float64float64float64float64float64
0.57318060077403250.140159721985909150.00029462119529411020.0196447476672036168.260477409168684e-05
0.57894120982703790.142555000969537850.00019707789939126470.020321928301454465.6193077320264214e-05
0.58475971444841510.14427347051622640.000163488737004317240.020814834294802184.717799283285726e-05
0.59063669650317310.145406523327225780.000162013493871894870.0211430570261111344.712005749875868e-05
0.596572743704210.146040122295797050.00016217522536523720.0213277173202355944.737224591468161e-05
0.60256844967108650.146262378972385170.000155502705709652810.0213926835026657524.5490326640701425e-05
0.60862441398938880.146158184173169610.00014638222054480860.0213622148008036644.2790612092112884e-05
0.61474124227068930.145820204172744660.00014307737245586570.0212635319449918924.1729795817659045e-05
0.62091954621310820.14536463692393780.000149447887359516390.021130877668028864.345358064343948e-05
...............
2.6115514682536690.146350919393620850.00056561184632294920.021418591607377510.00016556809356418434
2.6377982166783290.146602534145399870.00053869935332715860.0214923030178635760.0001579321469294182
2.66430875151931760.146835997724803880.00057362708278473710.0215608102284892760.00016844435880324725
2.69108572389639630.147016905952369020.00065877857753783880.021613970635825760.00019371368161863418
2.7181318115737470.147132668646269420.00072776302239830290.0216480221829748120.00021414604848910907
2.74544971922775450.147109335905758530.00073365940246872420.021641156710769420.00021582529436975226
2.773042178717480.146883446918928960.00073662289274990310.021574746979037770.0002163720976768297
2.8009119493578570.146459710115029530.00102940587792031820.0214504466870301220.00030135243153311947
2.82154426815518770.14601427221404940.0015825458964657450.0213201676988059480.00046154002777712774

©2024 Hannu Parviainen