# Data & models#

import exoplanet

exoplanet.utils.docs_setup()
print(f"exoplanet.__version__ = '{exoplanet.__version__}'")

WARNING (theano.tensor.blas): Using NumPy C-API based implementation for BLAS functions.

exoplanet.__version__ = '0.5.4.dev21+g6b0c682.d20230829'


This tutorial provides an overview of the kinds of datasets that exoplanet has been designed to model. It’s worth noting that this is certainly not a comprehensive list and exoplanet is designed to be low-level library that can be built on top of to support new use cases.

Tip

For real world use cases and examples, check out the Case Studies page.

## Overview#

One primary interface to exoplanet is via the exoplanet.orbits.KeplerianOrbit object (and its generalizations like exoplanet.orbits.TTVOrbit). The KeplerianOrbit object defines a set of independent (but possibly massive) bodies orbiting a central body on Keplerian trajectories. These orbits are parameterized by orbital elements, and one of the primary uses of exoplanet is the measurement of these orbital elements given observations.

Given a KeplerianOrbit, users of exoplanet can evaluate things like the positions and velocities of all the bodies as a function of time. For example, here’s how you could define an orbit with a single body and plot the orbits:

import numpy as np
import matplotlib.pyplot as plt
import exoplanet as xo

# Define the orbit
orbit = xo.orbits.KeplerianOrbit(
period=10.0,  # All times are in days
t0=0.5,
incl=0.5 * np.pi,  # All angles are in radians
ecc=0.3,
omega=-2.5,
Omega=1.2,
m_planet=0.05,  # All masses and distances are in Solar units
)

# Get the position and velocity as a function of time
t = np.linspace(0, 20, 5000)
x, y, z = orbit.get_relative_position(t)
vx, vy, vz = orbit.get_star_velocity(t)

# Plot the coordinates
# Note the use of .eval() throughout since the coordinates are all
# Aesara/Theano objects
fig, axes = plt.subplots(2, 1, figsize=(8, 8), sharex=True)
ax = axes
ax.plot(t, x.eval(), label="x")
ax.plot(t, y.eval(), label="y")
ax.plot(t, z.eval(), label="z")
ax.set_ylabel("position of orbiting body [$R_*$]")
ax.legend(fontsize=10, loc=1)

ax = axes
ax.plot(t, vx.eval(), label="$v_x$")
ax.plot(t, vy.eval(), label="$v_y$")
ax.plot(t, vz.eval(), label="$v_z$")
ax.set_xlim(t.min(), t.max())
ax.set_xlabel("time [days]")
ax.set_ylabel("velocity of central [$R_*$/day]")
_ = ax.legend(fontsize=10, loc=1) The key feture of exoplanet is that all of the parameters to a KeplerianOrbit can be PyMC3 variables. This means that these elements are now something that you can infer. For example, if we want to fit for the orbital period, we can define a PyMC3 model like the following:

import pymc3 as pm

with pm.Model():
log_period = pm.Normal("log_period", mu=np.log(10), sigma=2.0)
orbit = xo.orbits.KeplerianOrbit(
period=pm.math.exp(log_period),  # ...
)

# Define the rest of you model using orbit...


In the following sections, we will go through some of the specific ways that you might use this orbit to define a model, but first some orbital conventions!

## Orbital conventions#

These orbits are specified with respect to a single central body (generally the most massive body) and then a system of non-interacting bodies orbiting the central. This is a good parameterization for exoplanetary systems and binary stars, but it is sometimes not sufficient for systems with multiple massive bodies where the interactions will be important to the dynamics.

We follow a set of internally consistent orbital conventions that also attempts to respect as many of the established conventions of the stellar and exoplanetary fields as possible. The location of a body on a Keplerian orbit with respect to the center of mass is given in the perifocal plane by

$\begin{split} \boldsymbol{r} = \left [ \begin{array}{c} x\\ y\\ z\\ \end{array} \right ]. \end{split}$

By construction, the orbit of the body in the perifocal frame is constrained entirely to the $$x -y$$ plane as shown by this figure: The range of orbital convention choices mainly stems from how the location of the body in the perifocal frame is converted to the observer frame,

$\begin{split} \boldsymbol{R} = \left [ \begin{array}{c} X\\ Y\\ Z\\ \end{array} \right ]. \end{split}$

We choose to orient the $$\hat{\boldsymbol{X}}$$ unit vector in the north direction, $$\hat{\boldsymbol{Y}}$$ in the east direction, and $$\hat{\boldsymbol{Z}}$$ towards the observer. Under this convention, the inclination of an orbit is defined by the dot-product of the orbital angular momentum vector with $$\hat{\boldsymbol{Z}}$$, conforming to the common astrometric convention that a zero inclination ($$i = 0$$) orbit is face-on, with the test body orbiting in a counter-clockwise manner around the primary body.

In the stellar and exoplanetary fields, there is less agreement on which segment of the line of nodes is defined as the ascending node. We choose to define the ascending node as the point where the orbiting body crosses the plane of the sky moving away from the observer (i.e., crossing from $$Z > 0$$ to $$Z< 0$$). This convention has historically been the choice of the visual binary field; the opposite convention occasionally appears in exoplanet and planetary studies.

To implement the transformation from perifocal frame to observer frame, we consider the rotation matrices

$\begin{split} \boldsymbol{P}_x(\phi) = \left [ \begin{array}{ccc} 1 & 0 & 0 \\ 0 & \cos \phi & - \sin \phi \\ 0 & \sin \phi & \cos \phi \\ \end{array}\right] \end{split}$
$\begin{split} \boldsymbol{P}_z (\phi) = \left [ \begin{array}{ccc} \cos \phi & - \sin \phi & 0\\ \sin \phi & \cos \phi & 0 \\ 0 & 0 & 1 \\ \end{array}\right]. \end{split}$

which result in a clockwise rotation of the axes, as defined using the right hand rule.

This means when we look down the $$z$$-axis, for a positive angle $$\phi$$, it would be as if the $$x$$ and $$y$$ axes rotated clockwise. In order to find out what defines counter-clockwise when considering the other rotations, we look to the right hand rule and cross products of the axes unit vectors. Since $$\hat{\boldsymbol{x}} \times \hat{\boldsymbol{y}} = \hat{\boldsymbol{z}}$$, when looking down the $$z$$ axis the direction of the $$x$$-axis towards the $$y$$-axis defines counter clockwise. Similarly, we have $$\hat{\boldsymbol{y}} \times \hat{\boldsymbol{z}} = \hat{\boldsymbol{x}}$$, and $$\hat{\boldsymbol{z}} \times \hat{\boldsymbol{x}} = \hat{\boldsymbol{y}}$$.

To convert a position $$\boldsymbol{r}$$ in the perifocal frame to the observer frame $$\boldsymbol{R}$$ (referencing the figure above), three rotations are required: (i) a rotation about the $$z$$-axis through an angle $$\omega$$ so that the $$x$$-axis coincides with the line of nodes at the ascending node, (ii) a rotation about the $$x$$-axis through an angle ($$-i$$) so that the two planes are coincident, and finally (iii) a rotation about the $$z$$-axis through an angle $$\Omega$$. Applying these rotation matrices yields

$\boldsymbol{R} = \boldsymbol{P}_z (\Omega) \boldsymbol{P}_x(-i) \boldsymbol{P}_z(\omega) \boldsymbol{r}.$

As a function of true anomaly $$f$$, the position in the observer frame is given by

$\begin{split} \begin{array}{lc} X =& r [ \cos \Omega (\cos \omega \cos f - \sin \omega \sin f) - \sin \Omega \cos i (\sin \omega \cos f + \cos \omega \sin f) ] \\ Y =& r [ \sin \Omega (\cos \omega \cos f - \sin \omega \sin f) + \cos \Omega \cos i(\sin \omega \cos f + \cos \omega \sin f) ] \\ Z =& - r \sin i (\sin \omega \cos f + \cos \omega \sin f).\\ \end{array} \end{split}$

Simplifying the equations using the sum angle identities, we find

$\begin{split} \begin{array}{lc} X =& r (\cos \Omega \cos(\omega + f) - \sin(\Omega) \sin(\omega + f) \cos(i)) \\ Y =& r (\sin \Omega \cos(\omega + f) + \cos(\Omega) \sin(\omega + f) \cos(i)) \\ Z =& - r \sin(\omega + f) \sin(i).\\ \end{array} \end{split}$

The central body is defined by any two of radius $$R_\star$$, mass $$M_\star$$, or density $$\rho_\star$$. The third physical parameter can always be computed using the other two so exoplanet will throw an error if three are given (even if they are numerically self-consistent).

The orbits are then typically defined by (check out the docstring exoplanet.orbits.KeplerianOrbit for all the options):

• the period $$P$$ or semi-major axis $$a$$ of the orbit,

• the time of conjunction $$t_0$$ or time of periastron passage $$t_\mathrm{p}$$,

• the planet’s radius $$R_\mathrm{pl}$$ and mass $$M_\mathrm{pl}$$,

• the eccentricity $$e$$, argument of periastron $$\omega$$ and the longitude of ascending node $$\Omega$$, and

• the inclination of the orbital plane $$i$$ or the impact parameter $$b$$.

A KeplerianOrbit object in exoplanet will always be fully specified. For example, if the orbit is defined using the period $$P$$, the semi-major axis $$a$$ will be computed using Kepler’s third law.

A exoplanet.orbits.KeplerianOrbit can be used to compute the expected radial velocity time series for a given set of parameters. One typical parameterization for a radial velocity fit would look something like this:

import arviz as az
import pymc3_ext as pmx
import aesara_theano_fallback.tensor as tt

# Create a dummy dataset
random = np.random.default_rng(1234)
t_plot = np.linspace(0, 50, 500)
t = np.sort(random.uniform(0, 50, 20))
rv_err = 0.5 + np.zeros_like(t)
rv_obs = 5.0 * np.sin(2 * np.pi * t / 10.0) + np.sqrt(
0.05**2 + rv_err**2
) * random.normal(size=len(t))

with pm.Model():
# Period, semi-amplitude, and eccentricity
log_period = pm.Normal("log_period", mu=np.log(10.0), sigma=1.0)
period = pm.Deterministic("period", tt.exp(log_period))
log_semiamp = pm.Normal("log_semiamp", mu=np.log(5.0), sd=2.0)
semiamp = pm.Deterministic("semiamp", tt.exp(log_semiamp))
ecc = pm.Uniform("ecc", lower=0, upper=1)

# At low eccentricity, omega and the phase of periastron (phi) are
# correlated so it can be best to fit in (omega ± phi) / 2
plus = pmx.Angle("plus")
minus = pmx.Angle("minus")
phi = pm.Deterministic("phi", plus + minus)
omega = pm.Deterministic("omega", plus - minus)

# For non-zero eccentricity, it can sometimes be better to use
# sqrt(e)*sin(omega) and sqrt(e)*cos(omega) as your parameters:
#
#     ecs = pmx.UnitDisk("ecs", testval=0.01 * np.ones(2))
#     ecc = pm.Deterministic("ecc", tt.sum(ecs ** 2, axis=0))
#     omega = pm.Deterministic("omega", tt.arctan2(ecs, ecs))
#     phi = pmx.Angle("phi")

# Jitter & the system mean velocity offset
log_jitter = pm.Normal("log_jitter", mu=np.log(0.05), sd=5.0)
zero_point = pm.Normal("zero_point", mu=0, sd=10.0)

