*************** Reflected Light *************** kelp implements the reflected light phase curve parameterization for an homogeneous planetary atmospheres as derived by `Heng, Morris & Kitzmann (HMK; 2021) `_. The HMK reflected light model is a function of two fundamental parameters which describe the scattering in the planetary atmosphere: the single-scattering albedo :math:`\omega`; and the scattering asymmetry factor :math:`g`. Below, we'll show how to use the reflected light formulation for a homogeneous planetary atmosphere to fit the Kepler phase curve of HAT-P-7 b; then we'll move on to the case of an inhomogeneous atmosphere with the phase curve of Kepler-7 b. Homogeneous reflectivity map ---------------------------- We can implement a model for the Kepler phase curve of HAT-P-7 b, which combines all aspects of a challenging phase curve (except atmospheric inhomogeneity, for this see the next section). HAT-P-7 b is a 2700 K planet with strong ellipsoidal variations, nearly-equal contributions to the Kepler phase curve from thermal emission and reflected light, plus detectable Doppler beaming. We'll implement each of these contributions in the example below, and solve for the maximum-likelihood single-scattering albedo and scattering asymmetry factor, as well as the amplitudes of the Doppler beaming and ellipsoidal variations. First we'll import the necessary packages (there are quite a few!): .. code-block:: python import numpy as np import matplotlib.pyplot as plt from scipy.stats import binned_statistic import theano.tensor as tt import exoplanet as xo import pymc3 as pm import pymc3_ext as pmx from corner import corner from lightkurve import search_lightcurve import astropy.units as u from astropy.constants import R_sun, R_earth from astropy.stats import sigma_clip, mad_std from kelp import Filter from kelp.theano import reflected_phase_curve, thermal_phase_curve Note that we have imported the phase curve functions from the ``theano`` module, because we will be using PyMC3 to construct the model and draw posterior samples. Now we will define the system parmaeters that we will use to construct the phase curvec and phase fold the light curve: .. code-block:: python floatX = 'float64' t0 = 2454954.357462 # Bonomo 2017 period = 2.204740 # Stassun 2017 rp = 16.9 * R_earth # Stassun 2017 rstar = 1.991 * R_sun # Berger 2017 a = 4.13 * rstar # Stassun 2017 duration = 4.0398 / 24 # Holczer 2016 b = 0.4960 # Esteves 2015 rho_star = 0.27 * u.g / u.cm ** 3 # Stassun 2017 T_s = 6449 # Berger 2018 a_rs = float(a / rstar) a_rp = float(a / rp) rp_rstar = float(rp / rstar) eclipse_half_dur = duration / period / 2 Now we will use `~lightkurve.search.search_lightcurve` to download the long cadence light curve from Quarter 10 of the Kepler observations of HAT-P-7 b. We'll then "flatten" the light curve, which applies a savgol filter to the light curve to do some long-term detrending on the out-of-transit portions of the phase curve. We will also sigma-clip the fluxes to remove outliers: .. code-block:: python lcf = search_lightcurve( "HAT-P-7", mission="Kepler", cadence="long", quarter=10 ).download_all() slc = lcf.stitch() phases = ((slc.time.jd - t0) % period) / period in_eclipse = np.abs(phases - 0.5) < eclipse_half_dur in_transit = (phases < 1.5 * eclipse_half_dur) | ( phases > 1 - 1.5 * eclipse_half_dur) out_of_transit = np.logical_not(in_transit) slc = slc.flatten( polyorder=3, break_tolerance=10, window_length=1001, mask=~out_of_transit ).remove_nans() phases = ((slc.time.jd - t0) % period) / period in_eclipse = np.abs(phases - 0.5) < eclipse_half_dur in_transit = (phases < 1.5 * eclipse_half_dur) | ( phases > 1 - 1.5 * eclipse_half_dur) out_of_transit = np.logical_not(in_transit) sc = sigma_clip( np.ascontiguousarray(slc.flux[out_of_transit], dtype=np.float64), maxiters=100, sigma=8, stdfunc=mad_std ) Next we will compute the masked phases, times, and the normalized fluxes :math:`F_p/F_\mathrm{star}` in units of ppm: .. code-block:: python phase = np.ascontiguousarray( phases[out_of_transit][~sc.mask], dtype=np.float64 ) time = np.ascontiguousarray( slc.time.jd[out_of_transit][~sc.mask], dtype=np.float64 ) bin_in_eclipse = np.abs(phase - 0.5) < eclipse_half_dur unbinned_flux_mean = np.mean(sc[~sc.mask].data) unbinned_flux_mean_ppm = 1e6 * (unbinned_flux_mean - 1) flux_normed = np.ascontiguousarray( 1e6 * (sc[~sc.mask].data / unbinned_flux_mean - 1.0), dtype=np.float64 ) flux_normed_err = np.ascontiguousarray( 1e6 * slc.flux_err[out_of_transit][~sc.mask].value, dtype=np.float64 ) Now we will median-bin the phase folded Kepler light curve: .. code-block:: python bins = 100 bs = binned_statistic( phase, flux_normed, statistic=np.median, bins=bins ) bs_err = binned_statistic( phase, flux_normed_err, statistic=lambda x: 3 * np.median(x) / len(x) ** 0.5, bins=bins ) binphase = 0.5 * (bs.bin_edges[1:] + bs.bin_edges[:-1]) # Normalize the binned fluxes by the in-eclipse flux: binflux = bs.statistic - np.median(bs.statistic[np.abs(binphase - 0.5) < 0.01]) binerror = bs_err.statistic Now we will use the `~kelp.registries.Filter` object to define the filter transmittance curve for Kepler: .. code-block:: python filt = Filter.from_name("Kepler") filt.bin_down(6) # This speeds up integration by orders of magnitude filt_wavelength, filt_trans = filt.wavelength.to(u.m).value, filt.transmittance Next we construct the PyMC3 model. This is a long block of code, so let's jump straight into in-line comments: .. code-block:: python with pm.Model() as model: # Define a Keplerian orbit using `exoplanet`: orbit = xo.orbits.KeplerianOrbit( period=period, t0=0, b=b, rho_star=rho_star.to(u.g / u.cm ** 3), r_star=float(rstar / R_sun) ) # Compute the eclipse model (no limb-darkening): eclipse_light_curves = xo.LimbDarkLightCurve([0, 0]).get_light_curve( orbit=orbit._flip(rp_rstar), r=orbit.r_star, t=binphase * period, texp=(30 * u.min).to(u.d).value ) # Normalize the eclipse model to unity out of eclipse and # zero in-eclipse eclipse = 1 + pm.math.sum(eclipse_light_curves, axis=-1) # Define reflected light phase curve model according to # Heng, Morris & Kitzmann (2021) omega = pm.Uniform('omega', lower=0, upper=1) g = pm.TruncatedNormal('g', lower=0, upper=1, mu=0, sigma=0.4) reflected_ppm, A_g, q = reflected_phase_curve(binphase, omega, g, a_rp) # Define the ellipsoidal variation parameterization (simple sinusoid) ellipsoidal_amp = pm.Uniform('ellip_amp', lower=0, upper=50) ellipsoidal_model_ppm = - ellipsoidal_amp * tt.cos( 4 * np.pi * (binphase - 0.5)) + ellipsoidal_amp # Define the doppler variation parameterization (simple sinusoid) doppler_amp = pm.Uniform('doppler_amp', lower=0, upper=50) doppler_model_ppm = doppler_amp * tt.sin( 2 * np.pi * binphase) # Define the thermal emission model according to description in # Morris et al. (in prep) xi = 2 * np.pi * (binphase - 0.5) n_phi = 75 n_theta = 5 phi = np.linspace(-2 * np.pi, 2 * np.pi, n_phi, dtype=floatX) theta = np.linspace(0, np.pi, n_theta, dtype=floatX) theta2d, phi2d = np.meshgrid(theta, phi) ln_C_11_kepler = -2.6 C_11_kepler = tt.exp(ln_C_11_kepler) hml_eps = 0.72 hml_f = (2/3 - hml_eps * 5 / 12) ** 0.25 delta_phi = 0 A_B = 0.5 # Compute the thermal phase curve with zero phase offset thermal, T = thermal_phase_curve( xi, delta_phi, 4.5, 0.575, C_11_kepler, T_s, a_rs, 1 / a_rp, A_B, theta2d, phi2d, filt_wavelength, filt_trans, 2 ** -0.5 ) # Define the composite phase curve model flux_norm = eclipse * ( reflected_ppm + ellipsoidal_model_ppm + doppler_model_ppm + 1e6 * thermal ) # Keep track of the geometric albedo and integral phase function at # each step in the chain pm.Deterministic('A_g', A_g) pm.Deterministic('q', q) # Define the likelihood pm.Normal('obs', mu=flux_norm, sigma=binerror, observed=binflux) # Optimize a fast maximum-likelihood solution to seed posterior draws: map_soln = pm.find_MAP() Now our model is set up, and we are ready to draw posterior samples from the model given the data, which we will do with `pymc3-ext `_ for the most efficient posterior sampling of our degenerate phase curve parameterization. This will take up to a minute: .. code-block:: python with model: trace = pmx.sample( draws=100, tune=10, start=map_soln, compute_convergence_checks=False, target_accept=0.95, initial_accept=0.2, return_inferencedata=False ) Let's finally plot the final results: .. code-block:: python with model: corner(pm.trace_to_dataframe(trace)); plt.show() plt.errorbar(binphase, binflux, binerror, fmt='.', color='k', ecolor='silver') with model: for sample in xo.get_samples_from_trace(trace, size=10): plt.plot(binphase, pmx.eval_in_model(flux_norm, sample), alpha=0.5, color='r', zorder=10) plt.plot(binphase, pmx.eval_in_model(reflected_ppm, sample), color='DodgerBlue', zorder=10, label='reflected') plt.plot(binphase, pmx.eval_in_model(1e6 * thermal, sample), color='m', zorder=10, label='thermal') plt.plot(binphase, pmx.eval_in_model(ellipsoidal_model_ppm, sample), color='b', zorder=10, label='ellipsoidal') plt.plot(binphase, pmx.eval_in_model(doppler_model_ppm, sample), color='g', zorder=10, label='doppler') plt.legend() plt.ylim([-30, 110]) for sp in ['right', 'top']: plt.gca().spines[sp].set_visible(False) plt.gca().set(xlabel='Phase', ylabel='$F_p/F_\mathrm{star}$ [ppm]', title='HAT-P-7 b') plt.show() .. plot:: import numpy as np import matplotlib.pyplot as plt from scipy.stats import binned_statistic import theano.tensor as tt import exoplanet as xo import pymc3 as pm import pymc3_ext as pmx from corner import corner from lightkurve import search_lightcurve import astropy.units as u from astropy.constants import R_sun, R_earth from astropy.stats import sigma_clip, mad_std from kelp import Filter from kelp.theano import reflected_phase_curve, thermal_phase_curve floatX = 'float64' t0 = 2454954.357462 # Bonomo 2017 period = 2.204740 # Stassun 2017 rp = 16.9 * R_earth # Stassun 2017 rstar = 1.991 * R_sun # Berger 2017 a = 4.13 * rstar # Stassun 2017 duration = 4.0398 / 24 # Holczer 2016 b = 0.4960 # Esteves 2015 rho_star = 0.27 * u.g / u.cm ** 3 # Stassun 2017 T_s = 6449 # Berger 2018 a_rs = float(a / rstar) a_rp = float(a / rp) rp_rstar = float(rp / rstar) eclipse_half_dur = duration / period / 2 lcf = search_lightcurve( "HAT-P-7", mission="Kepler", cadence="long", quarter=10 ).download_all() slc = lcf.stitch() phases = ((slc.time.jd - t0) % period) / period in_eclipse = np.abs(phases - 0.5) < eclipse_half_dur in_transit = (phases < 1.5 * eclipse_half_dur) | ( phases > 1 - 1.5 * eclipse_half_dur) out_of_transit = np.logical_not(in_transit) slc = slc.flatten( polyorder=3, break_tolerance=10, window_length=1001, mask=~out_of_transit ).remove_nans() phases = ((slc.time.jd - t0) % period) / period in_eclipse = np.abs(phases - 0.5) < eclipse_half_dur in_transit = (phases < 1.5 * eclipse_half_dur) | ( phases > 1 - 1.5 * eclipse_half_dur) out_of_transit = np.logical_not(in_transit) sc = sigma_clip( np.ascontiguousarray(slc.flux[out_of_transit], dtype=np.float64), maxiters=100, sigma=8, stdfunc=mad_std ) phase = np.ascontiguousarray( phases[out_of_transit][~sc.mask], dtype=np.float64 ) time = np.ascontiguousarray( slc.time.jd[out_of_transit][~sc.mask], dtype=np.float64 ) bin_in_eclipse = np.abs(phase - 0.5) < eclipse_half_dur unbinned_flux_mean = np.mean(sc[~sc.mask].data) # .mean() unbinned_flux_mean_ppm = 1e6 * (unbinned_flux_mean - 1) flux_normed = np.ascontiguousarray( 1e6 * (sc[~sc.mask].data / unbinned_flux_mean - 1.0), dtype=np.float64 ) flux_normed_err = np.ascontiguousarray( 1e6 * slc.flux_err[out_of_transit][~sc.mask].value, dtype=np.float64 ) bins = 100 bs = binned_statistic( phase, flux_normed, statistic=np.median, bins=bins ) bs_err = binned_statistic( phase, flux_normed_err, statistic=lambda x: 3 * np.median(x) / len(x) ** 0.5, bins=bins ) binphase = 0.5 * (bs.bin_edges[1:] + bs.bin_edges[:-1]) # Normalize the binned fluxes by the in-eclipse flux: binflux = bs.statistic - np.median(bs.statistic[np.abs(binphase - 0.5) < 0.01]) binerror = bs_err.statistic filt = Filter.from_name("Kepler") filt.bin_down(6) # This speeds up integration by orders of magnitude filt_wavelength, filt_trans = filt.wavelength.to(u.m).value, filt.transmittance with pm.Model() as model: # Define a Keplerian orbit using `exoplanet`: orbit = xo.orbits.KeplerianOrbit( period=period, t0=0, b=b, rho_star=rho_star.to(u.g / u.cm ** 3), r_star=float(rstar / R_sun) ) # Compute the eclipse model (no limb-darkening): eclipse_light_curves = xo.LimbDarkLightCurve([0, 0]).get_light_curve( orbit=orbit._flip(rp_rstar), r=orbit.r_star, t=binphase * period, texp=(30 * u.min).to(u.d).value ) # Normalize the eclipse model to unity out of eclipse and # zero in-eclipse eclipse = 1 + pm.math.sum(eclipse_light_curves, axis=-1) # Define reflected light phase curve model according to # Heng, Morris & Kitzmann (2021) omega = pm.Uniform('omega', lower=0, upper=1) g = pm.TruncatedNormal('g', lower=0, upper=1, mu=0, sigma=0.4) reflected_ppm, A_g, q = reflected_phase_curve(binphase, omega, g, a_rp) # Define the ellipsoidal variation parameterization (simple sinusoid) ellipsoidal_amp = pm.Uniform('ellip_amp', lower=0, upper=50) ellipsoidal_model_ppm = - ellipsoidal_amp * tt.cos( 4 * np.pi * (binphase - 0.5)) + ellipsoidal_amp # Define the doppler variation parameterization (simple sinusoid) doppler_amp = pm.Uniform('doppler_amp', lower=0, upper=50) doppler_model_ppm = doppler_amp * tt.sin( 2 * np.pi * binphase) # Define the thermal emission model according to description in # Morris et al. (in prep) xi = 2 * np.pi * (binphase - 0.5) n_phi = 75 n_theta = 5 phi = np.linspace(-2 * np.pi, 2 * np.pi, n_phi, dtype=floatX) theta = np.linspace(0, np.pi, n_theta, dtype=floatX) theta2d, phi2d = np.meshgrid(theta, phi) ln_C_11_kepler = -2.6 C_11_kepler = tt.exp(ln_C_11_kepler) hml_eps = 0.72 hml_f = (2/3 - hml_eps * 5 / 12) ** 0.25 delta_phi = 0 A_B = 0.5 # Compute the thermal phase curve with zero phase offset thermal, T = thermal_phase_curve( xi, delta_phi, 4.5, 0.575, C_11_kepler, T_s, a_rs, 1 / a_rp, A_B, theta2d, phi2d, filt_wavelength, filt_trans, 2 ** -0.5 ) # Define the composite phase curve model flux_norm = eclipse * ( reflected_ppm + ellipsoidal_model_ppm + doppler_model_ppm + 1e6 * thermal ) # Keep track of the geometric albedo and integral phase function at # each step in the chain pm.Deterministic('A_g', A_g) pm.Deterministic('q', q) # Define the likelihood pm.Normal('obs', mu=flux_norm, sigma=binerror, observed=binflux) # Optimize a fast maximum-likelihood solution to seed posterior draws: map_soln = pm.find_MAP() with model: trace = pmx.sample( draws=1000, tune=50, start=map_soln, compute_convergence_checks=False, target_accept=0.95, initial_accept=0.2, return_inferencedata=False, cores=1, chains=1 ) with model: corner(pm.trace_to_dataframe(trace)); plt.show() plt.errorbar(binphase, binflux, binerror, fmt='.', color='k', ecolor='silver') with model: for i, sample in enumerate(pmx.get_samples_from_trace(trace, size=10)): plt.plot(binphase, pmx.eval_in_model(flux_norm, sample), alpha=0.5, color='r', zorder=10) plt.plot(binphase, pmx.eval_in_model(reflected_ppm, sample), color='DodgerBlue', zorder=10, label='reflected' if i==0 else None) plt.plot(binphase, pmx.eval_in_model(1e6 * thermal, sample), color='m', zorder=10, label='thermal' if i==0 else None) plt.plot(binphase, pmx.eval_in_model(ellipsoidal_model_ppm, sample), color='b', zorder=10, label='ellipsoidal' if i==0 else None) plt.plot(binphase, pmx.eval_in_model(doppler_model_ppm, sample), color='g', zorder=10, label='doppler' if i==0 else None) plt.legend() plt.ylim([-30, 120]) for sp in ['right', 'top']: plt.gca().spines[sp].set_visible(False) plt.gca().set(xlabel='Phase', ylabel='$F_p/F_\mathrm{star}$ [ppm]', title='HAT-P-7 b') plt.show() In the above corner plot, you'll see the joint posterior correlation plots for each of the free parameters in the fit, including the single-scattering albedo :math:`\omega`, the scattering asymmetry factor :math:`g`, and the derived parameters including the Bond albedo :math:`A_B`, the geometric albedo :math:`A_g`, and the integral phase function :math:`q`. You'll also see a plot above with several draws from the posteriors for each parameter plotted in light-curve space, showing the range of plausible contributions from each phase curve component shown in different colors. Inhomogeneous reflectivity map ------------------------------ Kepler-7 b is a warm Jupiter with an insignificant thermal emission contribution to the Kepler phase curve, but with a significant phase curve asymmetry, possibly resulting from an inhomogeneous albedo distribution on the surface of the planet. In this example, we'll give two parameters for the single scattering albedo in the brighter and darker regions, one scattering asymmetry factor, and one geometric albedo. .. note:: The analysis presented in this documentation is meant to be a quick-and-dirty example that demonstrates the capabilities of kelp, but is not meant to precisely reproduce the results of Heng, Morris & Kitzmann (2021). The results presented in that paper require a more complex and expensive model, so we opt to show a simpler and cheaper model for this tutorial. As with the previous example, we begin with some imports: .. code-block:: python import numpy as np import matplotlib.pyplot as plt from scipy.stats import binned_statistic import theano floatX = 'float64' theano.config.floatX = floatX import exoplanet as xo import pymc3 as pm import pymc3_ext as pmx from corner import corner from lightkurve import search_lightcurve import astropy.units as u from astropy.constants import R_jup, R_sun from astropy.stats import sigma_clip, mad_std from kelp.theano import reflected_phase_curve_inhomogeneous We also define the system parameters: .. code-block:: python t0 = 2454967.27687 # Esteves et al. 2015 period = 4.8854892 # Esteves et al. 2015 rp = 1.622 * R_jup # Esteves et al. 2015 rstar = 1.966 * R_sun # ±0.013 (NASA Exoplanet Archive) a = 0.06067 * u.AU # Esteves et al. 2015 duration = 5.1313 / 24 # Morton et al. 2016 b = 0.5599 # Esteves et al. 2015 +0.0045-0.0046 rho_star = 0.238 * u.g / u.cm ** 3 # Southworth et al. 2012 ±0.010 T_s = 5933 # NASA Exoplanet Archive a_rs = float(a / rstar) a_rp = float(a / rp) rp_rstar = float(rp / rstar) eclipse_half_dur = duration / period / 2 And we use ``lightkurve`` to download the entire Kepler-7 b long cadence light curve over all quarters: .. code-block:: python lcf = search_lightcurve( "Kepler-7", mission="Kepler", cadence="long", ).download_all() slc = lcf.stitch().remove_nans() phases = ((slc.time.jd - t0) % period) / period in_eclipse = np.abs(phases - 0.5) < eclipse_half_dur in_transit = (phases < 1.2 * eclipse_half_dur) | ( phases > 1 - 1.2 * eclipse_half_dur) out_of_transit = np.logical_not(in_transit) Next we sigma clip, normalize, and bin the Kepler-7 b observations: .. code-block:: python sc = sigma_clip( np.ascontiguousarray(slc.flux[out_of_transit], dtype=np.float64), maxiters=100, sigma=8, stdfunc=mad_std ) phase = np.ascontiguousarray(phases[out_of_transit][~sc.mask], dtype=np.float64) time = np.ascontiguousarray(slc.time.jd[out_of_transit][~sc.mask], dtype=np.float64) bin_in_eclipse = np.abs(phase - 0.5) < eclipse_half_dur unbinned_flux_mean = np.mean(sc[~sc.mask].data) unbinned_flux_mean_ppm = 1e6 * (unbinned_flux_mean - 1) flux_normed = np.ascontiguousarray( 1e6 * (sc[~sc.mask].data / unbinned_flux_mean - 1.0), dtype=np.float64) flux_normed_err = np.ascontiguousarray( 1e6 * slc.flux_err[out_of_transit][~sc.mask].value, dtype=np.float64) bins = 100 bs = binned_statistic(phase, flux_normed, statistic=np.median, bins=bins) bs_err = binned_statistic(phase, flux_normed_err, statistic=lambda x: np.median(x) / len(x) ** 0.5, bins=bins) binphase = 0.5 * (bs.bin_edges[1:] + bs.bin_edges[:-1]) binflux = bs.statistic - np.median(bs.statistic[np.abs(binphase - 0.5) < 0.01]) binerror = bs_err.statistic Now we construct a the phase curve model. This time there are only two components: the eclipse and the inhomogeneous reflected light phase curve. For the purposes of making this example fast, we will fix the single scattering albedo of the darker region to :math:`\omega_0 = 0` and the single scattering albedo of the more reflective region to :math:`\omega^\prime = 0.95`, and fix the start and stop longitudes of the darker region ``x1, x2 = 0, 0.8`` radians. We'll also add a constant ``offset`` term to the entire phase curve to account for the light curve normalization. .. code-block:: python with pm.Model() as model: # Define a Keplerian orbit: orbit = xo.orbits.KeplerianOrbit( period=period, t0=0, b=b, rho_star=rho_star.to(u.g / u.cm ** 3), r_star=float(rstar / R_sun) ) # Compute the eclipse model (no LD): eclipse_light_curves_kepler = xo.LimbDarkLightCurve([0, 0]).get_light_curve( orbit=orbit._flip(rp_rstar), r=orbit.r_star, t=binphase.astype(floatX) * period, texp=(30 * u.min).to(u.d).value ) # Normalize the eclipse model: eclipse_kepler = 1 + pm.math.sum(eclipse_light_curves_kepler, axis=-1) omega_0 = 0 omega_prime = 0.95 # Define the start and stop longitudes of the darker region x1 = 0 # [radians] x2 = 0.8 # [radians] # Sample for the geometric albedo A_g = pm.Uniform('A_g', lower=0, upper=1) # construct an inhomogeneous reflected light phase curve model flux_ratio_ppm, g, q = reflected_phase_curve_inhomogeneous( binphase, omega_0, omega_prime, x1, x2, A_g, a_rp ) # Apply a constant offset to the entire phase curve to account for normalization bias offset = pm.Uniform('offset', lower=-20, upper=20) # Construct a composite phase curve model flux_norm = eclipse_kepler * flux_ratio_ppm + offset # Keep track of the q and g values at each step in the chains pm.Deterministic('q', q) pm.Deterministic('g', g) # Construct our likelihood pm.Normal('obs_kepler', mu=flux_norm, sigma=binerror, observed=binflux) # Solve for a quick best-fit using scipy: map_soln = pm.find_MAP() # Plot the results fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(binphase, binflux, 'k.') ax.plot(binphase, pmx.eval_in_model(flux_norm, map_soln), 'r', lw=2, label='composite') plt.show() Now that the model is constructed, we're ready to sample from the posterior distribution for the geometric albedo, the integral phase function, and the scattering asymmetry factor, which we do again with `pymc3-ext `_ for the most efficient posterior sampling of our degenerate phase curve parameterization: .. code-block:: python with model: trace = pmx.sample( draws=1000, tune=100, compute_convergence_checks=False, target_accept=0.95, initial_accept=0.2, return_inferencedata=False, ) We can plot the results with the following commands: .. code-block:: python corner(pm.trace_to_dataframe(trace)); plt.show() fig, ax = plt.subplots(figsize=(10, 4)) ax.errorbar(binphase, binflux, binerror, fmt='.', color='k', ecolor='silver') with model: for i, sample in enumerate(pmx.get_samples_from_trace(trace, size=5)): ax.plot(binphase, pmx.eval_in_model(flux_norm, sample), 'r') ax.set(xlabel='Phase', ylabel='$F_p/F_\mathrm{star}$ [ppm]', title='Kepler-7 b') for sp in ['right', 'top']: ax.spines[sp].set_visible(False) .. plot:: import numpy as np import matplotlib.pyplot as plt from scipy.stats import binned_statistic import theano floatX = 'float64' theano.config.floatX = floatX import exoplanet as xo import pymc3 as pm import pymc3_ext as pmx from corner import corner from lightkurve import search_lightcurve import astropy.units as u from astropy.constants import R_jup, R_sun from kelp.theano import reflected_phase_curve_inhomogeneous t0 = 2454967.27687 # Esteves et al. 2015 period = 4.8854892 # Esteves et al. 2015 rp = 1.622 * R_jup # Esteves et al. 2015 rstar = 1.966 * R_sun # ±0.013 (NASA Exoplanet Archive) a = 0.06067 * u.AU # Esteves et al. 2015 duration = 5.1313 / 24 # Morton et al. 2016 b = 0.5599 # Esteves et al. 2015 +0.0045-0.0046 rho_star = 0.238 * u.g / u.cm ** 3 # Southworth et al. 2012 ±0.010 T_s = 5933 # NASA Exoplanet Archive a_rs = float(a / rstar) a_rp = float(a / rp) rp_rstar = float(rp / rstar) eclipse_half_dur = duration / period / 2 lcf = search_lightcurve( "Kepler-7", mission="Kepler", cadence="long", ).download_all() slc = lcf.stitch().remove_nans() phases = ((slc.time.jd - t0) % period) / period in_eclipse = np.abs(phases - 0.5) < eclipse_half_dur in_transit = (phases < 1.2 * eclipse_half_dur) | ( phases > 1 - 1.2 * eclipse_half_dur) out_of_transit = np.logical_not(in_transit) from astropy.stats import sigma_clip, mad_std sc = sigma_clip( np.ascontiguousarray(slc.flux[out_of_transit], dtype=np.float64), maxiters=100, sigma=8, stdfunc=mad_std ) phase = np.ascontiguousarray(phases[out_of_transit][~sc.mask], dtype=np.float64) time = np.ascontiguousarray(slc.time.jd[out_of_transit][~sc.mask], dtype=np.float64) bin_in_eclipse = np.abs(phase - 0.5) < eclipse_half_dur unbinned_flux_mean = np.mean(sc[~sc.mask].data) # .mean() unbinned_flux_mean_ppm = 1e6 * (unbinned_flux_mean - 1) flux_normed = np.ascontiguousarray( 1e6 * (sc[~sc.mask].data / unbinned_flux_mean - 1.0), dtype=np.float64) flux_normed_err = np.ascontiguousarray( 1e6 * slc.flux_err[out_of_transit][~sc.mask].value, dtype=np.float64) bins = 100 bs = binned_statistic(phase, flux_normed, statistic=np.median, bins=bins) bs_err = binned_statistic(phase, flux_normed_err, statistic=lambda x: np.median(x) / len(x) ** 0.5, bins=bins) binphase = 0.5 * (bs.bin_edges[1:] + bs.bin_edges[:-1]) binflux = bs.statistic - np.median(bs.statistic[np.abs(binphase - 0.5) < 0.01]) binerror = bs_err.statistic with pm.Model() as model: # Define a Keplerian orbit: orbit = xo.orbits.KeplerianOrbit( period=period, t0=0, b=b, rho_star=rho_star.to(u.g / u.cm ** 3), r_star=float(rstar / R_sun) ) # Compute the eclipse model (no LD): eclipse_light_curves_kepler = xo.LimbDarkLightCurve([0, 0]).get_light_curve( orbit=orbit._flip(rp_rstar), r=orbit.r_star, t=binphase.astype(floatX) * period, texp=(30 * u.min).to(u.d).value ) # Normalize the eclipse model: eclipse_kepler = 1 + pm.math.sum(eclipse_light_curves_kepler, axis=-1) omega_0 = 0 omega_prime = 0.95 # Define the start and stop longitudes of the darker region x1 = 0 # [radians] x2 = 0.8 # [radians] # Sample for the geometric albedo A_g = pm.Uniform('A_g', lower=0, upper=1) # construct an inhomogeneous reflected light phase curve model flux_ratio_ppm, g, q = reflected_phase_curve_inhomogeneous( binphase, omega_0, omega_prime, x1, x2, A_g, a_rp ) # Apply a constant offset to the entire phase curve to account for normalization bias offset = pm.Uniform('offset', lower=-20, upper=20) # Construct a composite phase curve model flux_norm = eclipse_kepler * flux_ratio_ppm + offset # Keep track of the q and g values at each step in the chains pm.Deterministic('q', q) pm.Deterministic('g', g) # Construct our likelihood pm.Normal('obs_kepler', mu=flux_norm, sigma=binerror, observed=binflux) # Solve for a quick best-fit using scipy: map_soln = pm.find_MAP() with model: trace = pmx.sample( draws=1000, tune=100, compute_convergence_checks=False, target_accept=0.95, initial_accept=0.2, return_inferencedata=False, cores=1, chains=1 ) corner(pm.trace_to_dataframe(trace)); plt.show() fig, ax = plt.subplots(figsize=(10, 4)) ax.errorbar(binphase, binflux, binerror, fmt='.', color='k', ecolor='silver') with model: for i, sample in enumerate(pmx.get_samples_from_trace(trace, size=5)): ax.plot(binphase, pmx.eval_in_model(flux_norm, sample), 'r', label='composite' if i == 0 else None) ax.set(xlabel='Phase', ylabel='$F_p/F_\mathrm{star}$ [ppm]', title='Kepler-7 b') for sp in ['right', 'top']: ax.spines[sp].set_visible(False) In the corner plot above, you'll see that the solution has a geometric albedo near :math:`A_g = 0.24`, and a scattering asymmetry factor near zero. The draws from the posteriors for each parameter produce phase curve models that are asymmetric (as we intended) which match the shape of the observations well, despite having only a few free parameters.