Basic Observability Calculations¶
Now we are going to teach our notice handler how to determine whether a gravitational-wave event is observable. We are going to use the astropy.coordinates module. (See also the Astropy example on observation planning in Python.) First, we will need to import a few extra Python modules:
import astropy.coordinates
import astropy.time
import astropy.units as u
import astropy_healpix as ah
The LIGO/Virgo/KAGRA probability sky maps are always in equatorial coordinates. Once we have looked up the coordinates of the HEALPix pixels, we will use Astropy to transform those coordinates to a horizontal (altitude–azimuth) frame for a particular site on the Earth at a particular time. Then we can quickly determine which pixels are visible from that site at that time, and integrate (sum) the probability contained in those pixels.
Note
You may want to do something more sophisticated like determine how much of
the probability is visible for at least a certain length of time. This
example will illustrate one key function of HEALPix (looking up coordinates
of the grid with ah.healpix_to_lonlat) and some of the key positional
astronomy functions with Astropy. For more advanced functionality, we
recommend the astroplan package.
def prob_observable(skymap, time):
"""
Determine the integrated probability contained in a gravitational-wave
sky map that is observable from a particular ground-based site at a
particular time.
Bonus: make a plot of probability versus UTC time!
"""
# Determine the sky area and probability contained in each pixel of the
# multi-resolution HEALPix sky map.
level, ipix = ah.uniq_to_level_ipix(skymap['UNIQ'])
nside = ah.level_to_nside(level)
pixel_area = ah.nside_to_pixel_area(nside)
prob = skymap['PROBDENSITY'] * pixel_area
# Look up the (celestial) coordinates of the center of each pixel.
ra, dec = ah.healpix_to_lonlat(ipix, nside, order='nested')
radecs = astropy.coordinates.SkyCoord(ra=ra, dec=dec)
# Geodetic coordinates of observatory (example here: Mount Wilson)
observatory = astropy.coordinates.EarthLocation(
lat=34.2247*u.deg, lon=-118.0572*u.deg, height=1742*u.m)
# Alt/az reference frame at observatory, at the given time
frame = astropy.coordinates.AltAz(obstime=time, location=observatory)
# Transform pixel coordinates to alt/az at the observatory
altaz = radecs.transform_to(frame)
# Where is the sun, at the given time?
sun_altaz = astropy.coordinates.get_sun(time).transform_to(frame)
# How likely is it that the (true, unknown) location of the source
# is within the area that is visible? Demand that the sun is at
# least 18 degrees below the horizon and that the airmass
# (secant of zenith angle approximation) is at most 2.5.
return prob[(sun_altaz.alt <= -18*u.deg) & (altaz.secz <= 2.5)].sum()
We will build on the JSON notice handler from the GCN sample code section. We need a few extra imports to decode the sky map and call our new function:
from base64 import b64decode
from io import BytesIO
import json
from astropy.table import Table
from gcn_kafka import Consumer
Now we update the handler to decode the sky map and call prob_observable:
def parse_notice(record):
record = json.loads(record)
# Only respond to mock events. Real events have GraceDB IDs like
# S1234567, mock events have GraceDB IDs like M1234567.
# NOTE NOTE NOTE replace the conditional below with this commented out
# conditional to only parse real events.
# if record['superevent_id'][0] != 'S':
# return
if record['superevent_id'][0] != 'M':
return
if record['alert_type'] == 'RETRACTION':
print(record['superevent_id'], 'was retracted')
return
# Respond only to 'CBC' events. Change 'CBC' to 'Burst' to respond to
# only unmodeled burst events.
if record['event']['group'] != 'CBC':
return
# Read the multi-resolution HEALPix sky map.
skymap_str = record.get('event', {}).get('skymap')
if skymap_str:
skymap_bytes = b64decode(skymap_str)
skymap = Table.read(BytesIO(skymap_bytes))
prob = prob_observable(skymap, astropy.time.Time.now())
print('Source has a {:d}% chance of being observable now'.format(
int(round(100 * prob))))
if prob > 0.5:
pass # FIXME: perform some action
Let’s run the new handler now…
consumer = Consumer(client_id='fill me in', client_secret='fill me in')
consumer.subscribe(['igwn.gwalert'])
while True:
for message in consumer.consume():
parse_notice(message.value())
When you run this script, each time you receive a sample LIGO/Virgo/KAGRA notice, it will print something like the following (note that probability will change as a function of time):
Source has a 76% chance of being observable now