Usage#
There are two interfaces to diffbank: a set of bank generation functions and
the convenient diffbank.bank.Bank class wrapper. The first interface is
more in keeping with jax’s functional approach, but we expect other users will
find the class interface more convenient. This page will show you how to use both.
Check out our genbank_*.py scripts
for more examples.
Waveform model and setup#
Let’s start by importing some jax modules and setting the random seed:
import jax.numpy as jnp
from jax import random
key = random.PRNGKey(526)
If you’re unfamiliar with jax random number generation, take a look here.
It’s a bit different than what you may be used to from e.g. numpy.
We will use a simple 3.5PN waveform as the signal model, taking the component black hole masses to lie between 1 and 3 solar masses.
from diffbank.waveforms.threePN_simple import Psi, amp
m_range = (1.0, 3.0)
Our bank generation scheme samples from the parameter space using the metric density.
This is done using rejection sampling, which means the user must provide a base
distribution, defined in terms of a sampler over the parameter space. Our base
sampler draws uniformly from the (m1, m2) parameter space, with the restriction
m1 > m2. This can be done using a convenience function from diffbank.utils:
from diffbank.utils import get_m1_m2_sampler
sampler = get_m1_m2_sampler(m_range, m_range)
Using a nonuniform base density necessitates defining its density function as well. This density function need not be normalized. Here we will take it to be 1.
Next we define a noise model. We will use an analytic version of the LIGO-I noise power spectral density:
from diffbank.noise import Sn_LIGOI as Sn
We also need a frequency grid to use for match calculations, in units of Hz:
fs = jnp.linspace(24.0, 512.0, 4880)
Lastly, bank generation requires setting the maximum mismatch m_star, target
parameter space coverage fraction eta and number of effectualness points to use
for convergence monitoring:
minimum_match = 0.95
m_star = 1 - minimum_match
eta = 0.9
n_eff = 1000
Bank class interface#
Initializing a bank requires the definitions from above and a name:
from diffbank.bank import Bank
bank = Bank(amp, Psi, fs, Sn, m_star, eta_star, sampler, name=f"3pn-bank")
Before we can generate the bank, we need to compute the maximum value of the ratio between the metric density and the base sampler density. For this waveform model we can easily find this point through numerical optimization, but it’s more convenient to estimate it with empirical supremum rejection sampling:
key, subkey = random.split(key)
bank.ratio_max = bank.est_ratio_max(subkey)
Now we can fill our bank! Let’s use the random bank generation scheme from our paper:
key, subkey = random.split(key)
bank.fill_bank(subkey, "random", n_eff)
This will print a tqdm progress bar to monitor
bank generation and take a few minutes to run. Afterwards, you can check the templates’
positions in bank.templates.
We could generate a stochastic bank by instead passing "stochastic" as the second
argument to fill_bank, which would take much longer to generate.
To estimate the coverage of our bank at 1000 points sampled from the metric density, we can run
key, subkey = random.split(key)
bank.calc_bank_effectualness(subkey, 1000)
This will populate the attributes effectualness, effectualness_points, eta_est
and eta_est_err of bank. The first two are the effectualness and sampled points.
The second are the resulting Monte Carlo estimate (and associated error) of the
banks’ coverage.
We can save the bank to the current working directory with
bank.save()
and reload it with
Bank.load("3pn-bank.npz", amp, Psi, Sn, sampler)
Note the variables you must provide when loading a bank. This is because we do not
use e.g. pickle to save function attributes.
Functional interface#
This interface requires a bit more manual setup. We must first set up the metric density:
from diffbank.metric import get_density
density_fun = lambda theta: get_density(theta, amp, Psi, fs, Sn)
A match function is also required to check whether effectualness points are covered. This requires defining padding arrays that make the IFFT maximization over the difference in time of coalescence for the two waveforms work correctly:
from diffbank.utils import get_match
eff_pad_low, eff_pad_high = get_eff_pads(fs)
match_fun = lambda theta1, theta2: get_match(
theta1, theta2, amp, Psi, amp, Psi, fs, Sn, eff_pad_low, eff_pad_high,
)
Finally we need to define the density of the base sampler, which we can set to 1 since it need not be normalized. Also, we need the ratio of the metric density to this one:
key, subkey = random.split(key)
density_fun_base = lambda _: jnp.array(1.0)
ratio_max = est_ratio_max(subkey, density_fun, sample_base, density_fun_base)
Then we can generate some templates:
key, subkey = random.split(key)
templates, eff_pts = gen_bank_random(
subkey,
1 - m_star,
eta,
match_fun,
ratio_max,
density_fun,
sample_base,
density_fun_base
)
This is the function wrapped by Bank.fill_bank.
For stochastic bank generation, you must pass in a sampler that proposes templates. This is because the choice of this sampler does not make a huge difference in bank generation time. We can set up a rejection sampler to draw from the metric density with
from diffbank.utils import gen_template_rejection
gen_template = lambda key: gen_template_rejection(
key, ratio_max, density_fun, sampler, density_fun_base
)
and then generate the bank:
templates, eff_pts = gen_bank_stochastic(
key, minimum_match, eta, match_fun, propose_template, eff_pt_sampler, n_eff
)
To do: explain how to check coverage.