From 619080fa31e52503f28f90fc750ce5ae5c9b47d3 Mon Sep 17 00:00:00 2001
From: Tanner Prestegard <tanner.prestegard@ligo.org>
Date: Wed, 15 May 2019 11:29:40 -0500
Subject: [PATCH] Update how VOEvent building is done

We can change a few things about how the VOEvent file building
works now that all of the relevant parameters are stored in the
database.
---
 gracedb/api/v1/events/views.py      | 18 +++---
 gracedb/events/buildVOEvent.py      | 87 ++++++++++++++---------------
 gracedb/superevents/buildVOEvent.py | 77 +++++++++++--------------
 gracedb/superevents/utils.py        | 14 ++---
 4 files changed, 94 insertions(+), 102 deletions(-)

diff --git a/gracedb/api/v1/events/views.py b/gracedb/api/v1/events/views.py
index e47d6e586..8fe188cfd 100644
--- a/gracedb/api/v1/events/views.py
+++ b/gracedb/api/v1/events/views.py
@@ -1628,23 +1628,25 @@ class VOEventList(InheritPermissionsAPIView):
             return Response({'error': msg}, status = status.HTTP_400_BAD_REQUEST)
 
         # Instantiate the voevent and save in order to get the serial number
-        voevent = VOEvent(voevent_type=voevent_type, event=event, issuer=request.user)
+        voevent = VOEvent(event=event, issuer=request.user,
+            voevent_type=voevent_type, skymap_type=skymap_type,
+            skymap_filename=skymap_filename, internal=internal,
+            hardware_inj=hardware_inj, coinc_comment=CoincComment,
+            prob_has_ns=ProbHasNS, prob_has_remnant=ProbHasRemnant,
+            prob_bns=BNS, prob_nsbh=NSBH, prob_bbh=BBH,
+            prob_terrestrial=Terrestrial, prob_mass_gap=MassGap)
 
         try:
             voevent.save()
+        except ValidationError as e:
+            return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
         except Exception as e:
             return Response("Failed to create VOEvent: %s" % str(e),
                     status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
         # Now, you need to actually build the VOEvent.
         try:
-            voevent_text, ivorn = buildVOEvent(event, voevent.N, voevent_type, request,
-                skymap_filename = skymap_filename, skymap_type = skymap_type,
-                internal = internal, open_alert=open_alert,
-                hardware_inj=hardware_inj, CoincComment=CoincComment,
-                ProbHasNS=ProbHasNS, ProbHasRemnant=ProbHasRemnant, BNS=BNS,
-                NSBH=NSBH, BBH=BBH, Terrestrial=Terrestrial, MassGap=MassGap)
-
+            voevent_text, ivorn = buildVOEvent(event, voevent, request=request)
         except VOEventBuilderException, e:
             msg = "Problem building VOEvent: %s" % str(e)
             return Response({'error': msg}, status = status.HTTP_400_BAD_REQUEST)
diff --git a/gracedb/events/buildVOEvent.py b/gracedb/events/buildVOEvent.py
index 5e7e5c190..1ef4f27e3 100644
--- a/gracedb/events/buildVOEvent.py
+++ b/gracedb/events/buildVOEvent.py
@@ -50,10 +50,7 @@ def get_voevent_type(short_name):
             return t[1]
     return None
 
-def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filename=None,
-    skymap_type=None, internal=True, open_alert=False, hardware_inj=False,
-    CoincComment=False, ProbHasNS=None, ProbHasRemnant=None, BNS=None,
-    NSBH=None, BBH=None, Terrestrial=None, MassGap=None):
+def buildVOEvent(event, voevent, request=None):
 
 # XXX Branson commenting out. Reed's MDC events do not have FAR for some reason.
 #    if not event.far:
@@ -62,18 +59,17 @@ def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filena
     if not event.gpstime:
         raise VOEventBuilderException("Cannot build a VOEvent because event has no gpstime.")
 
-    if not voevent_type in VOEVENT_TYPE_DICT.keys():
+    if not voevent.voevent_type in VOEVENT_TYPE_DICT.keys():
         raise VOEventBuilderException("voevent_type must be preliminary, initial, update, or retraction")
 
     # Let's convert that voevent_type to something nicer looking
-    default_voevent_type = voevent_type
-    voevent_type = VOEVENT_TYPE_DICT[voevent_type]
+    voevent_type = VOEVENT_TYPE_DICT[voevent.voevent_type]
 
     objid = event.graceid
 
     # Now build the IVORN. 
     type_string = voevent_type.capitalize()
-    event_id = "%s-%d-%s" % (objid, serial_number, type_string)
+    event_id = "%s-%d-%s" % (objid, voevent.N, type_string)
     ivorn = settings.IVORN_PREFIX + event_id
 
     ############ VOEvent header ############################
@@ -117,7 +113,7 @@ def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filena
             h.add_Description("L1: LIGO Livingston 4 km gravitational wave detector")
         if 'V1' in instruments:
             h.add_Description("V1: Virgo 3 km gravitational wave detector")
-        if int(CoincComment) == 1:
+        if int(voevent.coinc_comment) == 1:
             h.add_Description("A gravitational wave trigger identified a possible counterpart GRB")
         v.set_How(h)
 
@@ -141,18 +137,19 @@ def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filena
 
     # Add Packet_Type for GCNs
     w.add_Param(Param(name="Packet_Type",
-        value=PACKET_TYPES[default_voevent_type][0], dataType="int",
+        value=PACKET_TYPES[voevent.voevent_type][0], dataType="int",
         Description=[("The Notice Type number is assigned/used within GCN, eg "
         "type={typenum} is an {typedesc} notice").format(
-        typenum=PACKET_TYPES[default_voevent_type][0],
-        typedesc=PACKET_TYPES[default_voevent_type][1])]))
+        typenum=PACKET_TYPES[voevent.voevent_type][0],
+        typedesc=PACKET_TYPES[voevent.voevent_type][1])]))
 
     # Whether the alert is internal or not
-    w.add_Param(Param(name="internal", value=int(internal), dataType="int",
-        Description=['Indicates whether this event should be distributed to LSC/Virgo members only']))
+    w.add_Param(Param(name="internal", value=int(voevent.internal),
+        dataType="int", Description=['Indicates whether this event should be '
+        'distributed to LSC/Virgo members only']))
 
     # The serial number
