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

Automatically make files for comprehensive offline gracedb uploads #4438

Closed
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0534ec9
Work to include SNR tiemseries in offline gracedb uploads
GarethCabournDavies Jul 14, 2023
e65f24b
Work on fixing non-matching SNR timeseries
GarethCabournDavies Jul 17, 2023
b0508ec
Get other-detector timeseries and PSD plots into the plots
GarethCabournDavies Jul 17, 2023
b6066bf
Get other-detector timeseries and PSD plots into the hdf file for upload
GarethCabournDavies Jul 17, 2023
64ecff2
simplify keys
GarethCabournDavies Jul 17, 2023
7ddf5cc
minor change to plotting
GarethCabournDavies Jul 17, 2023
c3e7be2
Allow single event XML files to be made in the workflow
GarethCabournDavies Jul 21, 2023
ece1285
TDC comments, minor fixes
GarethCabournDavies Aug 3, 2023
b3a8d2c
add xmlall to config
GarethCabournDavies Aug 8, 2023
a31f0ba
add bayestar into followup
GarethCabournDavies Aug 9, 2023
6679f25
add skymap plotting script
GarethCabournDavies Aug 9, 2023
2e12d21
Add wrapper script for bayestar
GarethCabournDavies Aug 15, 2023
af852aa
fixes to bayestar wrapper script
GarethCabournDavies Aug 18, 2023
97e5cbf
revert config test changes, edit upload script usage
GarethCabournDavies Aug 21, 2023
c1386d1
Fixes to upload script
GarethCabournDavies Aug 21, 2023
e997ee7
unused import
GarethCabournDavies Aug 21, 2023
d23d4ff
Update pycbc/workflow/minifollowups.py
GarethCabournDavies Aug 21, 2023
5b84ed5
Update pycbc/workflow/minifollowups.py
GarethCabournDavies Aug 21, 2023
f29afc2
Update pycbc/workflow/minifollowups.py
GarethCabournDavies Aug 21, 2023
292d494
Update pycbc/workflow/minifollowups.py
GarethCabournDavies Aug 21, 2023
75ab141
CC
GarethCabournDavies Aug 22, 2023
76603c7
Add command line into event log
GarethCabournDavies Aug 22, 2023
3e77ee8
clarify File in docstring
GarethCabournDavies Aug 22, 2023
b0832de
CC
GarethCabournDavies Aug 23, 2023
f2bb680
use reduced chisq to be consistent with Live
GarethCabournDavies Sep 6, 2023
465fefe
typo
GarethCabournDavies Sep 6, 2023
fff46e7
Add channel name to gracedb XML
GarethCabournDavies Sep 6, 2023
7e09357
Add ability to add arbitrary labels to gracedb uploads
GarethCabournDavies Sep 6, 2023
43e1b61
check allowed labels and provide helpful message if the label isnt al…
GarethCabournDavies Sep 6, 2023
f406d72
unused import
GarethCabournDavies Sep 6, 2023
3050449
more unused imports / function
GarethCabournDavies Sep 6, 2023
7256f9a
some help messages I missed in haste
GarethCabournDavies Sep 6, 2023
a2a170d
Apply suggestions from code review
GarethCabournDavies Oct 10, 2023
0b5c326
Apply suggestions from code review
GarethCabournDavies Oct 10, 2023
ab77a6d
start making TDC comments
GarethCabournDavies Oct 10, 2023
46dacb8
TDC comments
GarethCabournDavies Oct 11, 2023
340f9b9
simplify removal of time from snr dictionary
GarethCabournDavies Oct 11, 2023
fea78f3
Update bin/all_sky_search/pycbc_prepare_xml_for_gracedb
GarethCabournDavies Oct 11, 2023
4d27e15
Update bin/all_sky_search/pycbc_prepare_xml_for_gracedb
GarethCabournDavies Oct 11, 2023
019addb
minor comments
GarethCabournDavies Oct 11, 2023
71f02ed
bugfix
GarethCabournDavies Oct 11, 2023
13ce612
sanity check improvements, rename badlynamed variables, check snr tim…
GarethCabournDavies Oct 11, 2023
5086f5b
rejig logging
GarethCabournDavies Oct 11, 2023
ea0af69
update docstring, move log before upload in case of errors
GarethCabournDavies Oct 11, 2023
c07c3bc
Move identical function into shared module
GarethCabournDavies Oct 11, 2023
38fe661
typo
GarethCabournDavies Oct 11, 2023
639d968
Remove default description for help for argument weve removed default…
GarethCabournDavies Oct 12, 2023
5c2cece
Some CC changes
GarethCabournDavies Oct 12, 2023
b811a54
TDC Comments on pycbc_upload_single_event_to_gracedb
GarethCabournDavies Oct 13, 2023
9307200
TDC comments pycbc_foreground_minifollowup
GarethCabournDavies Oct 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions bin/all_sky_search/pycbc_make_bayestar_skymap
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env python
titodalcanton marked this conversation as resolved.
Show resolved Hide resolved