# Then we define the orbit
tperi = pm.Deterministic("tperi", period * phi / (2 * np.pi))
orbit = xo.orbits.KeplerianOrbit(
period=period, t_periastron=tperi, ecc=ecc, omega=omega
)

# And define the RV model
rv_model = zero_point + orbit.get_radial_velocity(t, K=semiamp)

# Finally add in the observation model
err = tt.sqrt(rv_err**2 + tt.exp(2 * log_jitter))
pm.Normal("obs", mu=rv_model, sigma=rv_err, observed=rv_obs)

# We'll also track the model just for plotting purposes
pm.Deterministic(
)

soln = pmx.optimize(vars=[plus, minus, ecc])
soln = pmx.optimize(soln)
trace = pmx.sample(
tune=1000,
draws=1000,
cores=2,
chains=2,
start=soln,
return_inferencedata=True,
)

# Plot the results
rv_plot = trace.posterior["rv_plot"].values
q16, q50, q84 = np.percentile(rv_plot, [16, 50, 84], axis=(0, 1))
plt.errorbar(t, rv_obs, yerr=rv_err, fmt="+k", label="data")
plt.plot(t_plot, q50)
plt.fill_between(t_plot, q16, q84, alpha=0.3, label="posterior")
plt.xlim(0, 50)
plt.legend(fontsize=12, loc=2)
plt.xlabel("time [days]")

az.summary(trace, var_names=["^(?!rv_plot).*"], filter_vars="regex")

optimizing logp for variables: [ecc, minus, plus]

100.00% [60/60 00:00<00:00 logp = 1.658e+01]


message: Optimization terminated successfully.
logp: -961.5116559541921 -> 16.57595521451944

optimizing logp for variables: [zero_point, log_jitter, minus, plus, ecc, log_semiamp, log_period]

100.00% [33/33 00:00<00:00 logp = 1.726e+01]
message: Optimization terminated successfully.
logp: 16.57595521451944 -> 17.255963009144537



Multiprocess sampling (2 chains in 2 jobs)

NUTS: [zero_point, log_jitter, minus, plus, ecc, log_semiamp, log_period]

100.00% [4000/4000 00:21<00:00 Sampling 2 chains, 0 divergences]
Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 21 seconds.

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
log_period 2.302 0.004 2.294 2.309 0.000 0.000 1821.0 1560.0 1.0
log_semiamp 1.626 0.031 1.567 1.683 0.001 0.001 2061.0 1426.0 1.0
log_jitter -2.916 4.907 -12.677 5.887 0.095 0.095 2649.0 1399.0 1.0
zero_point -0.125 0.116 -0.352 0.084 0.002 0.002 2531.0 1441.0 1.0
period 9.991 0.040 9.916 10.065 0.001 0.001 1821.0 1560.0 1.0
semiamp 5.087 0.159 4.795 5.383 0.004 0.003 2061.0 1426.0 1.0
ecc 0.050 0.030 0.000 0.100 0.001 0.001 799.0 495.0 1.0
plus -0.934 1.120 -2.989 0.814 0.043 0.030 783.0 473.0 1.0
minus 0.808 0.035 0.738 0.871 0.001 0.001 1938.0 1568.0 1.0
phi -0.126 1.125 -2.188 1.620 0.043 0.034 781.0 473.0 1.0
omega -1.742 1.117 -3.783 0.014 0.042 0.030 784.0 478.0 1.0
tperi -0.202 1.789 -3.464 2.586 0.068 0.055 782.0 473.0 1.0 Instead of using semiamp as a parameter, we could have passed m_planet and incl as parameters to KeplerianOrbit and fit the physical orbit. Similarly, if we we doing a joint fit with a transit light curve (see Combining datasets below) we could have parameterized in terms of t0 (the time of a reference transit) instead of the phase of periastron.

As mentioned in the comments above and discussed in Reparameterization, the performance of these fits can be problem specific and depend on the choice of parameters. Therefore it is often worthwhile experimenting with different parameterizations, but this can be a good place to start for radial velocity-only fits.

