tesla_lightcurve

Analyzing the Reflected Light off Tesla Roadster in Space

In Feb. 6, 2018, SpaceX launched Falcon Heavy with a Tesla Roadster with a dummy driver called Starman towards Mars's direction.

credit: SpaceX

In Feb. 10, Erik Dennihy and JJ Hermes used the 4.1-meter SOAR telescope in CTIO, Chile to monitor its brightness for about 1 hour. JJ Hermes posted a tweet showing the light curve (brightness of object as function of time) of Starman. A video of the actual footage is here.

Hanno Rein et al. also posted a hilarious at first but insightful paper in ArXiv entitled The random walk of cars and their collision probabilities with planets. In this study, they ran N-body simulations using a large ensemble of simulations with slightly perturbed initial conditions, and estimated the probability of a collision of the Tesla roadster with Earth and Venus over the next one million years to be 6% and 2.5%, respectively. Cool right?

But anyway, in this post, we aim to confirm whether the Tesla Roadster (a.k.a. Starman, 2018-017A) is indeed rotating with a period of 4.7589 +/- 0.0060 minutes.

First, let's download data from here directly using pandas. The first column is time in seconds, and the second column in relative flux (or brightness of object per unit area).

In [1]:
import pandas as pd

link = 'http://k2wd.org/share/roadster.dat'
data = pd.read_csv(link, delimiter=' ', comment='#')
data.columns = ['Time','Flux']
  • inspect data
In [2]:
data.head()
Out[2]:
Time Flux
0 15.881 0.07878
1 31.706 0.18612
2 47.564 0.10497
3 63.405 0.07927
4 79.261 0.05045

From the data above, we can see that each measurement was taken in every 15 seconds.

In [3]:
%matplotlib inline

#styling
%config InlineBackend.figure_format = "retina"
from matplotlib import rcParams

rcParams["savefig.dpi"] = 100
rcParams["font.size"] = 20
In [4]:
import matplotlib.pyplot as pl

t=data.Time
f=data.Flux

fig,ax = pl.subplots(1,1,figsize=(15,3))
ax.plot(t,f,'o-')
ax.set(xlabel='Time [sec]',
       ylabel='Relative Flux');

Finding period

In [5]:
from gatspy.periodic import LombScargleFast

model = LombScargleFast().fit(t, f)
periods, power = model.periodogram_auto(nyquist_factor=100)

fig, ax = pl.subplots()
ax.plot(periods, power)
ax.set(xlim=(0, 500), ylim=(0, 1),
       xlabel='period (second)',
       ylabel='Lomb-Scargle Power',
       title='Periodogram');

The highest peak of the periodogram corresponds to optimum period.

In [6]:
import numpy as np

idx=np.argmax(power)

period in seconds

In [7]:
p=periods[idx]
p
Out[7]:
285.58900000000006

period in minutes

In [8]:
p/60
Out[8]:
4.7598166666666675
In [11]:
#built-in optimizer
model.optimizer.period_range=(250, 300)
period = model.best_period
print("period = {0}".format(period/60))
period = 4.760856841752395

Phase-folding

Let's fold the light curve and see how much the brightness varies during each period

In [10]:
phase = (t / period) % 1

# Compute best-fit template

phase_fit = np.linspace(0, 1, 1000)
f_fit = model.predict(period * phase_fit, period=period)

# Plot the phased data & model
fig, ax = pl.subplots()
ax.plot(phase, f, '.k', alpha=0.5, label='data')
ax.plot(phase_fit, f_fit, '-r', lw=3, label='model')
ax.set(xlabel='Phase', 
       ylabel='flux',
      title='Phase-folded light curve');
ax.legend()
Out[10]:
<matplotlib.legend.Legend at 0x7f598f83df10>

scipy's optimizer

In [15]:
def simple_sine(theta, t):
    """assumes light curve is sinusoidal"""
    a, b, c = theta
    return a * np.sin(b * t + c)