"""
Simple wraper around bayestar-localize-coincs to get sky localization for a
specific compact binary merger event.

Uses an XML containing SNR timeseries and PSD and calls BAYESTAR to produce a
sky localization.

The wrapper needs to exist as the XML format doesnt allow us to indicate
the low frequency cutoff or waveform used. We also want to be able to control
the output filename which is not possible in bayestar-localize-coincs

Unrecognised options will be passed straight to the bayestar subprocess
"""

import argparse
import subprocess
import shutil
import logging
import os
import glob
import sys
import random
import tempfile
import pycbc.version
from pycbc import init_logging

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--version', action="version",
version=pycbc.version.git_verbose_msg)
parser.add_argument('--verbose', action='count',
help="Make the logging increasingly more verbose")
parser.add_argument('--bayestar-executable',
help="The bayestar-localize-coinc executable to be run. "
"If not given, will use whatever is available in "
"the current environment.")
parser.add_argument('--event-xml', required=True,
help="XML file containing event information, SNR "
"timeseries and PSD to pass to bayestar")
parser.add_argument('--waveform', default='TaylorF2',
help="Waveform used in the matched-filtering "
"to generate the SNR timeseries.")
parser.add_argument('--low-frequency-cutoff', type=float, default=20,
help="Low-frequency cutoff used in the matched-filtering "
"to generate the SNR timeseries")
parser.add_argument('--output-file', required=True,
help="Filename to output the fits file to.")
titodalcanton marked this conversation as resolved.
Show resolved Hide resolved
args, unknown = parser.parse_known_args()

# Default logging is set higher than normal for this job
logging_level = args.verbose + 1 if args.verbose else None
init_logging(logging_level)
logging.info("Starting")

bayestar_exe = args.bayestar_executable or 'bayestar-localize-coincs'

tmpdir = tempfile.mkdtemp()

# Set up the command to pass to bayestar.
# The XML file path being passed twice is a legacy requirement, not a mistake.
cmd = [bayestar_exe,
args.event_xml,
args.event_xml,
'--waveform', args.waveform,
'--f-low', str(args.low_frequency_cutoff),
'-o', tmpdir]

# Pass any unknown options straight to the subprocess
cmd += unknown

logging.info("Running %s", ' '.join(cmd))
subprocess.check_output(cmd)

# Find the fits file in the temporary directory:
# It would be nice to do this better! - maybe use the input xml
# file to find the number which is going to be used?
fits_filenames = glob.glob(os.path.join(tmpdir, '*.fits*'))
if len(fits_filenames) != 1:
raise ValueError(f'argh, got {len(fits_filenames)} FITS files after running BAYESTAR, I want one!')

logging.info("Moving output to %s", args.output_file)
shutil.move(fits_filenames[0], args.output_file)

logging.info("Done")
227 changes: 227 additions & 0 deletions bin/all_sky_search/pycbc_prepare_xml_for_gracedb
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
#!/usr/bin/env python
GarethCabournDavies marked this conversation as resolved.
Show resolved Hide resolved

# Copyright (C) 2015-2023 Ian Harry, Gareth Cabourn Davies
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

"""
Take a coinc xml file containing multiple events and prepare one of them
for upload to gracedb.
"""

import argparse
import logging
import numpy as np
import h5py
import matplotlib
matplotlib.use('agg')

