libstempo tutorial: basic functionality

Michele Vallisneri, vallis@vallis.org; latest revision: 2016/10/12 for v2.3 revision


In [1]:
%matplotlib inline
%config InlineBackend.figure_format = 'retina'

In [2]:
from __future__ import print_function
import sys, math, numpy as N, matplotlib.pyplot as P

Load the libstempo Python extension. It requires a source installation of tempo2, as well as current Python and compiler, and the numpy and Cython packages.

(Both Python 2.7 and 3.4 are supported; this means that in Python 2.7 all returned strings will be unicode strings, while in Python 3 all function arguments should be default unicode strings rather than bytes. This should work transparently, although there are limitations to what characters can be passed to tempo2; you should probably restrain yourself to ASCII.


In [3]:
from libstempo.libstempo import *

In [4]:
import libstempo

In [5]:
libstempo.__path__


Out[5]:
['/Users/vallis/anaconda3/lib/python3.5/site-packages/libstempo-2.3.0-py3.5-macosx-10.6-x86_64.egg/libstempo']

In [6]:
import libstempo as T

T.data = T.__path__[0] + '/data/' # example files

In [7]:
print("Python version   :",sys.version.split()[0])
print("libstempo version:",T.__version__)
print("Tempo2 version   :",T.libstempo.tempo2version())


Python version   : 3.5.2
libstempo version: 2.2.5
Tempo2 version   : 2015.9

We load a single-pulsar object. Doing this will automatically run the tempo2 fit routine once.


In [8]:
psr = T.tempopulsar(parfile = T.data + '/J1909-3744_NANOGrav_dfg+12.par',
                    timfile = T.data + '/J1909-3744_NANOGrav_dfg+12.tim')

Let's start simple: what is the name of this pulsar? (You can change it, by the way.)


In [9]:
psr.name


Out[9]:
'1909-3744'

Next, let's look at observations: there are psr.nobs of them; we can get numpy arrays of the site TOAs [in MJDs] with psr.stoas, of the TOA measurement errors [in microseconds] with psr.toaerrs, and of the measurement frequencies with psr.freqs. These arrays are views of the tempo2 data, so you can write to them (but you cannot currently change the number of observations).


In [10]:
psr.nobs


Out[10]:
1001

In [11]:
psr.stoas


Out[11]:
array([ 53292.017,  53292.048,  53355.834, ...,  54641.173,  54706.993,
        54764.835], dtype=float128)

In [12]:
psr.toaerrs


Out[12]:
array([ 1.231,  4.668,  0.453, ...,  0.158,  1.336,  0.316])

In [13]:
psr.freqs


Out[13]:
array([ 1372.,  1372.,  1372., ...,   884.,   884.,   884.])

By contrast, barycentric TOAs and frequencies are computed on the basis of current pulsar parameters, so you get them by calling psr methods (with parentheses), and you get a copy of the current values. Writing to it has no effect on the tempo2 data.


In [14]:
psr.toas()


Out[14]:
array([ 53292.017,  53292.048,  53355.829, ...,  54641.18,  54706.997,
        54764.834], dtype=float128)

In [15]:
psr.ssbfreqs()


Out[15]:
array([  1.37213133e+09,   1.37213155e+09,   1.37204346e+09, ...,
         8.83982927e+08,   8.84066115e+08,   8.84080037e+08])

Residuals (in seconds) are returned by residuals(). The method takes a few options... I'll let its docstring help describe them. libstempo is fully documented in this way (try help(T.tempopulsar)).


In [16]:
help(psr.residuals)


Help on built-in function residuals:

residuals(...) method of libstempo.libstempo.tempopulsar instance
    tempopulsar.residuals(updatebats=True,formresiduals=True,removemean=True)
    
    Returns residuals as a numpy.longdouble array (a copy of current values).
    Will update TOAs/recompute residuals if `updatebats`/`formresiduals` is True
    (default for both). Will remove residual mean if `removemean` is True;
    first residual if `removemean` is 'first'; weighted residual mean
    if `removemean` is 'weighted'.


In [17]:
psr.residuals()


Out[17]:
array([-1.1914324e-07,  7.171673e-06,  1.0663671e-06, ...,  4.0556862e-07,
        1.4199422e-06, -4.9899445e-07], dtype=float128)

We can plot TOAs vs. residuals, but we should first sort the arrays; otherwise the array follow the order in the tim file, which may not be chronological.


In [18]:
# get sorted array of indices
i = N.argsort(psr.toas())
# use numpy fancy indexing to order residuals 
P.errorbar(psr.toas()[i],psr.residuals()[i],yerr=1e-6*psr.toaerrs[i],fmt='.',alpha=0.2);


We can also see what flags have been set on the observations, and what their values are. The latter returns a numpy vector of strings. Flags are not currently writable.


In [19]:
psr.flags()


Out[19]:
['fe', 'be', 'B', 'bw', 'tobs', 'pta', 'proc', 'chanid']

In [20]:
psr.flagvals('chanid')


Out[20]:
array(['gasp_1372', 'gasp_1372', 'gasp_1372', ..., 'gasp_884', 'gasp_884',
       'gasp_884'], 
      dtype='<U32')

In fact, there's a commodity routine in libstempo.plot to plot residuals, taking flags into account.


In [21]:
import libstempo.plot as LP

LP.plotres(psr,group='pta',alpha=0.2)


Timing-model parameters can be accessed by using psr as a Python dictionary. Each parameter is a special object with properties val, err (as well as fit, which is true is the parameter is currently being fitted, and set, which is true if the parameter was assigned a value).


In [22]:
psr['RAJ'].val, psr['RAJ'].err, psr['RAJ'].fit, psr['RAJ'].set


Out[22]:
(5.0169080674060326785, 7.7537595250585651792e-10, True, True)

The names of all fitted parameters, of all set parameters, and of all parameters are returned by psr.pars(which='fit'). We show only the first few.


In [23]:
fitpars = psr.pars() # defaults to fitted parameters
setpars = psr.pars(which='set')
allpars = psr.pars(which='all')

print(len(fitpars),len(setpars),len(allpars))
print(fitpars[:10])


82 157 1942
('RAJ', 'DECJ', 'F0', 'F1', 'PMRA', 'PMDEC', 'PX', 'SINI', 'PB', 'A1')

The number of fitting parameters is psr.ndim.


In [24]:
psr.ndim


Out[24]:
82

Changing the parameter values results in different residuals.


In [25]:
# look +/- 3 sigmas around the current value
x0, dx = psr['RAJ'].val, psr['RAJ'].err
xs = x0 + dx * N.linspace(-3,3,20)         

res = []
for x in xs:
    psr['RAJ'].val = x
    res.append(psr.rms()/1e-6)
psr['RAJ'].val = x0                       # restore the original value

P.plot(xs,res)


Out[25]:
[<matplotlib.lines.Line2D at 0x116b70898>]

We can also call a least-squares fitting routine, which will fit around the current parameter values, replacing them with their new best values. Individual parameters can be included or excluded in the fitting by setting their 'fit' field. (Note: as of version 2.3.0, libstempo provides its own fit, although it does call tempo2 to compute the design matrix.)


In [26]:
psr['DM'].fit


Out[26]:
False

In [27]:
psr['DM'].fit = True
print(psr['DM'].val)


10.39468

In [35]:
ret = psr.fit()

In [37]:
print(psr['DM'].val,psr['DM'].err)


17.004097499 2.4378940518

The fit returns a tuple consisting of best-fit vector, standard errors, covariance matrix, and linearized chisq. Note that these vectors and matrix are (ndim+1)- or (ndim+1)x(ndim+1)-dimensional, with the first row/column corresponding to a constant phase offset referenced to the first TOA (even if that point is not used).

The exact chisq can be recomputed by psr.chisq() (which evaluates N.sum(psr.residuals()**2 / (1e-12 * psr.toaerrs**2))).

The pulsar parameters can be read in bulk by calling psr.vals(which='fit'), which will default to fitted parameters, but can also be given 'all', 'set', or even a list of parameter names.


In [38]:
fitvals = psr.vals()
print(fitvals)


[ 5.0169081 -0.65864032  339.31569 -1.6148063e-15  17.004097 -9.6100038
 -35.65098  0.04600521  0.99843948  1.5334495  1.8979911  53113.951
 -6.5486068e-08 -2.1823683e-07  0.21374686 -0.00085471319 -0.003104991
 -0.0033092244 -0.0032289727 -0.002926524 -0.0010466857 -0.00079533981
 -0.00089972667 -0.0016601594 -0.0022611087 -0.0030596338 -0.0037880483
 -0.0012174287 -0.0017552682 -0.0035535757 -0.0037106356 -0.0036500237
 -0.0029082132 -0.001567544 -0.0015035292 -0.001784357 -0.0028467196
 -0.0033948048 -0.0039266233 -0.00428004 -0.0042036353 -0.0039126822
 -0.0033871291 -0.002090091 -0.0016336403 -0.0016992682 -8.4464611e-05
 -0.00016832383 -0.00025154713 -0.0003339948 -0.0004156387 -0.00049661981
 -0.00057695589 -0.00065646412 -0.00073537223 -0.00081364592 -0.00089128815
 -0.00096821988 -0.0010444583 -0.0011201486 -0.0011952089  0.029150287
  0.028280306  0.027855034  0.027436003  0.027023229  0.026215668
  0.025820843  0.025431592  0.025047884  0.024669643  0.024296888
  0.023929413  0.02356709  0.023209721  0.022857112  0.022510152
  0.022167709  0.021829954  0.021496654  0.021168063  0.020843863
  0.020524308]

In [39]:
psr.vals(which=['RAJ','DECJ','PMRA'])


Out[39]:
array([ 5.0169081, -0.65864032, -9.6100038], dtype=float128)

To set parameter values in bulk, you give a first argument to vals. Or call it with a dictionary.


In [40]:
psr.vals([5.1,-0.6],which=['RAJ','DECJ','PMRA'])
psr.vals({'PMRA': -9.5})

print(psr.vals(which=['RAJ','DECJ','PMRA']))

# restore original values
psr.vals(fitvals)


[ 5.1 -0.6 -9.5]

Be careful about loss of precision; tempopar.val is a numpy longdouble, so you should be careful about assigning it a regular Python double. By contrast, doing arithmetics with numpy longdoubles will preserve their nature and precision.

You can access errors in a similar way with psr.errs(...).

It's also possible to obtain the design matrix computed at the current parameter values, which has shape psr.nobs * (len(psr.pars) + 1), since a constant offset is always included among the fitting parameters.


In [41]:
d = psr.designmatrix()

These, for instance, are the derivatives with respect to RAJ and DECJ, evaluated at the TOAs.


In [42]:
# we need the sorted-index array compute above
P.plot(psr.toas()[i]/365.25,d[i,1],'-x'); P.hold(True)
P.plot(psr.toas()[i]/365.25,d[i,2],'-x')


Out[42]:
[<matplotlib.lines.Line2D at 0x10d9a6eb8>]

It's easy to save the current timing-model to a new par file. Omitting the argument will overwrite the original parfile.


In [43]:
psr.savepar('./foo.par')

In [44]:
!head foo.par


PSRJ           1909-3744
RAJ             19:09:47.4380382         1  0.00000763113145998940   
DECJ           -37:44:14.31884           1  0.00032618650418925702   
F0             339.31569275868008617     1  0.00000000000272685369   
F1             -1.61480632477416527e-15  1  2.3032800450468292559e-20
PEPOCH         53000                       
POSEPOCH       53000                       
DMEPOCH        53000                       
DM             17.004097498953143486     1  2.43789405180097107362   
PMRA           -9.610003825228750074     1  0.02202743585156533360   

Same for writing tim files.


In [45]:
psr.savetim('./foo.tim')

In [46]:
!head foo.tim


FORMAT 1
MODE 1
 53292.000004.1.000.000.tsum 1372.00000000 53292.01653552588140172 1.23100 gbt -fe Rcvr1_2 -be GASP -B L -bw 4.0 -tobs 901.322 -pta NANOGrav -proc dfg+12 -chanid gasp_1372 
 53292.000010.1.000.000.tsum 1372.00000000 53292.04810962983469835 4.66800 gbt -fe Rcvr1_2 -be GASP -B L -bw 4.0 -tobs 901.322 -pta NANOGrav -proc dfg+12 -chanid gasp_1372 
 53355.000005.1.000.000.tsum 1372.00000000 53355.83359727578050169 0.45300 gbt -fe Rcvr1_2 -be GASP -B L -bw 4.0 -tobs 1081.587 -pta NANOGrav -proc dfg+12 -chanid gasp_1372 
 53800.000018.1.000.000.tsum 1372.00000000 53800.48353665754979858 0.14800 gbt -fe Rcvr1_2 -be GASP -B L -bw 4.0 -tobs 3424.969 -pta NANOGrav -proc dfg+12 -chanid gasp_1372 
 53838.000020.1.000.000.tsum 1372.00000000 53838.37506136744340068 0.54300 gbt -fe Rcvr1_2 -be GASP -B L -bw 4.0 -tobs 3064.446 -pta NANOGrav -proc dfg+12 -chanid gasp_1372 
 53858.000028.1.000.000.tsum 1372.00000000 53858.31918408581089963 0.08900 gbt -fe Rcvr1_2 -be GASP -B L -bw 4.0 -tobs 1982.877 -pta NANOGrav -proc dfg+12 -chanid gasp_1372 
 53889.000025.1.000.000.tsum 1372.00000000 53889.23934070044100153 0.07300 gbt -fe Rcvr1_2 -be GASP -B L -bw 4.0 -tobs 2703.923 -pta NANOGrav -proc dfg+12 -chanid gasp_1372 
 53979.000023.1.000.000.tsum 1372.00000000 53979.98924810684179931 0.16500 gbt -fe Rcvr1_2 -be GASP -B L -bw 4.0 -tobs 2703.923 -pta NANOGrav -proc dfg+12 -chanid gasp_1372 

With libstempo, it's easy to replicate some of the "toasim" plugin functionality. By subtracting the residuals from the site TOAs (psr.stoas, vs. the barycentered psr.toas) and refitting, we can create a "perfect" timing solution. (Note that 1 ns is roughly tempo2's claimed accuracy.)


In [47]:
print(math.sqrt(N.mean(psr.residuals()**2)) / 1e-6)


2.1934488744024665

In [48]:
psr.stoas[:] -= psr.residuals() / 86400.0
ret = psr.fit(iters = 4)

In [49]:
print(math.sqrt(N.mean(psr.residuals()**2)) / 1e-6)


0.006614118417934996

Then we can add, e.g., homoskedastic white measurement noise at 100 ns (remember the tempo units: days for TOAs, us for errors, s for residuals).


In [50]:
psr.stoas[:] += 0.1e-6 * N.random.randn(psr.nobs) / 86400.0
psr.toaerrs[:] = 0.1
ret = psr.fit()

In [51]:
i = N.argsort(psr.toas())
P.errorbar(psr.toas()[i],psr.residuals()[i],yerr=1e-6*psr.toaerrs[i],fmt='.')


Out[51]:
<Container object of 3 artists>