def lnlike(params, t, f):    
    """model's ln likelihood"""
    m = simple_sine(params[:3], t)
    sig = params[3]
    resid = f - m
    inv_sigma2 = 1.0/(sig**2)

    return -0.5*(np.sum((resid)**2*inv_sigma2 - np.log(inv_sigma2)))


def lnprob(theta, t, f):
    """ln probability = ln like+ln prior"""
    #prior
    if np.any(theta < 0):
        return -np.inf
    
    #loglike
    ll = lnlike(theta, t, f)
    return ll if np.isfinite(ll) else -np.inf

nlp = lambda *args: -lnprob(*args)
In [13]:
import scipy.optimize as op

init_guess = (0.1,6,0.1,0.1) #amplitude, phase, lag, uncertainty in flux
args = (phase, f)

opt = op.minimize(nlp, init_guess, args=args, method='nelder-mead')
print(opt.success)
True
In [14]:
for i in opt.x:
    print ("parameter optimum: {:.4f}".format(i))
parameter optimum: 0.0874
parameter optimum: 5.7158
parameter optimum: 1.0097
parameter optimum: 0.0291

Plot optimized fit

In [16]:
# Plot the phased data & model
fig, ax = pl.subplots()

ax.plot(phase, f, '.k', alpha=0.5, label='data')
ax.plot(phase, simple_sine(opt.x[:3], phase), 'ro', lw=3, label='model')
ax.set(xlabel='Phase', 
       ylabel='flux',
      title='Phase-folded light curve');
ax.legend()
Out[16]:
<matplotlib.legend.Legend at 0x7f598f811710>
In [18]:
h = lambda x,a,b,c: a*np.sin(b*x+c) 

p_opt, p_cov = op.curve_fit(h, phase, f, p0=init_guess[:3])
var = np.diag(p_cov)
std = np.sqrt(np.diag(p_cov)) #a.k.a. sigma

for i,j in zip(p_opt, std):
    print ("parameter optimum: {:.4f} +/- {:.4f}".format(i, j))
parameter optimum: 0.0874 +/- 0.0030
parameter optimum: 5.7158 +/- 0.1210
parameter optimum: 1.0097 +/- 0.0806
In [19]:
pl.matshow(p_cov)
Out[19]:
<matplotlib.image.AxesImage at 0x7f598f6d5fd0>
In [20]:
# Plot the phased data & model
fig, ax = pl.subplots()

ax.plot(phase, f, '.k', alpha=0.5, label='data')
ax.plot(phase, h(phase, *p_opt), 'ro', lw=3, label='model')

for i in range(100):
    theta = p_opt + np.random.randn(p_opt.size) * std
    pl.plot(phase, h(phase, *theta),'ro', lw=3, alpha=0.1)
    
ax.set(xlabel='Phase', 
       ylabel='flux',
      title='Phase-folded light curve');
#ax.legend()

MCMC

Use markov chain monte carlo with ensemble sampler to explore parameter space and quantify uncertainty.

In [21]:
from emcee import EnsembleSampler
from emcee.utils import sample_ball
from tqdm import tqdm

# initial = opt.x

ndim = len(init_guess)
nwalkers = 8 * ndim
nsteps1 = 500
nsteps2 = 3000

sampler = EnsembleSampler(nwalkers, ndim, lnprob, args=args, threads=1)
pos0 = sample_ball(init_guess, [1e-1]*ndim, nwalkers)

width = 30
print("\nstage 1")
for pos,_,_ in tqdm(sampler.sample(pos0, iterations=nsteps1)):
    pass
0it [00:00, ?it/s]/home/jp/miniconda3/envs/py3/lib/python3.7/site-packages/emcee/moves/red_blue.py:99: RuntimeWarning: invalid value encountered in double_scalars
  lnpdiff = f + nlp - state.log_prob[j]