import pycbc
import lal
import lal.series
from ligo.lw import lsctables
from ligo.lw import utils as ligolw_utils
from ligo.segments import segment, segmentlist
from pycbc.io.ligolw import (
LIGOLWContentHandler,
snr_series_to_xml,
)
from pycbc.psd import interpolate
from pycbc.types import FrequencySeries, TimeSeries, load_timeseries
from pycbc.types import MultiDetOptionAction
from pycbc.results import generate_asd_plot, generate_snr_plot

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--verbose", action='count',
help="Increase logging level, default=info")
parser.add_argument("--psd-files", nargs='+', required=True,
help='HDF file(s) containing the PSDs to upload')
parser.add_argument("--snr-timeseries", nargs='+', required=True,
help='HDF file(s) containing the SNR timeseries to upload')
titodalcanton marked this conversation as resolved.
Show resolved Hide resolved
parser.add_argument('--input-file', required=True, type=str,
help='Input LIGOLW XML file of coincidences.')
parser.add_argument('--event-id', type=int, required=True,
help='ID for the event being prepared.')
parser.add_argument('--output-file',
help="Output filename for the locally stored XML. ")
parser.add_argument('--snr-timeseries-plot',
help="Output filename for the plot of SNR timeseries. "
"Must be a plot filetype as used by matplotlib.")
parser.add_argument('--psd-plot',
help="Output filename for the plot of the PSD. "
"Must be a plot filetype as used by matplotlib.")
parser.add_argument('--channel-name', action=MultiDetOptionAction,
required=True,
help="Channel name for adding to the uploaded single "
"inspiral table.")
parser.add_argument('--delta-f', type=float, default=0.25,
help="Frequency spacing of the PSD to be uploaded, Hz. "
"Default 0.25")

args = parser.parse_args()

if args.verbose:
args.verbose += 1
else:
args.verbose = 1
pycbc.init_logging(args.verbose)

xmldoc = ligolw_utils.load_filename(args.input_file,
contenthandler=LIGOLWContentHandler)

class psd_segment(segment):
def __new__(cls, psd, *args):
return segment.__new__(cls, *args)
def __init__(self, psd, *args):
self.psd = psd

titodalcanton marked this conversation as resolved.
Show resolved Hide resolved
logging.info("Reading PSDs")
psds = {}
for psd_file in args.psd_files:
(ifo, group), = h5py.File(psd_file, "r").items()
psd = [group["psds"][str(i)] for i in range(len(group["psds"].keys()))]
psds[ifo] = segmentlist(psd_segment(*segargs) for segargs in zip(
psd, group["start_time"], group["end_time"]))

titodalcanton marked this conversation as resolved.
Show resolved Hide resolved
# Get information from the single_template snr timeseries files:
logging.info("Reading SNR timeseries")
snr_timeseries = {}
for snr_filename in args.snr_timeseries:
with h5py.File(snr_filename, 'r') as snr_f:
ifo = snr_f.attrs['ifo']
time = snr_f.attrs['event_time']
snr_timeseries[(ifo, time)] = load_timeseries(snr_filename, 'snr')

coinc_table = lsctables.CoincTable.get_table(xmldoc)
coinc_inspiral_table = lsctables.CoincInspiralTable.get_table(xmldoc)
coinc_event_map_table = lsctables.CoincMapTable.get_table(xmldoc)
sngl_inspiral_table = lsctables.SnglInspiralTable.get_table(xmldoc)

xmldoc.childNodes[-1].removeChild(sngl_inspiral_table)
xmldoc.childNodes[-1].removeChild(coinc_event_map_table)
xmldoc.childNodes[-1].removeChild(coinc_inspiral_table)
xmldoc.childNodes[-1].removeChild(coinc_table)

# Find the coinc_table entry corresponding to the desired event ID
for event in coinc_table:
coinc_event_id = event.coinc_event_id
if args.event_id == coinc_event_id:
# This is the event we want
break
titodalcanton marked this conversation as resolved.
Show resolved Hide resolved
else:
raise ValueError(f"event {args.event_id} not found in coinc table")

coinc_event_table_curr = lsctables.New(lsctables.CoincTable)
coinc_event_table_curr.append(event)
coinc_inspiral_table_curr = lsctables.New(lsctables.CoincInspiralTable)
coinc_event_map_table_curr = lsctables.New(lsctables.CoincMapTable)
sngl_inspiral_table_curr = lsctables.New(lsctables.SnglInspiralTable)

