In [2]:
% matplotlib inline
%config InlineBackend.figure_format = 'retina'
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
from enterprise.pulsar import Pulsar
import enterprise.signals.parameter as parameter
from enterprise.signals import utils
from enterprise.signals import signal_base
from enterprise.signals import selections
from enterprise.signals.selections import Selection
#from tests.enterprise_test_data import datadir
import tests
In [3]:
tests
Out[3]:
In [2]:
def A(farg1, farg2):
class A(object):
def __init__(self, iarg):
self.iarg = iarg
def print_info(self):
print('Object instance {}\nInstance argument: {}\nFunction args: {} {}\n'.format(
self, self.iarg, farg1, farg2))
return A
In [3]:
# define class A with arguments that can be seen within the class
a = A('arg1', 'arg2')
# instantiate 2 instances of class A with different arguments
a1 = a('iarg1')
a2 = a('iarg2')
# call print_info method
a1.print_info()
a2.print_info()
In the example above we see that the arguments arg1 and arg2 are seen by both instances a1 and a2; however these instances were intantiated with different input arguments iarg1 and iarg2. So we see that class-factories are great when we want to give "global" parameters to a class without having to pass them on initialization. This also allows us to mix and match classes, as we will do in enterprise before we instantiate them.
Pulsar classThe Pulsar class is a simple data structure that stores all of the important information about a pulsar that is obtained from a timing package such as the TOAs, residuals, error-bars, flags, design matrix, etc.
This class is instantiated with a par and a tim file. Full documentation on this class can be found here.
In [4]:
psr = Pulsar(datadir+'/B1855+09_NANOGrav_9yv1.gls.par', datadir+'/B1855+09_NANOGrav_9yv1.tim')
This Pulsar object is then passed to other enterprise data structures in a loosley coupled way in order to interact with the pulsar data.
In [5]:
# lets define an efac parameter with a uniform prior from [0.5, 5]
efac = parameter.Uniform(0.5, 5)
print(efac)
Uniform is a class factory that returns a class. The parameter is then intialized via a name. This way, a single parameter class can be initialized for multiple signal parameters with different names (i.e. EFAC per observing backend, etc). Once the parameter is initialized then you then have access to many useful methods.
In [6]:
# initialize efac parameter with name "efac_1"
efac1 = efac('efac_1')
print(efac1)
# return parameter name
print(efac1.name)
# get pdf at a point (log pdf is access)
print(efac1.get_pdf(1.3), efac1.get_logpdf(1.3))
# return 5 samples from this prior distribution
print(efac1.sample(size=5))
Function structureIn enterprise we have defined a special data structure called Function. This data structure provides the user with a way to use and combine several different enterprise components in a user friendly way. More explicitly, it converts and standard function into an enterprise Function which can extract information from the Pulsar object and can also interact with enterprise Parameters.
[put reference to docstring here]
For example, consider the function:
In [7]:
@signal_base.function
def sine_wave(toas, log10_A=-7, log10_f=-8):
return 10**log10_A * np.sin(2*np.pi*toas*10**log10_f)
Notice that the first positional argument of the function is toas, which happens to be a name of an attribute in the Pulsar class and the keyword arguments specify the default parameters for this function.
The decorator converts this standard function to a Function which can be used in two ways: the first way is to treat it like any other function.
In [8]:
# treat it just as a standard function with a vector input
sw = sine_wave(np.array([1,2,3]), log10_A=-8, log10_f=-7.5)
print(sw)
the second way is to use it as a Function:
In [9]:
# or use it as an enterprise function
sw_function = sine_wave(log10_A=parameter.Uniform(-10,-5), log10_f=parameter.Uniform(-9, -7))
print(sw_function)
Here we see that Function is actually a class factory, that is, when initialized with enterprise Parameters it returns a class that is initialized with a name and a Pulsar object as follows:
In [10]:
sw2 = sw_function('sine_wave', psr=psr)
print(sw2)
Now this Function object carries around instances of the Parameter classes given above for this particular function and Pulsar
In [11]:
print(sw2.params)
Most importantly it can be called in three different ways: If given without parameters it will fall back on the defaults given in the original function definition
In [12]:
print(sw2())
or we can give it new fixed parameters
In [13]:
print(sw2(log10_A=-8, log10_f=-6.5))
or most importantly we can give it a parameter dictionary with the Parameter names as keys. This is how Functions are use internally inside enterprise.
In [14]:
params = {'sine_wave_log10_A':-8, 'sine_wave_log10_f':-6.5}
print(sw2(params=params))
Notice that the last two methods give the same answer since we gave it the same values just in different ways. So you may be thinking: "Why did we pass the Pulsar object on initialization?" or "Wait. How does it know about the toas?!". Well the first question answers the second. By passing the pulsar object it grabs the toas attribute internally. This feature, combined with the ability to recognize Parameters and the ability to call the original function as we always would are the main strengths of Function, which is used heavily in enterprise.
Note that if we define a function without the decorator then we can still obtain a Function via:
In [15]:
def sine_wave(toas, log10_A=-7, log10_f=-8):
return 10**log10_A * np.sin(2*np.pi*toas*10**log10_f)
sw3 = signal_base.Function(sine_wave, log10_A=parameter.Uniform(-10,-5),
log10_f=parameter.Uniform(-9, -7))
print(sw3)
FunctionTo define your own Function all you have to do is to define a function with these rules in mind.
Pulsar attributes, define them as positional arguments with the same name as used in the Pulsar class (see here for more information.Parameters must be keyword arguments (although you can have others that aren't Parameters)@function decorator.And thats it! You can now define your own Functions with minimal overhead and use them in enterprise or for tests and simulations or whatever you want!
Selection structureIn the course of our analysis it is useful to split different signals into pieces. The most common flavor of this is to split the white noise parameters (i.e., EFAC, EQUAD, and ECORR) by observing backend system. The Selection structure is here to make this as smooth and versatile as possible.
The Selection structure is also a class-factory that returns a specific selection dictionary with keys and Boolean arrays as values.
This will become more clear with an example. Lets say that you want to split our parameters between the first and second half of the dataset, then we can define the following function:
In [16]:
def cut_half(toas):
midpoint = (toas.max() + toas.min()) / 2
return dict(zip(['t1', 't2'], [toas <= midpoint, toas > midpoint]))
This function will return a dictionary with keys (i.e. the names of the different subsections) t1 and t2 and boolean arrays corresponding to the first and second halves of the data span, respectively. So for a simple input we have:
In [17]:
toas = np.array([1,2,3,4])
print(cut_half(toas))
To pass this to enterprise we turn it into a Selection via:
In [18]:
ch = Selection(cut_half)
print(ch)
As we have stated, this is class factory that will be initialized inside enterprise signals with a Pulsar object in a very similar way to Functions.
In [19]:
ch1 = ch(psr)
print(ch1)
print(ch1.masks)
The Selection object has a method masks that uses the Pulsar object to evaluate the arguments of cut_half (these can be any number of Pulsar attributes, not just toas). The Selection object can also be called to return initialized Parameters with the split names as follows:
In [20]:
# make efac class factory
efac = parameter.Uniform(0.1, 5.0)
# now give it to selection
params, masks = ch1('efac', efac)
# named parameters
print(params)
# named masks
print(masks)
SelectionTo define your own Selection all you have to do is to define a function with these rules in mind.
Pulsar attributes, define them as positional arguments with the same name as used in the Pulsar class (see here for more information.And thats it! You can now define your own Selections with minimal overhead and use them in enterprise or for tests and simulations or whatever you want!
Signal components which have their own Parameters.Signal components is a SignalCollection.SignalCollection that are combined to form a PTA.Signals are shared across pulsars Likelihoods act on PTAs. enterprise Signalclass Signal(object):
"""Base class for Signal objects."""
def get_ndiag(self, params):
"""Returns the diagonal of the white noise vector `N`.
This method also supports block diagaonal sparse matrices.
"""
return None
def get_delay(self, params):
"""Returns the waveform of a deterministic signal."""
return 0
def get_basis(self, params=None):
"""Returns the basis array of shape N_toa x N_basis."""
return None
def get_phi(self, params):
"""Returns a diagonal or full rank covaraince matrix
of the basis amplitudes."""
return None
def get_phiinv(self, params):
"""Returns inverse of the covaraince of basis amplitudes."""
return None
In [ ]: