From 5e90d979b7ddb8dfded562c850b284c2ca3ea9eb Mon Sep 17 00:00:00 2001
From: Brandon Piotrzkowski <brandon.piotrzkowski@ligo.org>
Date: Mon, 9 May 2022 11:43:57 -0400
Subject: [PATCH] Listen to GCN Fermi GBM_ALERT channel for earlier warning;
 fixes #396

---
 CHANGES.rst                                   |   2 +
 gwcelery/tasks/external_triggers.py           |  62 +++++++---
 gwcelery/tests/data/fermi_initial_grb_gcn.xml | 108 ++++++++++++++++++
 .../tests/test_tasks_external_triggers.py     |  71 ++++++++++++
 4 files changed, 227 insertions(+), 16 deletions(-)
 create mode 100644 gwcelery/tests/data/fermi_initial_grb_gcn.xml

diff --git a/CHANGES.rst b/CHANGES.rst
index 22bc89bb7..3b74319f0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -24,6 +24,8 @@ Changelog
 -   Move functions in handle_grb_gcn to asynchronous group to prevent detchar
     errors from interupting sky map generation.
 -   Prevent sub-threhsold GRBs from overwriting high-threshold GRBs.
+-   Listen to initial GBM alerts for earlier warning. Prevent these events
+    from triggering alerts unless later updated.
 
 1.0.1 (2022-05-09)
 ------------------
diff --git a/gwcelery/tasks/external_triggers.py b/gwcelery/tasks/external_triggers.py
index 5edb17a6e..3969be31a 100644
--- a/gwcelery/tasks/external_triggers.py
+++ b/gwcelery/tasks/external_triggers.py
@@ -69,7 +69,8 @@ def handle_snews_gcn(payload):
     detchar.check_vectors(event, event['graceid'], start, end)
 
 