for coinc_insp in coinc_inspiral_table:
if coinc_insp.coinc_event_id == event.coinc_event_id:
coinc_inspiral_table_curr.append(coinc_insp)

time = coinc_inspiral_table_curr[0].end_time \
+ coinc_inspiral_table_curr[0].end_time_ns * 1e-9

sngl_ids = []
for coinc_map in coinc_event_map_table:
if coinc_map.coinc_event_id == event.coinc_event_id:
coinc_event_map_table_curr.append(coinc_map)
sngl_ids.append(coinc_map.event_id)

# Get the SNR timeseries and PSDs at the time of this event
# these may not have contributed a trigger

# Check that the SNR timeseries correspond to the event
# - allow 0.5s either side of the time
snr_ts = {}
for ifo, t in snr_timeseries.keys():
if not abs(t - time) < 0.5:
raise ValueError("SNR timeseries for IFO %s does not look like it "
"corresponds to this event, event time %.3f, SNR timeseries is "
"around time %.3f" % (ifo, time, t))
# Convert the SNR dict to be keyed on IFO-only:
snr_ts[ifo] = snr_timeseries[(ifo, t)]

# IFOs from SNR timeseries:
psds_event = {}
psddict = {}
for ifo in snr_ts.keys():
psd = psds[ifo]

psd = psd[psd.find(time)].psd
# resample the psd to new spacing
psd_fs = FrequencySeries(psd, delta_f=psd.attrs["delta_f"],
dtype=np.float64)
psd_fs = interpolate(psd_fs, args.delta_f)

psds_event[ifo] = psd
psddict[ifo] = psd_fs

lal_psddict = {}
sample_freqs = {}

for sngl in sngl_inspiral_table:
if sngl.event_id not in sngl_ids:
continue
sngl_inspiral_table_curr.append(sngl)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we including "subthreshold" detectors here? E.g. how would the single inspiral table for a L1-only trigger with observing-mode H1 look like?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not including subthreshold detectors here, this is inherited from the pycbc_page_foreground output, so does not have this info added. I'm not sure whether we'd want that though or not

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one from the github example run, and have uploaded to https://gracedb-playground.ligo.org/events/G1268054/view/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I suspect we do need the single inspiral entries to be present in order for BAYESTAR to use the subthreshold detectors, but let's keep things as they are for now. We can fix that with a later PR if needed.

sngl.channel = args.channel_name[sngl.ifo]

# Two versions of the PSD dictionary have useful info here
psd = psds_event[sngl.ifo]
psd_fs = psddict[sngl.ifo]

flow = psd.file.attrs['low_frequency_cutoff']
kmin = int(flow / args.delta_f)

fseries = lal.CreateREAL8FrequencySeries(
"psd",
lal.LIGOTimeGPS(int(psd.attrs["epoch"])),
kmin * args.delta_f,
args.delta_f,
lal.StrainUnit**2 / lal.HertzUnit,
len(psd_fs) - kmin)
fseries.data.data = psd_fs[kmin:] / np.square(pycbc.DYN_RANGE_FAC)
lal_psddict[sngl.ifo] = fseries

snr_series_to_xml(snr_ts[sngl.ifo], xmldoc, sngl.event_id)

xmldoc.childNodes[-1].appendChild(coinc_event_table_curr)
xmldoc.childNodes[-1].appendChild(coinc_inspiral_table_curr)
xmldoc.childNodes[-1].appendChild(coinc_event_map_table_curr)
xmldoc.childNodes[-1].appendChild(sngl_inspiral_table_curr)
lal.series.make_psd_xmldoc(lal_psddict, xmldoc.childNodes[-1])

ligolw_utils.write_filename(xmldoc, args.output_file)
logging.info("Saved XML file %s", args.output_file)

if args.psd_plot:
logging.info("Saving ASD plot %s", args.psd_plot)
generate_asd_plot(psddict, args.psd_plot)

triggers = {sngl.ifo: (sngl.end_time + sngl.end_time_ns * 1e-9, sngl.snr)
for sngl in sngl_inspiral_table_curr}
base_time = int(np.floor(time))
if args.snr_timeseries_plot:
logging.info("Saving SNR plot %s", args.snr_timeseries_plot)
generate_snr_plot(
snr_ts,
args.snr_timeseries_plot,
triggers,
base_time
)

logging.info('Done!')
Loading
Loading