## Astrometry#

Astrometric observations usually consist of measurements of the separation and position angle of the secondary star (or directly imaged exoplanet), relative to the primary star as a function of time, but exoplanet could also be used to model the motion of the center of light for and unresolved orbit. The typical exoplanet.orbits.KeplerianOrbit definition for and astrometric dataset will be similar to a radial velocity fit:

random = np.random.default_rng(5678)
t_plot = np.linspace(0, 22 * 365.25, 500)
t = np.sort(random.uniform(0.0, 22 * 365.25, 45))
rho_err = random.uniform(0.05, 0.1, len(t))
theta_err = random.uniform(0.05, 0.1, len(t))

with pm.Model():
# Period, semi-major axis, eccentricity, and t0
log_period = pm.Normal("log_period", mu=np.log(25.0 * 365.25), sigma=1.0)
period = pm.Deterministic("period", tt.exp(log_period))
log_a = pm.Normal("log_a", mu=np.log(0.3), sd=2.0)
a = pm.Deterministic("a", tt.exp(log_a))
ecc = pm.Uniform("ecc", lower=0, upper=1)
tperi = pm.Normal("tperi", mu=3500.0, sigma=1000.0)

# For astrometric orbits, a good choice of parameterization can be
# (Omega ± omega) / 2
plus = pmx.Angle("plus")
minus = pmx.Angle("minus")
Omega = pm.Deterministic("Omega", plus + minus)
omega = pm.Deterministic("omega", plus - minus)

# We'll use a uniform prior on cos(incl)
cos_incl = pm.Uniform("cos_incl", lower=-1.0, upper=1.0, testval=0.3)
incl = pm.Deterministic("incl", tt.arccos(cos_incl))

# Then we define the orbit
orbit = xo.orbits.KeplerianOrbit(
a=a,
t_periastron=tperi,
period=period,
incl=incl,
ecc=ecc,
omega=omega,
Omega=Omega,
)

# And define the RV model
rho_model, theta_model = orbit.get_relative_angles(t)

# ================================================== #
# Simulate data from the model for testing           #
# You should remove the following lines in your code #
# ================================================== #
rho_obs, theta_obs = pmx.eval_in_model([rho_model, theta_model])
rho_obs += rho_err * random.normal(size=len(t))
theta_obs += theta_err * random.normal(size=len(t))
# =============== end simulated data =============== #

# Define the observation model this is simple for rho:
pm.Normal("rho_obs", mu=rho_model, sd=rho_err, observed=rho_obs)

# But we want to be cognizant of the fact that theta wraps so the following
# is equivalent to
#
#   pm.Normal("obs_theta", mu=theta_model, observed=theta_obs, sd=theta_err)
#
# but takes into account the wrapping
theta_diff = tt.arctan2(
tt.sin(theta_model - theta_obs), tt.cos(theta_model - theta_obs)
)
pm.Normal("theta_obs", mu=theta_diff, sd=theta_err, observed=0.0)

# We'll also track the model just for plotting purposes
rho_plot, theta_plot = orbit.get_relative_angles(t_plot)
pm.Deterministic("rho_plot", rho_plot)
pm.Deterministic("theta_plot", theta_plot)

trace = pmx.sample(
tune=1000,
draws=1000,
cores=2,
chains=2,
target_accept=0.95,
return_inferencedata=True,
)

# Plot the results
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 8), sharex=True)

q16, q50, q84 = np.percentile(
trace.posterior["rho_plot"].values, [16, 50, 84], axis=(0, 1)
)
ax1.errorbar(t, rho_obs, yerr=rho_err, fmt="+k", label="data")
ax1.plot(t_plot, q50)
ax1.fill_between(t_plot, q16, q84, alpha=0.3, label="posterior")
ax1.set_ylabel(r"$\rho$ [arcsec]")

