************ Optimization ************ scipy and emcee --------------- First let's import the necessary packages, which include ``scipy`` and ``emcee`` for non-linear optimization and MCMC respectively. .. code-block:: python import matplotlib.pyplot as plt import numpy as np from kelp import Model, Planet, Filter from scipy.optimize import minimize from emcee import EnsembleSampler from multiprocessing import Pool from corner import corner np.random.seed(42) Next let's set up the properties of the `~kelp.Planet`, which we'll assume is like HD 189733 b, and the `~kelp.Filter` which we'll assume is the Spitzer/IRAC Channel 1 (3.6 micron): .. code-block:: python planet = Planet.from_name('HD 189733') filt = Filter.from_name("IRAC 1") filt.bin_down(10) # this speeds up the integration We'll also set up the model parameters using the :math:`h_{m\ell}` basis: .. code-block:: python hotspot_offset = np.radians(-40) # Hotspot offset alpha = 0.6 # dimensionless fluid number omega = 4.5 # dimensionless drag frequency f = 2**-0.5 # greenhouse parameter A_B = 0 # Bond albedo lmax = 1 # \ell_{max} C = [[0], # C_{m \ell} terms [0, 0.15, 0]] obs_err = 50 # Observational white noise Now we'll initialize a noisy instance of the model to be our "truth" in this example, which we will recover with optimization techniques: .. code-block:: python # `xi` is the time axis of the phase curve, going from -pi (transit) to # 0 (eclipse) and back to pi (transit) xi = np.linspace(-np.pi, np.pi, 50) # Compute the phase curve of the exoplanet: model = Model(hotspot_offset, alpha, omega, A_B, C, lmax, planet=planet, filt=filt) obs = model.thermal_phase_curve(xi, f=f).flux # Add random noise to the light curve: obs += obs_err * np.random.randn(xi.shape[0]) # Plot the simulated "observations" errkwargs = dict(color='k', fmt='.', ecolor='silver') plt.errorbar(xi / np.pi, obs, obs_err, **errkwargs) plt.xlabel('$\\xi/\\pi$') plt.ylabel('$\\rm F_p/F_s$') .. plot:: import matplotlib.pyplot as plt import numpy as np from kelp import Model, Planet, Filter np.random.seed(42) planet = Planet.from_name('HD 189733') filt = Filter.from_name("IRAC 1") filt.bin_down(10) xi = np.linspace(-np.pi, np.pi, 50) hotspot_offset = np.radians(-40) alpha = 0.6 omega = 4.5 f = 2**-0.5 A_B = 0 lmax = 1 C = [[0], [0, 0.15, 0]] obs_err = 50 model = Model(hotspot_offset, alpha, omega, A_B, C, lmax, planet=planet, filt=filt) obs = model.thermal_phase_curve(xi, f=f).flux obs += obs_err * np.random.randn(xi.shape[0]) errkwargs = dict(color='k', fmt='.', ecolor='silver') plt.errorbar(xi / np.pi, obs, obs_err, **errkwargs) plt.xlabel('$\\xi/\\pi$') plt.ylabel('$\\rm F_p/F_s$') plt.tight_layout() plt.show() The simulated observations ``obs`` have small scale, uncorrelated scatter about the phase curve mean model. Next we'll use ``scipy`` to find the best-fit values for the hotspot offset :math:`\Delta \phi` and the power in the :math:`C_{11}` spherical harmonic coefficient term: .. code-block:: python def pc_model(p, x): """ Phase curve model with two free parameters """ offset, c_11, f = p C = [[0], [0, c_11, 0]] model = Model(hotspot_offset=offset, alpha=alpha, omega_drag=omega, A_B=A_B, C_ml=C, lmax=1, planet=planet, filt=filt) return model.thermal_phase_curve(x, f=f, check_sorted=False).flux def lnprior(p): """ Log-prior: sets reasonable bounds on the fitting parameters """ offset, c_11, f = p if (offset > np.pi or offset < -np.pi or c_11 > 1 or c_11 < 0): return -np.inf return 0 def lnlike(p, x, y, yerr): """ Log-likelihood: via the chi^2 """ return -0.5 * np.sum((pc_model(p, x) - y)**2 / yerr**2) def lnprob(p, x, y, yerr): """ Log probability: sum of lnlike and lnprior """ lp = lnprior(p) if np.isfinite(lp): return lp + lnlike(p, x, y, yerr) return -np.inf initp = np.array([-0.7, 0.1]) bounds = [[0, 2], [0.1, 1]] soln = minimize(lambda *args: -lnprob(*args), initp, args=(xi, obs, obs_err), method='powell') ``soln.x`` now contains the best-fit (:math:`\Delta \phi`, :math:`C_{11}`) parameters from Powell's method. With this guess at the maximum a posteriori values for the free parameters, let's now use Markov Chain Monte Carlo to measure the uncertainty on the maximum-likelihood parameters: .. code-block:: python ndim = 3 nwalkers = 2 * ndim # in real life, you should scale this factor up # Generate initial positions for the walkers p0 = [soln.x + 0.1 * np.random.randn(ndim) for i in range(nwalkers)] # Run the ensemble sampler: with Pool() as pool: sampler = EnsembleSampler(nwalkers, ndim, lnprob, args=(xi, obs, obs_err), pool=pool) p1 = sampler.run_mcmc(p0, 100) sampler.reset() sampler.run_mcmc(p1, 500, progress=True) # Plot the corner plot with the posteriors: corner(sampler.flatchain, truths=[hotspot_offset, C[1][1], 2**-0.5], labels=['$\Delta \phi$', '$C_{11}$', '$f$']) plt.show() # Extract the maximum a posteriori parameters, plot the corresponding model p_map = sampler.flatchain[np.argmax(sampler.flatlnprobability)] plt.errorbar(xi/np.pi, obs, obs_err, **errkwargs) plt.plot(xi/np.pi, pc_model(p_map, xi), color='r') plt.xlabel('$\\xi/\\pi$') plt.ylabel('$\\rm F_p/F_s$') plt.show() .. plot:: import matplotlib.pyplot as plt import numpy as np from kelp import Model, Planet, Filter np.random.seed(42) planet = Planet.from_name('HD 189733') filt = Filter.from_name("IRAC 1") filt.bin_down(10) xi = np.linspace(-np.pi, np.pi, 50) hotspot_offset = np.radians(-40) alpha = 0.6 omega = 4.5 f = 2**-0.5 A_B = 0 lmax = 1 C = [[0], [0, 0.15, 0]] obs_err = 50 model = Model(hotspot_offset, alpha, omega, A_B, C, lmax, planet=planet, filt=filt) obs = model.thermal_phase_curve(xi, f=f).flux obs += obs_err * np.random.randn(xi.shape[0]) def pc_model(p, x): """ Phase curve model with two free parameters """ offset, c_11, f = p C = [[0], [0, c_11, 0]] model = Model(hotspot_offset=offset, alpha=alpha, omega_drag=omega, A_B=A_B, C_ml=C, lmax=1, planet=planet, filt=filt) return model.thermal_phase_curve(x, f=f, check_sorted=False).flux def lnprior(p): """ Log-prior: sets reasonable bounds on the fitting parameters """ offset, c_11, f = p if (offset > np.pi or offset < -np.pi or c_11 > 1 or c_11 < 0): return -np.inf return 0 def lnlike(p, x, y, yerr): """ Log-likelihood: via the chi^2 """ return -0.5 * np.sum((pc_model(p, x) - y)**2 / yerr**2) def lnprob(p, x, y, yerr): """ Log probability: sum of lnlike and lnprior """ lp = lnprior(p) if np.isfinite(lp): return lp + lnlike(p, x, y, yerr) return -np.inf from scipy.optimize import minimize initp = np.array([-0.7, 0.2, 2**-0.5]) bounds = [[-2, 0], [0.0, 1], [0.5, 0.85]] soln = minimize(lambda *args: -lnprob(*args), initp, args=(xi, obs, obs_err), method='powell') from emcee import EnsembleSampler from multiprocessing import Pool from corner import corner ndim = 3 nwalkers = 2 * ndim p0 = [soln.x + 0.1 * np.random.randn(ndim) for i in range(nwalkers)] sampler = EnsembleSampler(nwalkers, ndim, lnprob, args=(xi, obs, obs_err)) p1 = sampler.run_mcmc(p0, 100) sampler.reset() sampler.run_mcmc(p1, 100, progress=True) corner(sampler.flatchain, truths=[hotspot_offset, C[1][1], 2**-0.5], labels=['$\Delta \phi$', '$C_{11}$', '$f$']) plt.show() p_map = sampler.flatchain[np.argmax(sampler.flatlnprobability)] errkwargs = dict(color='k', fmt='.', ecolor='silver') plt.errorbar(xi/np.pi, obs, obs_err, **errkwargs) plt.plot(xi/np.pi, pc_model(p_map, xi), color='r') plt.xlabel('$\\xi/\\pi$') plt.ylabel('$\\rm F_p/F_s$') plt.tight_layout() plt.show() The blue lines on the corner plot represent the "true" (input) values which we used to construct the simulated observations. The recovered (:math:`\Delta \phi`, :math:`C_{11}`) parameters are consistent with their true values. The maximum likelihood parameters generate a model (red) that looks very consistent with the observations (black). When doing this integration for non-demonstration purposes, you should tweak the number of walkers to be more like a factor of 5-10 greater than the number of dimensions, and the number of steps should be increased by a factor of at least a few. PyMC3 ----- The :math:`h_{m\ell}` basis has also been implemented within kelp using theano for compatibility with PyMC3. Here's a simple example that shows inference with the theano module: .. code-block:: python import multiprocessing as mp import numpy as np import matplotlib.pyplot as plt import astropy.units as u import pymc3 as pm from corner import corner floatX = 'float32' from kelp import theano, Model, Filter, Planet np.random.seed(42) planet = Planet.from_name('HD 189733') filt = Filter.from_name("IRAC 1") filt.bin_down(5) # Set orbital phase axis: xi = np.linspace(-np.pi, np.pi, 100) hotspot_offset = np.radians(-40) alpha = 0.6 omega = 4.5 true_f = 2 ** -0.5 A_B = 0 lmax = 1 true_C_11 = 0.15 C = [[0], [0, true_C_11, 0]] obs_err = 50 model = Model(hotspot_offset, alpha, omega, A_B, C, lmax, planet=planet, filt=filt) obs = model.thermal_phase_curve(xi, f=true_f).flux obs += obs_err * np.random.randn(xi.shape[0]) # Set planet parameters: lmax = 1 T_s = planet.T_s a_rs = planet.a rp_a = planet.rp_a # Set resolution of grid points on sphere: n_phi = 100 n_theta = 10 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) with pm.Model() as hml_model: delta_phi = pm.Uniform( "delta_phi", lower=np.radians(-90), upper=np.radians(90), testval=np.radians(-40) ) C_11 = pm.Uniform( "C_11", lower=0.01, upper=0.3, testval=0.15 ) f = pm.Normal( "f", mu=2**-0.5, sigma=0.1 ) thermal_phase_curve, temperature_map, ps = theano.thermal_phase_curve( xi.astype(floatX), delta_phi, omega, alpha, C_11, T_s, a_rs, rp_a, A_B, theta2d.astype(floatX), phi2d.astype(floatX), filt.wavelength.to(u.m).value.astype(floatX), filt.transmittance.astype(floatX), true_f ) pm.Normal( "obs", mu=thermal_phase_curve, observed=obs, sigma=obs_err ) trace = pm.sample( draws=1000, tune=1000, compute_convergence_checks=False, mp_ctx=mp.get_context('fork') ) truths = { "delta_phi": hotspot_offset, "C_11": true_C_11, "f": true_f } df = pm.trace_to_dataframe(trace)[truths.keys()] corner( df, truths=[truths[k] for k in truths.keys()] ) plt.show()