-@gcn.handler(gcn.NoticeType.FERMI_GBM_FLT_POS,
+@gcn.handler(gcn.NoticeType.FERMI_GBM_ALERT,
+             gcn.NoticeType.FERMI_GBM_FLT_POS,
              gcn.NoticeType.FERMI_GBM_GND_POS,
              gcn.NoticeType.FERMI_GBM_FIN_POS,
              gcn.NoticeType.SWIFT_BAT_GRB_POS_ACK,
@@ -87,6 +88,13 @@ def handle_grb_gcn(payload):
     Filters out candidates likely to be noise. Creates external events
     from the notice if new notice, otherwise updates existing event. Then
     creates and/or grabs external sky map to be uploaded to the external event.
+
+    More info for these notices can be found at:
+    Fermi-GBM: https://gcn.gsfc.nasa.gov/fermi_grbs.html
+    Fermi-GBM sub: https://gcn.gsfc.nasa.gov/fermi_gbm_subthresh_archive.html
+    Swift: https://gcn.gsfc.nasa.gov/swift.html
+    INTEGRAL: https://gcn.gsfc.nasa.gov/integral.html
+    AGILE-MCAL: https://gcn.gsfc.nasa.gov/agile_mcal.html
     """
     root = etree.fromstring(payload)
     u = urlparse(root.attrib['ivorn'])
@@ -99,6 +107,9 @@ def handle_grb_gcn(payload):
         trig_id = root.find("./What/Param[@name='Trans_Num']").attrib['value']
     ext_group = 'Test' if root.attrib['role'] == 'test' else 'External'
 
+    notice_type = \
+        int(root.find("./What/Param[@name='Packet_Type']").attrib['value'])
+
     stream_obsv_dict = {'/SWIFT': 'Swift',
                         '/Fermi': 'Fermi',
                         '/INTEGRAL': 'INTEGRAL',
@@ -114,19 +125,27 @@ def handle_grb_gcn(payload):
     #  If not at least 50% chance of GRB we will not consider it for RAVEN
     likely_source = root.find("./What/Param[@name='Most_Likely_Index']")
     likely_prob = root.find("./What/Param[@name='Most_Likely_Prob']")
-    if likely_source is not None and \
+    not_likely_grb = likely_source is not None and \
         (likely_source.attrib['value'] != FERMI_GRB_CLASS_VALUE
-         or likely_prob.attrib['value'] < FERMI_GRB_CLASS_THRESH):
-        labels = ['NOT_GRB']
-    else:
-        labels = None
+         or likely_prob.attrib['value'] < FERMI_GRB_CLASS_THRESH)
+
+    #  Check if initial Fermi alert. These are generally unreliable and should
+    #  never trigger a RAVEN alert, but will give us earlier warning of a
+    #  possible coincidence. Later notices could change this.
+    initial_gbm_alert = notice_type == gcn.NoticeType.FERMI_GBM_ALERT
 
     #  Check if Swift has lost lock. If so then veto
     lost_lock = \
         root.find("./What/Group[@name='Solution_Status']" +
                   "/Param[@name='StarTrack_Lost_Lock']")
-    if lost_lock is not None and lost_lock.attrib['value'] == 'true':
+    swift_veto = lost_lock is not None and lost_lock.attrib['value'] == 'true'
+
+    #  Only send alerts if likely a GRB, is not a low-confidence early Fermi
+    #  alert, and if not a Swift veto
+    if not_likely_grb or initial_gbm_alert or swift_veto:
         labels = ['NOT_GRB']
+    else:
+        labels = None
 
     ivorn = root.attrib['ivorn']
     if 'subthresh' in ivorn.lower():
@@ -165,8 +184,6 @@ def handle_grb_gcn(payload):
         group_canvas += _launch_external_detchar.s(),
 
     if search in {'GRB', 'MDC'}:
-        notice_type = \
-            int(root.find("./What/Param[@name='Packet_Type']").attrib['value'])
         notice_date = root.find("./Who/Date").text
         group_canvas += external_skymaps.create_upload_external_skymap.s(
                       notice_type, notice_date),
@@ -279,23 +296,23 @@ def handle_grb_igwn_alert(alert):
         if _skymaps_are_ready(alert['object'], alert['data']['name'],
                               'compare'):
             # if both sky maps present and a coincidence, compare sky maps
-            se_id, ext_ids = _get_superevent_ext_ids(graceid, alert['object'],
-                                                     'compare')
-            superevent = gracedb.get_superevent(se_id)
+            superevent_id, ext_ids = _get_superevent_ext_ids(
+                graceid, alert['object'], 'compare')
+            superevent = gracedb.get_superevent(superevent_id)
             preferred_event_id = superevent['preferred_event']
             gw_group = gracedb.get_group(preferred_event_id)
             tl, th = raven._time_window(graceid, gw_group,
                                         [alert['object']['pipeline']],
                                         [alert['object']['search']])
-            raven.raven_pipeline([alert['object']], se_id, superevent,
+            raven.raven_pipeline([alert['object']], superevent_id, superevent,
                                  tl, th, gw_group)
         if _skymaps_are_ready(alert['object'], alert['data']['name'],
                               'combine'):
             # if both sky maps present and a raven alert, create combined
             # skymap
-            se_id, ext_id = _get_superevent_ext_ids(graceid, alert['object'],
-                                                    'combine')
-            external_skymaps.create_combined_skymap(se_id, ext_id)
+            superevent_id, ext_id = _get_superevent_ext_ids(
+                graceid, alert['object'], 'combine')
+            external_skymaps.create_combined_skymap(superevent_id, ext_id)
         elif 'EM_COINC' in alert['object']['labels']:
             # if not complete, check if GW sky map; apply label to external
             # event if GW sky map
@@ -310,6 +327,19 @@ def handle_grb_igwn_alert(alert):
             gracedb.create_label.si('SKYMAP_READY', ext_id)
             for ext_id in alert['object']['em_events']
         ).delay()
+    elif alert['alert_type'] == 'label_removed' and \
+            alert['object'].get('group') == 'External':
+        if alert['data']['name'] == 'NOT_GRB':
+            # if NOT_GRB is removed, re-check publishing conditions
+            superevent_id = alert['object']['superevent']
+            superevent = gracedb.get_superevent(superevent_id)
+            gw_group = superevent['preferred_event_data']['group']
+            coinc_far_dict = {
+                'temporal_coinc_far': superevent['time_coinc_far'],
+                'spatiotemporal_coinc_far': superevent['space_coinc_far']
+            }
+            raven.trigger_raven_alert(coinc_far_dict, superevent, graceid,
+                                      alert['object'], gw_group)
 
 
 @igwn_alert.handler('superevent',
diff --git a/gwcelery/tests/data/fermi_initial_grb_gcn.xml b/gwcelery/tests/data/fermi_initial_grb_gcn.xml
new file mode 100644
index 000000000..b5b8a197e
--- /dev/null
+++ b/gwcelery/tests/data/fermi_initial_grb_gcn.xml
@@ -0,0 +1,108 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<voe:VOEvent
+      ivorn="ivo://nasa.gsfc.gcn/Fermi#GBM_ALERT2018-05-24T09:58:26.31_548848711_0-566"
+      role="observation" version="2.0"
+      xmlns:voe="http://www.ivoa.net/xml/VOEvent/v2.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.ivoa.net/xml/VOEvent/v2.0  http://www.ivoa.net/xml/VOEvent/VOEvent-v2.0.xsd" >
+  <Who>
+    <AuthorIVORN>ivo://nasa.gsfc.tan/gcn</AuthorIVORN>
+    <Author>
+      <shortName>Fermi (via VO-GCN)</shortName>
+      <contactName>Julie McEnery</contactName>
+      <contactPhone>+1-301-286-1632</contactPhone>
+      <contactEmail>Julie.E.McEnery@nasa.gov</contactEmail>
+    </Author>
+    <Date>2018-05-24T18:35:45</Date>
+    <Description>This VOEvent message was created with GCN VOE version: 1.25 07feb18</Description>
+  </Who>
+  <What>
+    <Param name="Packet_Type"    value="110" />
+    <Param name="Pkt_Ser_Num"    value="15" />
+    <Param name="TrigID"         value="548841234" ucd="meta.id" />
+    <Param name="Sequence_Num"   value="0" ucd="meta.id.part" />
+    <Param name="Burst_TJD"      value="18262" unit="days" ucd="time" />
+    <Param name="Burst_SOD"      value="35906.31" unit="sec" ucd="time" />
+    <Param name="Burst_Inten"    value="0" unit="cts" ucd="phot.count" />
+    <Param name="Data_Integ"     value="0.000" unit="sec" ucd="time.interval" />
+    <Param name="Burst_Signif"   value="0.00" unit="sigma" ucd="stat.snr" />
+    <Param name="Phi"            value="262.01" unit="deg" ucd="pos.az.azi" />
+    <Param name="Theta"          value="64.10" unit="deg" ucd="pos.az.zd" />
+    <Param name="Algorithm"      value="415" unit="dn" />
+    <Param name="Lo_Energy"      value="50000" unit="keV"  />
+    <Param name="Hi_Energy"      value="300000" unit="keV"  />
+    <Param name="Trigger_ID"     value="0x0" />
+    <Param name="Misc_flags"     value="0x40000001" />
+    <Group name="Trigger_ID" >
+      <Param name="Def_NOT_a_GRB"         value="false" />
+      <Param name="Target_in_Blk_Catalog" value="false" />
+      <Param name="Human_generated"       value="false" />
+      <Param name="Robo_generated"        value="true" />
+      <Param name="Spatial_Prox_Match"    value="false" />
+      <Param name="Temporal_Prox_Match"   value="false" />
+      <Param name="Test_Submission"       value="false" />
+    </Group>
+    <Group name="Misc_Flags" >
+      <Param name="Values_Out_of_Range"   value="false" />
+      <Param name="Flt_Generated"         value="false" />
+      <Param name="Gnd_Generated"         value="true" />
+      <Param name="CRC_Error"             value="false" />
+    </Group>
+    <Param name="LightCurve_URL" value="http://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/triggers/2018/bn180524416/quicklook/glg_lc_medres34_bn180524416.gif" ucd="meta.ref.url" />
+    <Param name="LocationMap_URL" value="http://heasarc.gsfc.nasa.gov/FTP/fermi/data/gbm/triggers/2018/bn180524416/quicklook/glg_locplot_all_bn180524416.png" ucd="meta.ref.url" />
+    <Param name="Coords_Type"    value="1" unit="dn" />
+    <Param name="Coords_String"  value="source_object" />
+    <Group name="Obs_Support_Info" >
+      <Description>The Sun and Moon values are valid at the time the VOEvent XML message was created.</Description>
+      <Param name="Sun_RA"        value="61.53" unit="deg" ucd="pos.eq.ra" />
+      <Param name="Sun_Dec"       value="20.86" unit="deg" ucd="pos.eq.dec" />
+      <Param name="Sun_Distance"  value="94.75" unit="deg" ucd="pos.angDistance" />
+      <Param name="Sun_Hr_Angle"  value="-5.25" unit="hr" />
+      <Param name="Moon_RA"       value="187.65" unit="deg" ucd="pos.eq.ra" />
+      <Param name="Moon_Dec"      value="1.42" unit="deg" ucd="pos.eq.dec" />
+      <Param name="MOON_Distance" value="59.39" unit="deg" ucd="pos.angDistance" />
+      <Param name="Moon_Illum"    value="77.31" unit="%" ucd="arith.ratio" />
+      <Param name="Galactic_Long" value="264.32" unit="deg" ucd="pos.galactic.lon" />
+      <Param name="Galactic_Lat"  value="7.50" unit="deg" ucd="pos.galactic.lat" />
+      <Param name="Ecliptic_Long" value="160.83" unit="deg" ucd="pos.ecliptic.lon" />
+      <Param name="Ecliptic_Lat"  value="-50.93" unit="deg" ucd="pos.ecliptic.lat" />
+    </Group>
+    <Description>The Fermi-GBM location of a transient.</Description>
+  </What>
+  <WhereWhen>
+    <ObsDataLocation>
+      <ObservatoryLocation id="GEOLUN" />
+      <ObservationLocation>
+        <AstroCoordSystem id="UTC-FK5-GEO" />
+        <AstroCoords coord_system_id="UTC-FK5-GEO">
+          <Time unit="s">
+            <TimeInstant>
+              <ISOTime>2018-05-24T09:58:26.31</ISOTime>
+            </TimeInstant>
+          </Time>
+          <Position2D unit="deg">
+            <Name1>RA</Name1>
+            <Name2>Dec</Name2>
+            <Value2>
+              <C1>0.0</C1>
+              <C2>0.0</C2>
+            </Value2>
+            <Error2Radius>0.000</Error2Radius>
+          </Position2D>
+        </AstroCoords>
+      </ObservationLocation>
+    </ObsDataLocation>
+  <Description>The RA,Dec coordinates are of the type: source_object.</Description>
+  </WhereWhen>
+  <How>
+    <Description>Fermi Satellite, GBM Instrument</Description>
+    <Reference uri="http://gcn.gsfc.nasa.gov/fermi.html" type="url" />
+  </How>
+  <Why importance="0.95">
+    <Inference probability="1.0">
+      <Concept>process.variation.burst;em.gamma</Concept>
+    </Inference>
+  </Why>
+  <Description>
+  </Description>
+</voe:VOEvent>
diff --git a/gwcelery/tests/test_tasks_external_triggers.py b/gwcelery/tests/test_tasks_external_triggers.py
index 09be53574..2dd456827 100644
--- a/gwcelery/tests/test_tasks_external_triggers.py
+++ b/gwcelery/tests/test_tasks_external_triggers.py
@@ -155,6 +155,38 @@ def test_handle_noise_fermi_event(mock_check_vectors,
     mock_get_upload_external_skymap.assert_called_once()
 
 
+@patch('gwcelery.tasks.external_skymaps.create_external_skymap')
+@patch('gwcelery.tasks.external_skymaps.get_upload_external_skymap.run')
+@patch('gwcelery.tasks.gracedb.get_events', return_value=[])
+@patch('gwcelery.tasks.gracedb.create_event.run', return_value={
+    'graceid': 'E1', 'gpstime': 1, 'instruments': '', 'pipeline': 'Fermi',
+    'search': 'GRB',
+    'extra_attributes': {'GRB': {'trigger_duration': 1, 'trigger_id': 123,
+                                 'ra': 0., 'dec': 0., 'error_radius': 0.}},
+    'links': {'self': 'https://gracedb.ligo.org/events/E356793/'}})
+@patch('gwcelery.tasks.detchar.check_vectors.run')
+def test_handle_initial_fermi_event(mock_check_vectors,
+                                    mock_create_event,
+                                    mock_get_events,
+                                    mock_get_upload_external_skymap,
+                                    mock_create_external_skymap):
+    text = read_binary(data, 'fermi_initial_grb_gcn.xml')
+    external_triggers.handle_grb_gcn(payload=text)
+    mock_get_events.assert_called_once_with(query=(
+                                            'group: External pipeline: '
+                                            'Fermi grbevent.trigger_id '
+                                            '= "548841234"'))
+    # Note that this is the exact ID in the .xml file
+    mock_create_event.assert_called_once_with(filecontents=text,
+                                              search='GRB',
+                                              pipeline='Fermi',
+                                              group='External',
+                                              labels=['NOT_GRB'])
+    mock_check_vectors.assert_called_once()
+    mock_get_upload_external_skymap.assert_called_once()
+    mock_create_external_skymap.assert_not_called()
+
+
 @pytest.mark.parametrize('filename',
                          ['fermi_grb_gcn.xml',
                           'fermi_noise_gcn.xml',
@@ -261,6 +293,45 @@ def test_handle_skymap_comparison(mock_get_event, mock_get_superevent,
                                                 -5, 1, 'CBC')
 
 
+@patch('gwcelery.tasks.raven.trigger_raven_alert')
+@patch('gwcelery.tasks.gracedb.get_superevent',
+       return_value={'superevent_id': 'S1234',
+                     'preferred_event': 'G1234',
+                     'preferred_event_data': {
+                         'group': 'CBC'},
+                     'time_coinc_far': 1e-9,
+                     'space_coinc_far': 1e-10})
+def test_handle_label_removed(mock_get_superevent,
+                              mock_trigger_raven_alert):
+    alert = {"uid": "E1212",
+             "alert_type": "label_removed",
+             "data": {"name": "NOT_GRB"},
+             "object": {
+                 "graceid": "E1212",
+                 "group": "External",
+                 "labels": ["EM_COINC", "EXT_SKYMAP_READY", "SKYMAP_READY"],
+                 "superevent": "S1234",
+                 "pipeline": "Fermi",
+                 "search": "GRB"
+                       }
+             }
+    superevent = {'superevent_id': 'S1234',
+                  'preferred_event': 'G1234',
+                  'preferred_event_data': {
+                      'group': 'CBC'},
+                  'time_coinc_far': 1e-9,
+                  'space_coinc_far': 1e-10}
+    coinc_far_dict = {
+                'temporal_coinc_far': 1e-9,
+                'spatiotemporal_coinc_far': 1e-10
+            }
+    external_triggers.handle_grb_igwn_alert(alert)
+    mock_trigger_raven_alert.assert_called_once_with(
+        coinc_far_dict, superevent, alert['uid'],
+        alert['object'], 'CBC'
+    )
+
+
 @patch('gwcelery.tasks.external_skymaps.create_combined_skymap')
 def test_handle_skymap_combine(mock_create_combined_skymap):
     alert = {"uid": "E1212",
-- 
GitLab