q16, q50, q84 = np.percentile(
trace.posterior["theta_plot"].values, [16, 50, 84], axis=(0, 1)
)
ax2.errorbar(t, theta_obs, yerr=rho_err, fmt="+k", label="data")
ax2.plot(t_plot, q50)
ax2.fill_between(t_plot, q16, q84, alpha=0.3, label="posterior")
ax2.set_xlim(t_plot.min(), t_plot.max())
ax2.legend(fontsize=12, loc=2)
ax2.set_ylabel(r"$\theta$ [radians]")
ax2.set_xlabel("time [days]")

# Compute the convergence stats
az.summary(trace, var_names=["^(?!.*_plot).*"], filter_vars="regex")

Multiprocess sampling (2 chains in 2 jobs)

NUTS: [cos_incl, minus, plus, tperi, ecc, log_a, log_period]

100.00% [4000/4000 00:22<00:00 Sampling 2 chains, 0 divergences]
Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 23 seconds.

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
log_period 9.232 0.116 9.042 9.466 0.005 0.003 673.0 679.0 1.0
log_a -1.131 0.071 -1.264 -0.997 0.003 0.002 752.0 750.0 1.0
tperi 3548.315 35.427 3483.178 3613.973 0.985 0.697 1266.0 1474.0 1.0
period 10286.162 1246.596 8260.791 12684.548 51.818 37.402 673.0 679.0 1.0
a 0.324 0.023 0.283 0.369 0.001 0.001 752.0 750.0 1.0
ecc 0.530 0.038 0.464 0.606 0.002 0.001 654.0 704.0 1.0
plus 0.020 0.016 -0.012 0.048 0.000 0.000 1358.0 1278.0 1.0
minus -0.021 0.029 -0.069 0.040 0.001 0.001 1491.0 1503.0 1.0
Omega -0.001 0.020 -0.038 0.038 0.000 0.000 1659.0 1407.0 1.0
omega 0.041 0.042 -0.042 0.115 0.001 0.001 1373.0 1483.0 1.0
cos_incl 0.296 0.018 0.263 0.332 0.001 0.000 908.0 1032.0 1.0
incl 1.270 0.019 1.233 1.305 0.001 0.000 908.0 1032.0 1.0 In this example, the units of a are arcseconds, but the exoplanet.orbits.KeplerianOrbit.get_relative_angles() function accepts a parallax argument if you have a constraint on the parallax of the system. If you provide parallax as an argument, a should be provided in the usual units of Solar radii.

## Transits, occultations, and eclipses#

exoplanet has built in support for evaluating quadratically limb darkened light curve models using the algorithm from Agol et al. (2020). If you need flexible surface models or higher order limb darkening, check out the starry package which also integrates with PyMC3.

Transit and occultation modeling is one of the primary applications of exoplanet so there are quite a few options (including transit timing variations, detached eclipsing binary modeling, and much more) that are highlighted on the Case Studies page. But, a bread-and-butter transit model implemented in exoplanet might look something like the following:

random = np.random.default_rng(123)
num_transits = 4
t = np.arange(0, 35, 0.02)
yerr = 5e-4

with pm.Model():
# The baseline flux
mean = pm.Normal("mean", mu=0.0, sigma=1.0)

# Often the best parameterization is actually in terms of the
# times of two reference transits rather than t0 and period
t0 = pm.Normal("t0", mu=4.35, sigma=1.0)
t1 = pm.Normal("t1", mu=33.2, sigma=1.0)
period = pm.Deterministic("period", (t1 - t0) / num_transits)

# The Kipping (2013) parameterization for quadratic limb darkening
# paramters

# The radius ratio and impact parameter; these parameters can
# introduce pretty serious covariances and are ripe for
# reparameterization
log_r = pm.Normal("log_r", mu=np.log(0.04), sigma=2.0)
r = pm.Deterministic("r", tt.exp(log_r))
b = xo.distributions.ImpactParameter("b", ror=r, testval=0.35)

