GBGPU Tutorial

GBGPU is a GPU-accelerated version of the FastGB waveform which has been developed by Neil Cornish, Tyson Littenberg, Travis Robson, and Stas Babak. It computes gravitational waveforms for Galactic binary systems observable by LISA using a fast/slow-type decomposition. For more details on the original construction of FastGB see arXiv:0704.1808.

The current version of the code is very closely related to the implementation of FastGB in the LISA Data Challenges’ Python code package. The waveform code is entirely Python-based. It is about 1/2 the speed of the full C version, but much simpler in Python for right now. There are also many additional functions including fast likelihood computations for individual Galactic binaries, as well as fast C-based methods to combine waveforms into global fitting templates.

The code is CPU/GPU agnostic. CUDA and NVIDIA GPUs are required to run these codes for GPUs.

See the documentation for more details. This code was designed for arXiv:2205.03461. If you use any part of this code, please cite arXiv:2205.03461, its Zenodo page, arXiv:0704.1808, and arXiv:1806.00500.

[1]:
import numpy as np
import time

import matplotlib.pyplot as plt
%matplotlib inline

from gbgpu.gbgpu import GBGPU
from gbgpu.thirdbody import GBGPUThirdBody

from gbgpu.utils.constants import *
from gbgpu.utils.utility import *

Generating Galactic binary waveforms

Initialize the waveform class.

[2]:
gb = GBGPU(use_gpu=False)

Setup all the binary information. GBGPU operates in a vectorized manner, so it takes arrays of parameters as inputs.

[3]:
dt = 10.0
Tobs = 1.0 * YEAR


# number of points in waveform
# if None, will determine inside the code based on amp, f0 (and P2 if running third-body waveform)
N = None

# number of binaries to batch
num_bin = 10

# parameters
amp = 2e-23  # amplitude
f0 = 2e-3  # f0
fdot = 7.538331e-18  # fdot
fddot = 0.0 # fddot
phi0 = 0.1  # initial phase
iota = 0.2  # inclination
psi = 0.3  # polarization angle
lam = 0.4  # ecliptic longitude
beta_sky = 0.5  # ecliptic latitude


# for batching
amp_in = np.full(num_bin, amp)
f0_in = np.full(num_bin, f0)
fdot_in = np.full(num_bin, fdot)
fddot_in = np.full(num_bin, fddot)
phi0_in = np.full(num_bin, phi0)
iota_in = np.full(num_bin, iota)
psi_in = np.full(num_bin, psi)
lam_in = np.full(num_bin, lam)
beta_sky_in = np.full(num_bin, beta_sky)

Generate the waveforms.

[4]:
params = np.array(
    [amp_in, f0_in, fdot_in, fddot_in, phi0_in, iota_in, psi_in, lam_in, beta_sky_in,]
)

gb.run_wave(*params, N=N, dt=dt, T=Tobs, oversample=2)

# signal from first binary
A = gb.A[0]
freqs = gb.freqs[0]
print("signal length:", A.shape)
plt.plot(freqs, np.abs(A))
plt.ylabel("TDI-A Channel", fontsize=16)
plt.xlabel("freqs (Hz)", fontsize=16)
dx = 7e-7
plt.xlim(f0 - dx, f0 + dx)
signal length: (128,)
[4]:
(0.0019993, 0.0020007000000000002)
_images/GBGPU_tutorial_9_2.png

Adding additional GB astrophysics