-    w.add_Param(Param(name="Pkt_Ser_Num", value=serial_number,
+    w.add_Param(Param(name="Pkt_Ser_Num", value=voevent.N,
         Description=["A number that increments by 1 each time a new revision "
         "is issued for this event"]))
 
@@ -175,13 +172,13 @@ def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filena
     w.add_Param(Param(name="HardwareInj",
         dataType="int",
         ucd="meta.number",
-        value=int(hardware_inj),
+        value=int(voevent.hardware_inj),
         Description=['Indicates that this event is a hardware injection if 1, no if 0']))
 
     w.add_Param(Param(name="OpenAlert",
         dataType="int",
         ucd="meta.number",
-        value=int(open_alert),
+        value=int(voevent.open_alert),
         Description=['Indicates that this event is an open alert if 1, no if 0']))
 
     w.add_Param(Param(name="EventPage",
@@ -232,20 +229,20 @@ def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filena
     # new feature (10/24/5/2016): preliminary VOEvents can have a skymap,
     # but they don't have to.
     if (voevent_type in ["initial", "update"] or 
-       (voevent_type == "preliminary" and skymap_filename != None)):
-        if not skymap_filename:
+       (voevent_type == "preliminary" and voevent.skymap_filename != None)):
+        if not voevent.skymap_filename:
             raise VOEventBuilderException("Skymap filename not provided.")
 
-        fits_name = skymap_filename
+        fits_name = voevent.skymap_filename
         fits_path = os.path.join(event.datadir, fits_name)
         if not os.path.exists(fits_path):
-            raise VOEventBuilderException("Skymap file does not exist: %s" % skymap_filename)
+            raise VOEventBuilderException("Skymap file does not exist: %s" % voevent.skymap_filename)
 
-        if not skymap_type:
+        if not voevent.skymap_type:
             raise VOEventBuilderException("Skymap type must be provided.")
 
         # Skymaps. Create group and set fits file name
-        g = Group('GW_SKYMAP', skymap_type)
+        g = Group('GW_SKYMAP', voevent.skymap_type)
 
         fits_skymap_url = build_absolute_uri(reverse(
             "api:default:events:files", args=[objid, fits_name]), request)
@@ -274,54 +271,56 @@ def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filena
             eta = pow((mchirp/mass),5.0/3.0)
 
             # EM-Bright mass classifier information for CBC event candidates
-            if BNS is not None:
+            if voevent.prob_bns is not None:
                 classification_group.add_Param(Param(name="BNS",
                     dataType="float", ucd="stat.probability",
-                    value=BNS, Description=["Probability that the "
-                    "source is a binary neutron star merger (both objects "
+                    value=voevent.prob_bns, Description=["Probability that "
+                    "the source is a binary neutron star merger (both objects "
                     "lighter than 3 solar masses)"]))
 
-            if NSBH is not None:
+            if voevent.prob_nsbh is not None:
                 classification_group.add_Param(Param(name="NSBH",
                     dataType="float", ucd="stat.probability",
-                    value=NSBH, Description=["Probability that the "
-                    "source is a neutron star-black hole merger (primary "
+                    value=voevent.prob_nsbh, Description=["Probability that "
+                    "the source is a neutron star-black hole merger (primary "
                     "heavier than 5 solar masses, secondary lighter than 3 "
                     "solar masses)"]))
 
-            if BBH is not None:
+            if voevent.prob_bbh is not None:
                 classification_group.add_Param(Param(name="BBH",
                     dataType="float", ucd="stat.probability",
-                    value=BBH, Description=["Probability that the "
-                    "source is a binary black hole merger (both objects "
+                    value=voevent.prob_bbh, Description=["Probability that "
+                    "the source is a binary black hole merger (both objects "
                     "heavier than 5 solar masses)"]))
 
-            if MassGap is not None:
+            if voevent.prob_mass_gap is not None:
                 classification_group.add_Param(Param(name="MassGap",
                     dataType="float", ucd="stat.probability",
-                    value=MassGap, Description=["Probability that the source "
-                    "has at least one object between 3 and 5 solar masses"]))
+                    value=voevent.prob_mass_gap, Description=["Probability "
+                    "that the source has at least one object between 3 and 5 "
+                    "solar masses"]))
 
-            if Terrestrial is not None:
+            if voevent.prob_terrestrial is not None:
                 classification_group.add_Param(Param(name="Terrestrial",
                     dataType="float", ucd="stat.probability",
-                    value=Terrestrial, Description=["Probability "
+                    value=voevent.prob_terrestrial, Description=["Probability "
                     "that the source is terrestrial (i.e., a background noise "
                     "fluctuation or a glitch)"]))
 
             # Add to source properties group
-            if ProbHasNS is not None:
+            if voevent.prob_has_ns is not None:
                 properties_group.add_Param(Param(name="HasNS",
-                    dataType="float", ucd="stat.probability", value=ProbHasNS,
+                    dataType="float", ucd="stat.probability",
+                    value=voevent.prob_has_ns,
                     Description=["Probability that at least one object in the "
                     "binary has a mass that is less than 3 solar masses"]))
 
-            if ProbHasRemnant is not None:
+            if voevent.prob_has_remnant is not None:
                 properties_group.add_Param(Param(name="HasRemnant",
                     dataType="float", ucd="stat.probability",
-                    value=ProbHasRemnant, Description=["Probability that a "
-                    "nonzero mass was ejected outside the central remnant "
-                    "object"]))
+                    value=voevent.prob_has_remnant, Description=["Probability "
+                    "that a nonzero mass was ejected outside the central "
+                    "remnant object"]))
 
             # build up MaxDistance. event.singleinspiral_set.all()?
             # Each detector calculates an effective distance assuming the inspiral is 
@@ -456,7 +455,7 @@ def buildVOEvent(event, serial_number, voevent_type, request=None, skymap_filena
         c = Citations()
         for ve in event.voevent_set.all():
             # Oh, actually we need to exclude *this* voevent.
-            if serial_number == ve.N:
+            if ve.N == voevent.N:
                 continue
             if voevent_type == 'initial':
                 ei = EventIVORN('supersedes', ve.ivorn)
diff --git a/gracedb/superevents/buildVOEvent.py b/gracedb/superevents/buildVOEvent.py
index 6cee928d6..10bbdaaa6 100644
--- a/gracedb/superevents/buildVOEvent.py
+++ b/gracedb/superevents/buildVOEvent.py
@@ -53,23 +53,12 @@ def get_voevent_type(short_name):
     return None
 
 
-def construct_voevent_file(superevent, voevent, request=None,
-    skymap_filename=None, skymap_type=None, internal=True, open_alert=False,
-    hardware_inj=False, CoincComment=False, ProbHasNS=None,
-    ProbHasRemnant=None, BNS=None, NSBH=None, BBH=None, Terrestrial=None,
-    MassGap=None):
+def construct_voevent_file(superevent, voevent, request=None):
 
     # Set preferred_event as event to be used in most of this
-    event = superevent.preferred_event
     # Get the event subclass (CoincInspiralEvent, MultiBurstEvent, etc.) and
     # set that as the event
-    subclass_fields = [f for f in event.__class__._meta.get_fields()
-        if (f.one_to_one and f.auto_created and not f.concrete and
-            event.__class__ in f.related_model.__bases__)]
-    for f in subclass_fields:
-        if hasattr(event, f.name):
-            event = getattr(event, f.name)
-            break
+    event = superevent.preferred_event.get_subclass_or_self()
 
     # Let's convert that voevent_type to something nicer looking
     voevent_type = VOEVENT_TYPE_DICT[voevent.voevent_type]
@@ -114,7 +103,7 @@ def construct_voevent_file(superevent, voevent, request=None,
             h.add_Description("L1: LIGO Livingston 4 km gravitational wave detector")
         if 'V1' in instruments:
             h.add_Description("V1: Virgo 3 km gravitational wave detector")
-        if CoincComment:
+        if voevent.coinc_comment:
             h.add_Description("A gravitational wave trigger identified a possible counterpart GRB")
         v.set_How(h)
 
@@ -145,8 +134,9 @@ def construct_voevent_file(superevent, voevent, request=None,
         typedesc=PACKET_TYPES[voevent.voevent_type][1])]))
 
     # Whether the alert is internal or not
-    w.add_Param(Param(name="internal", value=int(internal), dataType="int",
-        Description=['Indicates whether this event should be distributed to LSC/Virgo members only']))
+    w.add_Param(Param(name="internal", value=int(voevent.internal),
+        dataType="int", Description=['Indicates whether this event should be '
+        'distributed to LSC/Virgo members only']))
     
     # The serial number
     w.add_Param(Param(name="Pkt_Ser_Num", value=voevent.N,
@@ -171,13 +161,13 @@ def construct_voevent_file(superevent, voevent, request=None,
     w.add_Param(Param(name="HardwareInj",
         dataType="int",
         ucd="meta.number",
-        value=int(hardware_inj),
+        value=int(voevent.hardware_inj),
         Description=['Indicates that this event is a hardware injection if 1, no if 0']))
 
     w.add_Param(Param(name="OpenAlert",
         dataType="int",
         ucd="meta.number",
-        value=int(open_alert),
+        value=int(voevent.open_alert),
         Description=['Indicates that this event is an open alert if 1, no if 0']))
 
     # Superevent page
@@ -230,16 +220,15 @@ def construct_voevent_file(superevent, voevent, request=None,
     # new feature (10/24/2016): preliminary VOEvents can have a skymap,
     # but they don't have to.
     if (voevent_type in ["initial", "update"] or 
-       (voevent_type == "preliminary" and skymap_filename != None)):
-
-        fits_name = skymap_filename
+       (voevent_type == "preliminary" and voevent.skymap_filename != None)):
 
         # Skymaps. Create group and set fits file name
-        g = Group('GW_SKYMAP', skymap_type)
+        g = Group('GW_SKYMAP', voevent.skymap_type)
 
         fits_skymap_url = build_absolute_uri(reverse(
             "api:default:superevents:superevent-file-detail",
-            args=[superevent.default_superevent_id, fits_name]), request)
+            args=[superevent.default_superevent_id, voevent.skymap_filename]),
+            request)
 
         # Add parameters to the skymap group
         g.add_Param(Param(name="skymap_fits", dataType="string",
@@ -265,54 +254,56 @@ def construct_voevent_file(superevent, voevent, request=None,
             eta = pow((mchirp/mass),5.0/3.0)
 
             # EM-Bright mass classifier information for CBC event candidates
-            if BNS is not None:
+            if voevent.prob_bns is not None:
                 classification_group.add_Param(Param(name="BNS",
                     dataType="float", ucd="stat.probability",
-                    value=BNS, Description=["Probability that the "
-                    "source is a binary neutron star merger (both objects "
+                    value=voevent.prob_bns, Description=["Probability that "
+                    "the source is a binary neutron star merger (both objects "
                     "lighter than 3 solar masses)"]))
 
-            if NSBH is not None:
+            if voevent.prob_nsbh is not None:
                 classification_group.add_Param(Param(name="NSBH",
                     dataType="float", ucd="stat.probability",
-                    value=NSBH, Description=["Probability that the "
-                    "source is a neutron star-black hole merger (primary "
+                    value=voevent.prob_nsbh, Description=["Probability that "
+                    "the source is a neutron star-black hole merger (primary "
                     "heavier than 5 solar masses, secondary lighter than 3 "
                     "solar masses)"]))
 
-            if BBH is not None:
+            if voevent.prob_bbh is not None:
                 classification_group.add_Param(Param(name="BBH",
                     dataType="float", ucd="stat.probability",
-                    value=BBH, Description=["Probability that the "
-                    "source is a binary black hole merger (both objects "
+                    value=voevent.prob_bbh, Description=["Probability that "
+                    "the source is a binary black hole merger (both objects "
                     "heavier than 5 solar masses)"]))
 
-            if MassGap is not None:
+            if voevent.prob_mass_gap is not None:
                 classification_group.add_Param(Param(name="MassGap",
                     dataType="float", ucd="stat.probability",
-                    value=MassGap, Description=["Probability that the source "
-                    "has at least one object between 3 and 5 solar masses"]))
+                    value=voevent.prob_mass_gap,
+                    Description=["Probability that the source has at least "
+                    "one object between 3 and 5 solar masses"]))
 
-            if Terrestrial is not None:
+            if voevent.prob_terrestrial is not None:
                 classification_group.add_Param(Param(name="Terrestrial",
                     dataType="float", ucd="stat.probability",
-                    value=Terrestrial, Description=["Probability "
+                    value=voevent.prob_terrestrial, Description=["Probability "
                     "that the source is terrestrial (i.e., a background noise "
                     "fluctuation or a glitch)"]))
 
             # Add to source properties group
-            if ProbHasNS is not None:
+            if voevent.prob_has_ns is not None:
                 properties_group.add_Param(Param(name="HasNS",
-                    dataType="float", ucd="stat.probability", value=ProbHasNS,
+                    dataType="float", ucd="stat.probability",
+                    value=voevent.prob_has_ns,
                     Description=["Probability that at least one object in the "
                     "binary has a mass that is less than 3 solar masses"]))
 
-            if ProbHasRemnant is not None:
+            if voevent.prob_has_remnant is not None:
                 properties_group.add_Param(Param(name="HasRemnant",
                     dataType="float", ucd="stat.probability",
-                    value=ProbHasRemnant, Description=["Probability that a "
-                    "nonzero mass was ejected outside the central remnant "
-                    "object"]))
+                    value=voevent.prob_has_remnant, Description=["Probability "
+                    "that a nonzero mass was ejected outside the central "
+                    "remnant object"]))
 
             # build up MaxDistance. event.singleinspiral_set.all()?
             # Each detector calculates an effective distance assuming the inspiral is 
diff --git a/gracedb/superevents/utils.py b/gracedb/superevents/utils.py
index 63fd04589..c216b1968 100644
--- a/gracedb/superevents/utils.py
+++ b/gracedb/superevents/utils.py
@@ -595,15 +595,15 @@ def create_voevent_for_superevent(superevent, issuer, voevent_type,
 
     # Instantiate VOEvent object
     voevent = VOEvent.objects.create(superevent=superevent, issuer=issuer,
-        voevent_type=voevent_type)
+        voevent_type=voevent_type, skymap_type=skymap_type,
+        skymap_filename=skymap_filename, internal=internal,
+        open_alert=open_alert, hardware_inj=hardware_inj,
+        coinc_comment=CoincComment, prob_has_ns=ProbHasNS,
+        prob_has_remnant=ProbHasRemnant, prob_bns=BNS, prob_nsbh=NSBH,
+        prob_bbh=BBH, prob_terrestrial=Terrestrial, prob_mass_gap=MassGap)
 
     # Construct VOEvent file text
-    voevent_text, ivorn = construct_voevent_file(superevent, voevent,
-        skymap_type=skymap_type, skymap_filename=skymap_filename,
-        internal=internal, open_alert=open_alert, hardware_inj=hardware_inj,
-        CoincComment=CoincComment, ProbHasNS=ProbHasNS,
-        ProbHasRemnant=ProbHasRemnant, BNS=BNS, NSBH=NSBH, BBH=BBH,
-        Terrestrial=Terrestrial, MassGap=MassGap)
+    voevent_text, ivorn = construct_voevent_file(superevent, voevent)
 
     # Save versioned VOEvent file
     voevent_display_type = dict(VOEvent.VOEVENT_TYPE_CHOICES) \
-- 
GitLab