# Set up a Keplerian orbit for the planets
orbit = xo.orbits.KeplerianOrbit(period=period, t0=t0, b=b)

# Compute the model light curve; note that we index using [:, 0]
# since get_light_curve returns an object with the shape
# (n_times, n_planets)
light_curve = (
xo.LimbDarkLightCurve(u, u).get_light_curve(
orbit=orbit, r=r, t=t
)[:, 0]
+ mean
)

# Here we track the value of the model light curve for plotting
# purposes
pm.Deterministic("light_curve", light_curve)

# ================================================== #
# Simulate data from the model for testing           #
# You should remove the following lines in your code #
# ================================================== #
y = pmx.eval_in_model(light_curve)
y += yerr * random.normal(size=len(y))
# =============== end simulated data =============== #

# The likelihood function assuming known Gaussian uncertainty
pm.Normal("obs", mu=light_curve, sd=yerr, observed=y)

trace = pmx.sample(
tune=1000,
draws=1000,
cores=2,
chains=2,
return_inferencedata=True,
)

# Plot the results
q16, q50, q84 = np.percentile(
trace.posterior["light_curve"].values, [16, 50, 84], axis=(0, 1)
)
plt.plot(t, y, ".k", ms=2, label="data")
plt.plot(t, q50)
plt.fill_between(t, q16, q84, alpha=0.3, label="posterior")
plt.xlim(0.0, 35)
plt.legend(fontsize=12, loc=3)
plt.xlabel("time [days]")
plt.ylabel("relative flux")

# Compute the convergence stats
az.summary(trace, var_names=["^(?!light_curve).*"], filter_vars="regex")

Multiprocess sampling (2 chains in 2 jobs)

NUTS: [b, log_r, u, t1, t0, mean]

100.00% [4000/4000 00:46<00:00 Sampling 2 chains, 0 divergences]
Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 50 seconds.

The number of effective samples is smaller than 25% for some parameters.

mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
mean 0.000 0.000 -0.000 0.000 0.000 0.000 1539.0 1316.0 1.00
t0 4.350 0.003 4.344 4.357 0.000 0.000 1297.0 768.0 1.00
t1 33.200 0.003 33.194 33.206 0.000 0.000 1135.0 1392.0 1.00
log_r -3.227 0.044 -3.313 -3.153 0.002 0.001 646.0 1053.0 1.00
period 7.212 0.001 7.210 7.215 0.000 0.000 1352.0 1268.0 1.00
u 0.532 0.340 0.002 1.114 0.012 0.008 687.0 565.0 1.00
u 0.120 0.351 -0.544 0.733 0.010 0.008 1285.0 919.0 1.00
r 0.040 0.002 0.036 0.043 0.000 0.000 646.0 1053.0 1.00
b 0.333 0.121 0.081 0.505 0.006 0.004 464.0 387.0 1.01 ## Combining datasets#

Since exoplanet is built on top of PyMC3, it has the capacity to support essentially arbitrariliy complicated models. This means that you can share parameters or fit multiple datasets however you want. We won’t go into too many details about this here, but you can see some examples on the Case Studies of joint transit/radial velocity fits, or inferences based on datasets from multiple instruments.

The basic idea behind how to simultaneously fit multiple datasets is that you’ll need to define the relationship between the datasets (perhaps share parameters or a shared KeplerianOrbit) and then you’ll include the likelihood for each dataset in your model. In all of the above examples, we had a line like

pm.Normal("obs", mu=rv_model, sigma=rv_err, observed=rv_obs)


in all of our models. This defines a Gaussian likelihood conditioned on the observed data. To combine datasets, you can simply add multiple lines like this (one for each dataset) and, behind the scenes, PyMC3 will multiply these likelihoods (or actually add their logarithms) as it should.

For more concrete examples, check out the Case Studies and (if that’s not sufficient) feel free to start a “discussion” on the GitHub repository asking for help.