Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the code to generate the sky grid that will be used by pycbc_multi_inspiral #4485

Merged
merged 10 commits into from
Jan 25, 2024
112 changes: 112 additions & 0 deletions bin/pycbc_make_sky_grid
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env python

"""For a given external trigger (GRB, FRB, neutrino, etc...), generate a sky grid covering its localization error region.

This sky grid will be used by `pycbc_multi_inspiral` to find multi-detector gravitational wave triggers and calculate the
coherent SNRs and related statistics.

The grid is constructed following the method described in Section V of https://arxiv.org/abs/1410.6042"""

import numpy as np
import h5py
import argparse
from pycbc.detector import Detector
import itertools
from scipy.spatial.transform import Rotation as R

def spher_to_cart(sky_points):
"""Convert spherical coordinates to cartesian coordinates.
"""
cart = np.zeros((len(sky_points), 3))
cart[:,0] = np.cos(sky_points[:,0]) * np.cos(sky_points[:,1])
cart[:,1] = np.sin(sky_points[:,0]) * np.cos(sky_points[:,1])
cart[:,2] = np.sin(sky_points[:,1])
return cart

def cart_to_spher(sky_points):
"""Convert cartesian coordinates to spherical coordinates.
"""
spher = np.zeros((len(sky_points), 2))
spher[:,0] = np.arctan2(sky_points[:,1], sky_points[:,0])
spher[:,1] = np.arcsin(sky_points[:,2])
return spher

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--ra', type=float, help="Right ascension (in rad) of the center of the external trigger error box")
parser.add_argument('--dec', type=float, help="Declination (in rad) of the center of the external trigger error box")
parser.add_argument('--instruments', nargs="+", type=str, required=True, help="List of instruments to analyze.")
parser.add_argument('--sky-error', type=float, required=True, help="3-sigma confidence radius (in rad) of the external trigger error box")
parser.add_argument('--trigger-time', type=int, required=True, help="Time (in s) of the external trigger")
parser.add_argument('--timing-uncertainty', type=float, default=0.0001, help="Timing uncertainty (in s) we are willing to accept")
parser.add_argument('--output', type=str, required=True, help="Name of the sky grid")

args = parser.parse_args()

if len(args.instruments) == 1:
parser.error('Can not make a sky grid for only one detector.')

args.instruments.sort() # Put the ifos in alphabetical order
detectors = args.instruments
detectors = [Detector(d) for d in detectors]
detector_pairs = list(itertools.combinations(detectors, 2))

# Select the detector pair for which the target location has smallest relative arrival time difference
tds = [detector_pairs[i][0].time_delay_from_detector(detector_pairs[i][1], args.ra, args.dec, args.trigger_time) for i in range(len(detector_pairs))]
smallest_td = np.argmin(tds)

# Calculate the light travel time between the two detectors
t = detector_pairs[smallest_td][0].light_travel_time_to_detector(detector_pairs[smallest_td][1])

# Calculate the required angular spacing of the sky points
angular_spacing = (2*args.timing_uncertainty) / np.sqrt(t**2 - tds[smallest_td]**2)

sky_points = np.zeros((1, 2))

number_of_rings = int(args.sky_error / angular_spacing)
titodalcanton marked this conversation as resolved.
Show resolved Hide resolved

# Generate the sky grid centered at the North pole
for i in range(number_of_rings+1):
if i == 0:
sky_points[0][0] = 0
sky_points[0][1] = np.pi/2
else:
number_of_points = int(2*np.pi*i)
for j in range(number_of_points):
sky_points = np.row_stack((sky_points, np.array([j/i, np.pi/2 - i*angular_spacing])))

# Convert spherical coordinates to cartesian coordinates
cart = spher_to_cart(sky_points)

grb = np.zeros((1, 2))
grb[0] = args.ra, args.dec
grb_cart = spher_to_cart(grb)

north_pole = [0, 0, 1]

ort = np.cross(grb_cart, north_pole)
norm = np.linalg.norm(ort)
ort /= norm
n = -np.arccos(np.dot(grb_cart, north_pole))
u = ort*n

# Rotate the sky grid to the center of the external trigger error box
r = R.from_rotvec(u)
rota = r.apply(cart)

# Convert cartesian coordinates back to spherical coordinates
spher = cart_to_spher(rota)

# Calculate the time delay corresponding to each sky point
time_delays = [detector_pairs[smallest_td][0].time_delay_from_detector(
detector_pairs[smallest_td][1], spher[i][0], spher[i][1], args.trigger_time) for i in range(len(spher))]

with h5py.File(args.output, 'w') as hf:
hf['ra'] = spher[:,0]
hf['dec'] = spher[:,1]
hf['trigger_ra'] = [args.ra]
hf['trigger_dec'] = [args.dec]
hf['sky_error'] = [args.sky_error]
hf['trigger_time'] = [args.trigger_time]
hf['timing_uncertainty'] = [args.timing_uncertainty]
hf['instruments'] = [d for d in args.instruments]
hf['time_delays'] = time_delays
Loading