It is possible in GBGPU to inherit a special class (``gbgpu.gbgpu.InheritGBGPU`) <https://mikekatz04.github.io/GBGPU/html/user/derivedwaves.html#gbgpu.gbgpu.InheritGBGPU>`__ that allows users to add other types of astrophysics to the FastGB waveform. This requires that the astrophysical effects vary slowly.

The methods that need to be written when adding astrophysics are prepare_additional_args, special_get_N, shift_frequency, and add_to_argS.

prepare_additional_args: Prepares all arguments beyond the base GB parameters.

special_get_N: Implemented if the new setup puts limitations on the sampling rate in the time-domain for the slow part of the waveform.

shift_frequency: Shifts the frequency in the slow computation.

add_to_argS: Adjusts the phasing in the transfer function of the slow waveform.

See `gbgpu.thirdbody.GBGPUThirdBody <https://mikekatz04.github.io/GBGPU/html/user/derivedwaves.html#gbgpu.thirdbody.ThirdBody>`__ for an example and more information.

Example: Third-body in orbit around the inner binary

[5]:
gb_third = GBGPUThirdBody(use_gpu=False)

A2 = 400.0  # third body amplitude parameter
varpi = 0.0  # varpi phase parameter
e2 = 0.3  # eccentricity of third body
P2 = 1.2  # period of third body
T2 = 0.5 * P2  # time of periapsis passage of third body


A2_in = np.full(num_bin, A2)
P2_in = np.full(num_bin, P2)
varpi_in = np.full(num_bin, varpi)
e2_in = np.full(num_bin, e2)
T2_in = np.full(num_bin, T2)
[6]:
params = np.array(
    [amp_in, f0_in, fdot_in, fddot_in, phi0_in, iota_in, psi_in, lam_in, beta_sky_in, A2_in, varpi_in, e2_in, P2_in, T2_in]
)

gb_third.run_wave(*params, N=N, dt=dt, T=Tobs, oversample=2)

# signal from first binary
A_third = gb_third.A[0]
freqs = gb_third.freqs[0]
print("Third-body signal length:", A_third.shape)
plt.plot(freqs, np.abs(A), label="No third body")
plt.plot(freqs, np.abs(A_third), label="No third body")
plt.ylabel("TDI-A Channel", fontsize=16)
plt.xlabel("freqs (Hz)", fontsize=16)
dx = 7e-7
plt.xlim(f0 - dx, f0 + dx)
Third-body signal length: (128,)
[6]:
(0.0019993, 0.0020007000000000002)
_images/GBGPU_tutorial_14_2.png

Calculating the Information Matrix

[7]:
params = np.array(
    [amp_in, f0_in, fdot_in, fddot_in, phi0_in, iota_in, psi_in, lam_in, beta_sky_in,]
)

inds = np.array([0, 1, 2, 4, 5, 6, 7, 8])

info_matrix = gb.information_matrix(
    params,
    easy_central_difference=False,
    eps=1e-9,
    inds=inds,
    N=1024,
    dt=dt,
    T=Tobs,
)

cov = np.linalg.pinv(info_matrix)

cov.shape
[7]:
(10, 8, 8)

Covariance Matrix for first binary:

[8]:
cov[0]
[8]:
array([[ 1.03670758e-02, -3.03751670e-09, -4.34853085e-06,
         9.30287319e-05, -4.16250330e-04,  5.58219344e-04,
        -5.55445555e-05,  4.37703212e-04],
       [-3.03751670e-09,  2.75957296e-12,  4.60004509e-09,
        -1.30903471e-07,  2.27487105e-10, -7.85411493e-07,
         1.13683606e-07, -6.31724576e-08],
       [-4.34853085e-06,  4.60004509e-09,  8.78628478e-06,
        -2.50761137e-04,  3.73944799e-07, -1.50454902e-03,
         1.72593472e-04, -7.75682309e-05],
       [ 9.30287318e-05, -1.30903471e-07, -2.50761137e-04,
         7.17103859e-03, -9.43217244e-06,  4.30257258e-02,
        -5.08042272e-03,  1.88810467e-03],
       [-4.16250330e-04,  2.27487092e-10,  3.73944776e-07,
        -9.43217177e-06,  1.67174837e-05, -5.65945132e-05,
         6.51025824e-06, -1.93069353e-05],
       [ 5.58219343e-04, -7.85411493e-07, -1.50454902e-03,
         4.30257258e-02, -5.65945172e-05,  2.58151320e-01,
        -3.04822260e-02,  1.13284197e-02],
       [-5.55445553e-05,  1.13683606e-07,  1.72593472e-04,
        -5.08042272e-03,  6.51025872e-06, -3.04822260e-02,
         1.08018147e-02, -8.85702255e-04],
       [ 4.37703212e-04, -6.31724576e-08, -7.75682309e-05,
         1.88810467e-03, -1.93069355e-05,  1.13284197e-02,
        -8.85702255e-04,  9.57210495e-03]])

Standard deviation on the marginalized parameters:

[9]:
params[inds, 0] * cov[0].diagonal() ** (1/2)
[9]:
array([2.03637676e-24, 3.32239249e-09, 2.23448704e-20, 8.46819850e-03,
       8.17740391e-04, 1.52425781e-01, 4.15727117e-02, 4.89185674e-02])

Plot the Information Matrix ellipse for the intial frequency and frequency derivative:

[10]:
ind1 = 1
ind2 = 2
inds_get = np.array([
    [ind1, ind1],
    [ind1, ind2],
    [ind2, ind1],
    [ind2, ind2]
]).T
sub_mat = cov[0][tuple(inds_get)].reshape(2, 2)

# calculate and draw covariance ellipse
a, b, b, c = sub_mat.flatten()

lam1 = (a + c) / 2. + np.sqrt(((a-c)/2) ** 2 + b ** 2)
lam2 = (a + c) / 2. - np.sqrt(((a-c)/2) ** 2 + b ** 2)

if b == 0. and a >= c:
    theta = 0.0
elif b == 0. and a < c:
    theta = np.pi / 2.
else:
    theta = -np.arctan2(lam1 - a, b)

t_vals = np.linspace(0., 2 * np.pi, 1000)
x = np.sqrt(lam1) * np.cos(theta) * np.cos(t_vals) - np.sqrt(lam2) * np.sin(theta) * np.sin(t_vals)
y = np.sqrt(lam1) * np.sin(theta) * np.cos(t_vals) + np.sqrt(lam2) * np.cos(theta) * np.sin(t_vals)

x_in = params[1, 0] * (1 + x)
y_in = params[2, 0] * (1 + y)
plt.plot(x_in, y_in)
[10]:
[<matplotlib.lines.Line2D at 0x154fa1220>]
_images/GBGPU_tutorial_22_1.png

Utility functions

GBGPU provides many utility functions. Below are some examples of GBGPU utility functions. This may not include all utility functions. See the utility documentation for all included utility functions.

Get the instantaneous gravitational wave frequency

Given \(f_0\), \(\dot{f}_0\), and \(\ddot{f}_0\) calculate the instantaneous frequency of the gravitational waves approximating the frequency evolution to quadratic order:

[11]:
num = 15
f0 = np.random.uniform(0.001, 0.002, num)
fdot = np.random.uniform(1e-17, 2e-17, num)
fddot = np.random.uniform(1e-28, 2e-28, num)

t = np.arange(0.0, 10 * YEAR, YEAR/200)

# cast t to be for all binaries
t = t[None, :]

f = get_fGW(f0, fdot, fddot, t)

plt.plot(t[0], f[0])
plt.xlabel(r"$t$ (s)")
plt.ylabel(r"$f_{gw}$ (Hz)")

[11]:
Text(0, 0.5, '$f_{gw}$ (Hz)')
_images/GBGPU_tutorial_27_1.png

Get amplitude (for slowly evolving source)

[12]:
m1 = np.full(num, 0.2)
m2 = np.full(num, 0.1)
f0 = np.full(num, 0.1)
d = np.full(num, 6.0)  # kpc
amp = get_amplitude(m1, m2, f0, d)

print(amp)
[6.37190427e-23 6.37190427e-23 6.37190427e-23 6.37190427e-23
 6.37190427e-23 6.37190427e-23 6.37190427e-23 6.37190427e-23
 6.37190427e-23 6.37190427e-23 6.37190427e-23 6.37190427e-23
 6.37190427e-23 6.37190427e-23 6.37190427e-23]

Get \(\dot{f}\)

[13]:
m1 = np.full(num, 0.2)
m2 = np.full(num, 0.1)
f0 = np.full(num, 0.001)

fdot = get_fdot(f0, m1=m1, m2=m2)

print(fdot)
[1.73056526e-19 1.73056526e-19 1.73056526e-19 1.73056526e-19
 1.73056526e-19 1.73056526e-19 1.73056526e-19 1.73056526e-19
 1.73056526e-19 1.73056526e-19 1.73056526e-19 1.73056526e-19
 1.73056526e-19 1.73056526e-19 1.73056526e-19]

Determine necessary sampling rate in the time-domin

[14]:
get_N(amp, f0, Tobs, oversample=1)
[14]:
array([64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64])

Citations

You can access the necessary citations for the specific waveforms with the citation property.

[15]:
print(gb.citation)

@software{michael_l_katz_2022_6500434,
  author       = {Michael L. Katz},
  title        = {mikekatz04/GBGPU: First official public release!},
  month        = apr,
  year         = 2022,
  publisher    = {Zenodo},
  version      = {v1.0.0},
  doi          = {10.5281/zenodo.6500434},
  url          = {https://doi.org/10.5281/zenodo.6500434}
}

@article{Cornish:2007if,
    author = "Cornish, Neil J. and Littenberg, Tyson B.",
    title = "{Tests of Bayesian Model Selection Techniques for Gravitational Wave Astronomy}",
    eprint = "0704.1808",
    archivePrefix = "arXiv",
    primaryClass = "gr-qc",
    doi = "10.1103/PhysRevD.76.083006",
    journal = "Phys. Rev. D",
    volume = "76",
    pages = "083006",
    year = "2007"
}

@article{Robson:2018svj,
    author = "Robson, Travis and Cornish, Neil J. and Tamanini, Nicola and Toonen, Silvia",
    title = "{Detecting hierarchical stellar systems with LISA}",
    eprint = "1806.00500",
    archivePrefix = "arXiv",
    primaryClass = "gr-qc",
    doi = "10.1103/PhysRevD.98.064012",
    journal = "Phys. Rev. D",
    volume = "98",
    number = "6",
    pages = "064012",
    year = "2018"
}