This just sets up the plotting to be "inline" rather than opening separate windows for each plot, and boosts the resolution (I have a "retina" display on my mac, so you need pretty high resolution to get good-looking plots).
In [1]:
%pylab inline
%config InlineBackend.figure_format = 'retina'
You will need the rebound and seaborn packages to run (but not view) this notebook. You can create a fresh environment called trappist
and install them on a machine with Anaconda in the following way:
conda create -n trappist
pip install rebound
pip install seaborn
Then issuing the command source activate trappist
(mac or linux) or activate trappist
(windows) will activate this environment. From here you can run python code that uses these modules, or you can start up jupyter notebooks (like this one!):
source activate trappist
jupyter notebook TRAPPIST1.ipynb
Note that rebound will not install on Windows; if you do not have access to a mac/linux machine (Y2 labs have linux machines, for example, as do some of the other computing labs across campus) you will only be able to look at this code, not run it. Don't worry---there are many ways to get 10 marks on this assignment, so you will still be able to complete the work.
If you need a tutorial on Jupyter notebooks, you can find one here: http://jupyter.org
These are a few of my favourite seaborn
plotting settings:
In [2]:
import rebound as reb
import seaborn as sns
sns.set_context("notebook")
sns.set_style("ticks")
sns.set_palette("colorblind")
A couple of utility functions for dealing with angles that wrap:
In [24]:
def zero_to_2pi(x):
x = x % 2*pi
if x < 0:
return x + 2*pi
else:
return x
def mpi_to_pi(x):
x = x%2*pi
if x > pi:
return x - 2*pi
elif x < -pi:
return x + 2*pi
else:
return x
Set up the TRAPPIST-1 parameters. See the paper on Canvas or here. epoch
is a time (in days) that sits in the middle of the sequence of transits reported there.
In [4]:
mtrapa = 0.0802 # MSun
mearth = 0.000003003 # MSun
days = 1.0/365.25 # yr
deg = pi/180.0 # radians
epoch = 7400.0 # Days.
A word about rebound
: it stores the planetary system it is currently simulating in a "simulation" object. Between integrations, you can add and remove planets from the sim, change their properties, etc. At the beginning of the integration, you set up your sim with all the planets you want to include in it. You can see examples of this in both the C and Python code at the rebound git repository. There are even iPython notebooks with examples, too! There is a nice feature in the interface as well, which is called a "simulation archive." It stores the states of a simulation at various points through the integration and allows for replay.
Here we write some code to set up the TRAPPIST system. We take the convention that the $x$-axis points toward the Earth, so when planets cross the $x$ axis in the $x$-$y$ plane, they transit. We don't know the exact orientation of the orbits in the system (i.e. the angles $\omega$, $\Omega$ or $i$), so we choose these randomly, and then work out the orbital phase that is necessary to ensure that the object transits at the given time for the orbit we have chosen.
Because we don't know the exact masess, periods, etc, (well, periods are actually really precisely determined), we draw them randomly within the reported uncertainties. Therefore, every time we run the code below we are drawing a Monte-Carlo sample of a possible TRAPPIST1 system that is consistent with the observations.
In [5]:
def add_trappista(sim):
sim.add(m=mtrapa)
def add_trappistp(sim, P0, dP0, T0, dT0, M0, dM0):
e = abs(0.04*randn())
i = 0.5*deg*randn()
omega = 2*pi*rand()
Omega = 2*pi*rand()
P = abs(P0 + dP0*randn())
T = abs(T0 + dT0*randn())
M = abs(M0 + dM0*randn())
pomega = omega + Omega
M_at_trans = - pomega
dM = (T-epoch)/P*2*pi
M_at_epoch = M_at_trans - dM
a = cbrt(sim.G*mtrapa*P*P*days*days/(4.0*pi*pi))
sim.add(m=M*mearth, a=a, M=M_at_epoch, omega=omega, Omega=Omega, e=e, inc=i)
A note on units: we choose $G = 4\pi^2$, which means we can measure masses in $M_\odot$, time in years, and lengths in AU.
The bits at the beginning where we are modifying parameters of the simulation are just set up to optimize the performance of the simulation. It will use a type of integrator called "Wisdom-Holman" (the same Wisdom as explained the Kirkwood gaps we talked about earlier in class). You can read about W-H integrators here; essentially, they exploit the fact that the planets move along Keplerian orbits about the host star with only small perturbations to make the integration much more efficient. Rebound also uses a "corrector" to improve the accuracy of the simulations; those with interest in Hamiltonian dynamics should read about correctors here.
Rebound has the nice feature that it can exit (stop the simulation) if any body gets sufficiently far from the central star; here we set a boundary at $100 \, \mathrm{AU}$ to detect if any bodies have been ejected from the simulatino.
In [7]:
def draw_sim():
sim = reb.Simulation()
sim.G = 4.0*pi*pi
sim.exit_max_distance = 100 # Raise an exception if any body goes beyond 100 AU.
sim.integrator = "whfast" # Hermes uses the Wisdom-Holman scheme with close encounters integrated by IAS15
sim.ri_whfast.safe_mode = 0 # This and the next setting are much faster, but only OK if you don't access the particle array between steps
sim.ri_whfast.corrector = 11 # Same
sim.dt = 0.05*pi/3.0*1.51087081*days # Close to 5% of inner orbit period, but not commensurate
add_trappista(sim)
add_trappistp(sim, 1.51087081, 0.6e-6, 7322.51736, 0.00010, 0.85, 0.72)
add_trappistp(sim, 2.4218233, 0.17e-5, 7282.80728, 0.00019, 1.38, 0.61)
add_trappistp(sim, 4.049610, 0.63e-4, 7670.14165, 0.00035, 0.41, 0.27)
add_trappistp(sim, 6.099615, 0.11e-4, 7660.37859, 0.00038, 0.62, 0.58)
add_trappistp(sim, 9.206690, 0.15e-4, 7671.39767, 0.00023, 0.68, 0.18)
add_trappistp(sim, 12.35294, 0.12e-3, 7665.34937, 0.00021, 0.94, 0.63)
add_trappistp(sim, 20.0, 6.0, 7662.55463, 0.00056, 0.755**3, 3.0*0.755**2*0.034)
sim.move_to_com()
return sim
sim = draw_sim()
E0 = sim.calculate_energy()
# Make a pretty plot.
reb.OrbitPlot(sim, color=True, slices=True)
sim.status()
For those without access to rebound
or who just prefer C or C++, here is a suite of initial conditions (1k) in all. The format is
You can use these initial conditions to fulfill parts of the project using your own code if you like.
Note that you can access all the particles in a simulation via sim.particles
; each particle has its own orbital elements (p.e
, p.omega
, p.pomega
, p.Omega
, p.a
, p.l
(mean longitude), p.i
, p.P
(period), etc) in addition to the six-dimensional $\vec{x}$ and $\vec{v}$ that we use below
In [9]:
with open("ics.dat", "w") as out:
for i in range(1000):
s = draw_sim()
ps = s.particles
mass_line = " ".join(["{:g}".format(p.m) for p in ps])
out.write(mass_line + "\n")
for p in ps:
xv_line = " ".join(["{:g}".format(x) for x in [p.x, p.y, p.z, p.vx, p.vy, p.vz]])
out.write(xv_line + "\n")
Let's do the above 5-mark part with rebound. First, we set up a simulation, and initialise an archive stored on disk in "1000yr.bin". We will have the archive store its state every year (or so).
In [10]:
sim = draw_sim()
sim.initSimulationArchive("1000yr.bin", interval=1)
Now we integrate the system to 1000 years. The state of the system will be stored 1 year or so; we don't force the integrator to stop evenly on 1-year boundaries, but rather let it keep its step-size and just record the state at the closest step to 1-year boundaries. (Note that we don't have to make the simulation archive if we don't want to save the intermediate states of the system---we can just call sim.integrate
without setting up the archive.)
In [11]:
sim.integrate(1000, exact_finish_time=0)
Now we can load the archive and make some plots:
In [13]:
sa = reb.SimulationArchive("1000yr.bin")
print("Maximum time achieved = {:g}".format(sa[-1].t))
Let's check the energy error in the simulation (note that energy should be exactly conserved, so any difference between the initial energy and subsequent energies is down to integration error).
In [14]:
E0 = sa[0].calculate_energy()
es = array([s.calculate_energy() for s in sa])
ts = array([s.t for s in sa])
plot(ts, abs((es-E0)/E0))
yscale("log")
print("Energy error = {:g}".format(abs(1.0-sa[-1].calculate_energy()/E0)))
Let's plot the state of the system at the beginning and end of our simulation:
In [16]:
reb.OrbitPlot(sa[0], color=True, slices=True) # Beginning
reb.OrbitPlot(sa[-1], color=True, slices=True) # End
Out[16]:
Now let's plot the eccentricity of each planet as a function of time over our simulation.
In [22]:
ts = []
es = []
for s in sa:
ts.append(s.t)
es.append([p.e for p in s.particles[1:]])
ts = array(ts)
es = array(es)
plot(ts, es)
legend(["{:d}".format(i) for i in range(1,8)], loc='best')
xlabel(r"$t$ ($\mathrm{yr}$)")
ylabel(r"$e$")
Out[22]:
It looks like, in this particular simulation, there are a number of quasi-periodic oscillations in the eccentricity of some of the planets. This is a standard feature of planets in disks with small eccentricity; essentially we are seeing the normal modes of the system. If you want to know more, look up "Laplace-Lagrange theory," or see Murray & Dermott, Chapter 7.
What about resonances? The discovery paper states that the inner two planets are near a 5:8 commensurability; is there a corresponding resonant angle: $$ \theta_\mathrm{res} = 5 \lambda_1 - 8 \lambda_2 + 3 \begin{cases}\varpi_1 & \mathrm{inner} \\ \varpi_2 & \mathrm{outer}\end{cases} $$ that oscillates?
In [25]:
ts = []
theta_inner = []
theta_outer = []
for s in sa:
ts.append(s.t)
ps = s.particles
theta_inner.append(mpi_to_pi(5*ps[1].l - 8*ps[2].l + 3*ps[1].pomega))
theta_outer.append(mpi_to_pi(5*ps[1].l - 8*ps[2].l + 3*ps[2].pomega))
ts = array(ts)
theta_inner = array(theta_inner)
theta_outer = array(theta_outer)
In [27]:
plot(ts, theta_inner, label="inner")
plot(ts, theta_outer, label="outer")
legend(loc="best")
Out[27]:
Apparently not.
From here, there are a number of ways to proceed in the assignment; pick whichever combination of tasks you would prefer. I will mark everything you do, but the maximum score is 10 marks; only do significantly more than 10 marks worth of work if you are excited to explore these topics (good for you!).
sim.integrate((t_transit - epoch)*days)
or using your own code). Verify that the appropriate planet is close to the $x$-axis at the transit time.
In [ ]: