Simulated BBH example

This example recovers the parameters of a simulated binary black-hole (BBH) that has similar parameters has GW150914.

1. Create the injection

First, we need to create an injection.hdf file that specifies the parameters of the simulated signal. To do that we will use pycbc_create_injection. Like pycbc_inference, pycbc_create_injections uses a configuration file to set the parameters of the injections it will create. To create a binary-black hole with parameters similar to GW150914, use the following configuration file:


tc = 1126259462.420
mass1 = 37
mass2 = 32
ra = 2.2
dec = -1.25
inclination = 2.5
coa_phase = 1.5
polarization = 1.75
distance = 100
f_ref = 20
f_lower = 18
approximant = IMRPhenomPv2
taper = start


Note the similarity to the configuration file for pycbc_inference: you must have a [variable_params] section. If we wanted to randomize one or more of the parameters, we would list them there, then add [prior] sections to specify what distribution to draw the parameters from. In this case, however, we want to fix the parameters, so we just put all of the necessary parameters in the [static_params] section.

To create the injection file, run:

pycbc_create_injections --verbose \
        --config-files injection.ini \
        --ninjections 1 \
        --seed 10 \
        --output-file injection.hdf \
        --variable-params-section variable_params \
        --static-params-section static_params \
        --dist-section prior \


This will create the injection.hdf file, which we will give to pycbc_inference. For more information on generating injection files, run pycbc_create_injections --help.

2. Setup the configuration files

Now we need to set up the configuration for pycbc_inference. Since we will be analyzing data, we will need to provide several additional options in a [data] section. To keep the configuration files easy to read, we will split the data, sampler, and prior settings into their own configuration files.

Here are the model and prior settings we will use:

name = gaussian_noise
low-frequency-cutoff = 20.0

; waveform parameters that will vary in MCMC
delta_tc =
mass1 =
mass2 =
spin1_a =
spin1_azimuthal =
spin1_polar =
spin2_a =
spin2_azimuthal =
spin2_polar =
distance =
coa_phase =
inclination =
polarization =
ra =
dec =

; waveform parameters that will not change in MCMC
approximant = IMRPhenomPv2
f_lower = 20
f_ref = 20
; we'll set the tc by using the trigger time in the data
; section of the config file + delta_tc
trigger_time = ${data|trigger-time}

; coalescence time prior
name = uniform
min-delta_tc = -0.1
max-delta_tc = 0.1

; we need to provide tc to the waveform generator
name = custom
inputs = delta_tc
tc = ${data|trigger-time} + delta_tc

name = uniform
min-mass1 = 10.
max-mass1 = 80.

name = uniform
min-mass2 = 10.
max-mass2 = 80.

name = uniform
min-spin1_a = 0.0
max-spin1_a = 0.99

name = uniform_solidangle
polar-angle = spin1_polar
azimuthal-angle = spin1_azimuthal

name = uniform
min-spin2_a = 0.0
max-spin2_a = 0.99

name = uniform_solidangle
polar-angle = spin2_polar
azimuthal-angle = spin2_azimuthal

; The waveform generator expects spins to be in cartesian coordinates, with
; names spin(1|2)(x|y|z). We therefore need to provide a waveform transform
; that converts the spherical coordinates that we have defined the spin prior
; in to cartesian coordinates.
name = spherical_to_cartesian
x = spin1x
y = spin1y
z = spin1z
radial = spin1_a
polar = spin1_polar
azimuthal = spin1_azimuthal

name = spherical_to_cartesian
x = spin2x
y = spin2y
z = spin2z
radial = spin2_a
polar = spin2_polar
azimuthal = spin2_azimuthal

; following gives a uniform volume prior
name = uniform_radius
min-distance = 10
max-distance = 1000

; coalescence phase prior
name = uniform_angle

; inclination prior
name = sin_angle

; sky position prior
name = uniform_sky

; polarization prior
name = uniform_angle


In the [model] section we have set the model to be gaussian_noise. As described above, this is the standard model to use for CBC signals. It assumes that the noise is wide-sense stationary Gaussian noise. Notice that we provide a low frequency argument which is the lower bound for the likelihood integral. (See the GaussianNoise docs for details.)

The GaussianNoise model will need to generate model waveforms in order to evaluate the likelihood. This means that we need to provide it with a waveform approximant to use. Which model to use is set by the approximant argument in the [static_params] section. Here, we are using IMRPhenomPv2. This is a frequency-domain, precessing model that uses the dominant, \(\ell=|m|=2\) mode. For this reason, we are varying all three components of each object’s spin, along with the masses, location, orientation, and phase of the signal. We need to provide a lower frequency cutoff (f_lower), which is the starting frequency of the waveform. This must be \(\leq\) the smallest low frequency cutoff set in the model section.


In this example we have to sample over a reference phase for the waveform (coa_phase). This is because we are using the GaussianNoise model. For dominant-mode only waveforms, it is possible to analytically marginalize over the phase using the MarginalizedPhaseGaussianNoise model. This can speed up the convergence of the sampler by a factor of 3 or faster. To use the marginalized phase model, change the model name to marginalized_phase, and remove coa_phase from the variable_params and the prior. However, the marginalized phase model should not be used with fully precessing models or models that include higher modes. You can use it with IMRPhenomPv2 due to some simplifications that the approximant makes.

We also need a prior for the coaslesence time tc. We have done this by setting a reference time in the static_params section, and varying a +/-0.1s window around it with the delta_tc parameter. Notice that the trigger time is not set to a value; instead, we reference the trigger-time option that is set in the [data] section. This way, we only need to set the trigger time in one place; we can reuse this prior file for different BBH events by simply providing a different data configuration file.

Here is the data configuration file we will use:

instruments = H1 L1
trigger-time = 1126259462.42
analysis-start-time = -6
analysis-end-time = 2
; strain settings
sample-rate = 2048
fake-strain = H1:aLIGOaLIGODesignSensitivityT1800044 L1:aLIGOaLIGODesignSensitivityT1800044
fake-strain-seed = H1:44 L1:45
; psd settings
psd-estimation = median-mean
psd-inverse-length = 8
psd-segment-length = 8
psd-segment-stride = 4
psd-start-time = -256
psd-end-time = 256
; even though we're making fake strain, the strain
; module requires a channel to be provided, so we'll
; just make one up
channel-name = H1:STRAIN L1:STRAIN
; Providing an injection file will cause a simulated
; signal to be added to the data
injection-file = injection.hdf
; We'll use a high-pass filter so as not to get numerical errors from the large
; amplitude low frequency noise. Here we use 15 Hz, which is safely below the
; low frequency cutoff of our likelihood integral (20 Hz)
strain-high-pass = 15
; The pad-data argument is for the high-pass filter: 8s are added to the
; beginning/end of the analysis/psd times when the data is loaded. After the
; high pass filter is applied, the additional time is discarded. This pad is
; *in addition to* the time added to the analysis start/end time for the PSD
; inverse length. Since it is discarded before the data is transformed for the
; likelihood integral, it has little affect on the run time.
pad-data = 8


In this case, we are generating fake Gaussian noise (via the fake-strain option) that is colored by the Advanced LIGO updated design sensitivity curve. (Note that this is ~3 times more sensitive than what the LIGO detectors were when GW150914 was detected.) The duration of data that will be analyzed is set by the analysis-(start|end)-time arguments. These values are measured with respect to the trigger-time. The analyzed data should be long enough such that it encompasses the longest waveform admitted by our prior, plus our timing uncertainty (which is determined by the prior on delta_tc). Waveform duration is approximately determined by the total mass of a system. The lowest total mass (= mass1 + mass2) admitted by our prior is 20 solar masses. This corresponds to a duration of ~6 seconds, so we start the analysis time 6 seconds before the trigger time. (See the pycbc.waveform module for utilities to estimate waveform duration.) Since the trigger time is approximately where we expect the merger to happen, we only need a small amount of time afterward to account for the timing uncertainty and ringdown. Here, we choose 2 seconds, which is a good safety margin.

We also have to provide arguments for estimating a PSD. Although we know the exact shape of the PSD in this case, we will still estimate it from the generated data, as this most closely resembles what you do with a real event. To do this, we have set psd-estimation to median-mean and we have set psd-segment-length, psd-segment-stride, and psd-(start|end)-time (which are with respect to the trigger time). This means that a Welch-like method will be used to estimate the PSD. Specifically, we will use 512s of data centered on the trigger time to estimate the PSD. This time will be divided up into 8s-long segments (the segment length) each overlapping by 4s (the segment stride). The data in each segment will be transformed to the frequency domain. Two median values will be determined in each frequency bin from across all even/odd segments, then averaged to obtain the PSD.

The beginning and end of the analysis segment will be corrupted by the convolution of the inverse PSD with the data. To limit the amount of time that is corrupted, we set psd-inverse-length to 8. This limits the corruption to at most the first and last four seconds of the data segment. To account for the corruption, psd-inverse-length/2 seconds are subtracted/added by the code from/to the analysis start/end times specified by the user before the data are transformed to the frequency domain. Consequently, our data will have a frequency resolution of \(\Delta f = 1/16\,\) Hz. The 4s at the beginning/end of the segment are effectively ignored since the waveform is contained entirely in the -6/+2s we set with the analysis start/end time.

Finally, we will use the emcee_pt sampler with the following settings:

name = emcee_pt
nwalkers = 200
ntemps = 20
effective-nsamples = 1000
checkpoint-interval = 2000
max-samples-per-chain = 1000

burn-in-test = nacl & max_posterior

;   Sampling transforms
; parameters on the left will be sampled in
; parametes on the right
mass1, mass2 : mchirp, q

; inputs mass1, mass2
; outputs mchirp, q
name = mass1_mass2_to_mchirp_q


Here, we will use 200 walkers and 20 temperatures. We will checkpoint (i.e., dump results to file) every 2000 iterations. Since we have provided an effective-nsamples argument and a [sampler-burn_in] section, pycbc_inference will run until it has acquired 1000 independent samples after burn-in, which is determined by a combination of the nacl and max_posterior tests; i.e., the sampler will be considered converged when both of these tests are satisfied.

The number of independent samples is checked at each checkpoint: after dumping the results, the burn-in test is applied and an autocorrelation length is calculated. The number of independent samples is then nwalkers x (the number of iterations since burn in)/ACL. If this number exceeds effective-nsamples, pycbc_inference will finalize the results and exit.

3. Run

To perform the analysis, run:


# sampler parameters

# the following sets the number of cores to use; adjust as needed to
# your computer's capabilities

# run sampler
# Running with OMP_NUM_THREADS=1 stops lalsimulation
# from spawning multiple jobs that would otherwise be used
# by pycbc_inference and cause a reduced runtime.
pycbc_inference --verbose \
    --seed 12 \
    --config-file ${PRIOR_CONFIG} ${DATA_CONFIG} ${SAMPLER_CONFIG} \
    --output-file ${OUTPUT_PATH} \
    --nprocesses ${NPROCS} \


Since we are generating waveforms and analyzing a 15 dimensional parameter space, this run will be much more computationally expensive than the analytic example. We recommend running this on a cluster or a computer with a large number of cores. In the example, we have set the parallelization to use 10 cores. With these settings, it should checkpoint approximately every hour or two. The run should complete in a few hours. If you would like to acquire more samples, increase effective-nsamples.


We have found that the emcee_pt sampler struggles to accumulate more than ~2000 independent samples with 200 walkers and 20 temps. The basic issue is that the ACT starts to grow at the same rate as new iterations, so that the number of independent samples remains the same. Increasing the number of walkers and decreasing the number of temperatures can help, but this sometimes leads to the sampler not fully converging before passing the burn in tests. If you want more than 2000 samples, we currently recommend doing multiple independent runs with different seed values, then combining posterior samples after they have finished.