4it [00:00, 36.21it/s]
stage 1
500it [00:14, 34.02it/s]
In [22]:
print("\nstage 2")
idx = np.argmax(sampler.lnprobability)
best = sampler.flatchain[idx]
pos0 = sample_ball(best, [1e-5]*ndim, nwalkers)
sampler.reset()

for pos,_,_ in tqdm(sampler.sample(pos0, iterations=nsteps2)):
    pass
2it [00:00, 14.81it/s]
stage 2
3000it [01:32, 32.56it/s]
In [23]:
chain = sampler.chain
labels = ['${}$'.format(i) for i in r'A,\theta,\phi,\sigma'.split(',')]
In [24]:
fig, axs = pl.subplots(ndim, 1, figsize=(15,ndim), sharex=True)
[axs.flat[i].plot(c, drawstyle='steps', color='k', alpha=4./nwalkers) for i,c in enumerate(chain.T)]
[ax.set_ylabel(labels[i], fontsize=20) for i,ax in enumerate(axs)]
fig.tight_layout()
In [25]:
chain.shape
Out[25]:
(32, 3000, 4)
In [26]:
#flatten the chain
fc = np.reshape(chain,(-1,ndim))

Measure autocorrelation needed to ensure independent sampling.

In [27]:
from acor import acor

taus = []
for i in range(ndim):
    j = fc[:,i]
    tau,_,_ =acor(j)
    tau = int(round(tau))
    taus.append(tau)
    print('tau={}\t ndata={}'.format(tau,len(j[::tau])))
tau=47	 ndata=2043
tau=55	 ndata=1746
tau=45	 ndata=2134
tau=38	 ndata=2527

joint and marginal distributions

In [28]:
from corner import corner

hist_kwargs = dict(lw=2, alpha=0.5)
title_kwargs = dict(fontdict=dict(fontsize=12))

fig = pl.figure(figsize=(8,8))

corner(fc, labels=labels, #truths=truths,
       hist_kwargs=hist_kwargs,
       title_kwargs=title_kwargs,
       show_titles=True, 
       quantiles=[0.16,0.5,0.84],
       title_fmt='.4f');
#     fig.tight_layout()
<Figure size 576x576 with 0 Axes>
In [29]:
df=pd.DataFrame(fc)
df.columns = ['A','theta','phi','sigma']
In [30]:
df.describe()
Out[30]:
A theta phi sigma
count 96000.000000 96000.000000 96000.000000 96000.000000
mean 0.087307 5.716060 1.008236 0.029550
std 0.002961 0.109834 0.069403 0.001527
min 0.074923 5.270794 0.703959 0.024256
25% 0.085313 5.644638 0.963196 0.028471
50% 0.087269 5.713094 1.007949 0.029464
75% 0.089271 5.788691 1.054815 0.030548
max 0.099071 6.141251 1.298558 0.036770
In [31]:
def get_stats(sample,tol=4):
    '''
    return 50th, 16th, and 84th percentiles
    (median & 1 sigma limits) of a given sample
    '''
    l,m,r = np.percentile(sample,[16,50,84])

    return m, m-l, r-m
In [33]:
vals, uncs = [], []

for i in range(ndim):
    j=fc[:,i][taus[i]]
    d=get_stats(j)
    vals.append(d[0])
    uncs.append((d[1],d[2]))
    print('{:.6f} +{:.6f} -{:.6f}'.format((d[0]),d[1],d[2]))
0.084231 +0.000000 -0.000000
5.776425 +0.000000 -0.000000
0.980014 +0.000000 -0.000000
0.029066 +0.000000 -0.000000
In [35]:
# Plot the phased data & model
fig, ax = pl.subplots()

ax.plot(phase, f, '.k', alpha=0.5, label='data')
ax.plot(phase, h(phase, *vals[:3]), 'ro', lw=3, label='model')
    
ax.set(xlabel='Phase', 
       ylabel='flux',
      title='Phase-folded light curve');
ax.legend()
Out[35]:
<matplotlib.legend.Legend at 0x7f598c9e1610>