From 28a01c2d0c0d5c858acd8b4ffc8ba0fb5233778c Mon Sep 17 00:00:00 2001
From: Brandon Piotrzkowski <brandon.piotrzkowski@ligo.org>
Date: Fri, 26 Aug 2022 16:56:28 -0400
Subject: [PATCH] Use multi-ordered sky maps in RAVEN pipeline; fixes #408

---
 CHANGES.rst                                   |   6 +
 gwcelery/tasks/alerts.py                      |  55 ++++--
 gwcelery/tasks/external_skymaps.py            | 138 +++++++++++---
 gwcelery/tasks/external_triggers.py           |  68 ++++---
 gwcelery/tasks/orchestrator.py                |  84 ++++++++-
 .../data/gracedb_externaltrigger_log.json     |   8 +
 .../tests/data/gracedb_setrigger_log.json     |   2 +-
 gwcelery/tests/test_tasks_external_skymaps.py |  78 +++++++-
 .../tests/test_tasks_external_triggers.py     |  53 +++---
 gwcelery/tests/test_tasks_orchestrator.py     | 174 ++++++++++++------
 10 files changed, 493 insertions(+), 173 deletions(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index 110b05a41..4402ed2b1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -9,6 +9,12 @@ Changelog
 
 -   Allow alerts (using multi-order skymaps) for burst events.
 
+-   Add the ability to use multi-order GW sky maps to create combined sky maps
+    and include these in alerts. The presence of the COMBINEDSKYMAP_READY label
+    indicates the combined sky map is now available in that external event or
+    superevent. We will only copy a combined sky map to the superevent when
+    sending an alert once if the preferred external event has one available.
+
 -   Don't compute p-astro for PyCBC/AllSky because it now computes
     and uploads its own.
 
diff --git a/gwcelery/tasks/alerts.py b/gwcelery/tasks/alerts.py
index 64a3b73f4..774e6b065 100644
--- a/gwcelery/tasks/alerts.py
+++ b/gwcelery/tasks/alerts.py
@@ -11,8 +11,7 @@ from . import gracedb
 log = get_logger(__name__)
 
 
-def _create_base_alert_dict(classification, superevent, alert_type,
-                            raven_coinc=False):
+def _create_base_alert_dict(classification, superevent, alert_type):
     '''Create the base of the alert dictionary, with all contents except the
     skymap and the external coinc information.'''
     # NOTE Everything that comes through this code path will be marked as
@@ -59,9 +58,14 @@ def _create_base_alert_dict(classification, superevent, alert_type,
 
 
 @gracedb.task(shared=False)
-def _add_external_coinc_to_alert(alert_dict, superevent):
+def _add_external_coinc_to_alert(alert_dict, superevent,
+                                 combined_skymap_filename):
     external_event = gracedb.get_event(superevent['em_type'])
-
+    if combined_skymap_filename:
+        combined_skymap = gracedb.download(combined_skymap_filename,
+                                           superevent['em_type'])
+    else:
+        combined_skymap = None
     alert_dict['external_coinc'] = {
         'gcn_notice_id':
             external_event['extra_attributes']['GRB']['trigger_id'],
@@ -74,7 +78,7 @@ def _add_external_coinc_to_alert(alert_dict, superevent):
         'time_sky_position_coincidence_far': superevent['space_coinc_far']
     }
 
-    return alert_dict
+    return alert_dict, combined_skymap
 
 
 @app.task(bind=True, shared=False, queue='kafka', ignore_result=True)
@@ -101,7 +105,7 @@ def _upload_notice(self, payload, brokerhost, superevent_id):
 
 
 @app.task(bind=True, queue='kafka', shared=False)
-def _send(self, alert_dict, skymap, brokerhost):
+def _send(self, alert_dict, skymap, brokerhost, combined_skymap=None):
     """Write the alert to the Kafka topic"""
     # Copy the alert dictionary so we dont modify the original
     payload_dict = alert_dict.copy()
@@ -116,6 +120,10 @@ def _send(self, alert_dict, skymap, brokerhost):
         encoder = config['skymap_encoder']
         payload_dict['event']['skymap'] = encoder(skymap)
 
+        if combined_skymap:
+            payload_dict['external_coinc']['combined_skymap'] = \
+                encoder(combined_skymap)
+
     # Write to kafka topic
     serialization_model = \
         self.app.conf['kafka_streams'][brokerhost].serialization_model
@@ -129,9 +137,16 @@ def _send(self, alert_dict, skymap, brokerhost):
     return payload
 
 
+@app.task(bind=True, queue='kafka', shared=False)
+def _send_with_combined(self, alert_dict_combined_skymap, skymap, brokerhost):
+    alert_dict, combined_skymap = alert_dict_combined_skymap
+    return _send(alert_dict, skymap, brokerhost,
+                 combined_skymap=combined_skymap)
+
+
 @app.task(bind=True, ignore_result=True, queue='kafka', shared=False)
 def send(self, skymap_and_classification, superevent, alert_type,
-         raven_coinc=False):
+         raven_coinc=False, combined_skymap_filename=None):
     """Send an public alert to all currently connected kafka brokers.
 
     Parameters
@@ -151,7 +166,8 @@ def send(self, skymap_and_classification, superevent, alert_type,
         The alert type.
     raven_coinc: bool
         Is there a coincident external event processed by RAVEN?
-
+    combined_skymap_filename : str
+        Combined skymap filename. Default None.
     """
 
     if skymap_and_classification is not None:
@@ -163,17 +179,20 @@ def send(self, skymap_and_classification, superevent, alert_type,
     alert_dict = _create_base_alert_dict(
         classification,
         superevent,
-        alert_type,
-        raven_coinc=raven_coinc
+        alert_type
     )
 
     if raven_coinc and alert_type != 'retraction':
         canvas = (
-            _add_external_coinc_to_alert.s(alert_dict, superevent)
+            _add_external_coinc_to_alert.si(
+                alert_dict,
+                superevent,
+                combined_skymap_filename
+            )
             |
             group(
                 (
-                    _send.s(skymap, brokerhost)
+                    _send_with_combined.s(skymap, brokerhost)
                     |
                     _upload_notice.s(brokerhost, superevent['superevent_id'])
                 ) for brokerhost in self.app.conf['kafka_streams'].keys()
@@ -200,7 +219,8 @@ def _create_skymap_classification_tuple(skymap, classification):
 
 @app.task(shared=False, ignore_result=True)
 def download_skymap_and_send_alert(classification, superevent, alert_type,
-                                   skymap_filename=None, raven_coinc=False):
+                                   skymap_filename=None, raven_coinc=False,
+                                   combined_skymap_filename=None):
     """Wrapper for send function when caller has not already downloaded the
     skymap.
 
@@ -221,7 +241,8 @@ def download_skymap_and_send_alert(classification, superevent, alert_type,
         The skymap filename.
     raven_coinc: bool
         Is there a coincident external event processed by RAVEN?
-
+    combined_skymap_filename : str
+        The combined skymap filename. Default None
     """
 
     if skymap_filename is not None and alert_type != 'retraction':
@@ -233,14 +254,16 @@ def download_skymap_and_send_alert(classification, superevent, alert_type,
             |
             _create_skymap_classification_tuple.s(classification)
             |
-            send.s(superevent, alert_type, raven_coinc=raven_coinc)
+            send.s(superevent, alert_type, raven_coinc=raven_coinc,
+                   combined_skymap_filename=combined_skymap_filename)
         )
     else:
         canvas = send.s(
             (None, classification),
             superevent,
             alert_type,
-            raven_coinc=raven_coinc
+            raven_coinc=raven_coinc,
+            combined_skymap_filename=combined_skymap_filename
         )
 
     canvas.apply_async()
diff --git a/gwcelery/tasks/external_skymaps.py b/gwcelery/tasks/external_skymaps.py
index dbef49883..c8fefd441 100644
--- a/gwcelery/tasks/external_skymaps.py
+++ b/gwcelery/tasks/external_skymaps.py
@@ -1,11 +1,13 @@
 """Create and upload external sky maps."""
 from astropy import units as u
 from astropy.coordinates import ICRS, SkyCoord
+import astropy_healpix as ah
 from astropy_healpix import HEALPix, pixel_resolution_to_nside
 from celery import group
 #  import astropy.utils.data
 import numpy as np
 from ligo.skymap.io import fits
+from ligo.skymap.distance import parameters_to_marginal_moments
 from ligo.skymap.tool import ligo_skymap_combine
 import gcn
 import healpy as hp
@@ -23,42 +25,51 @@ from ..util.tempfile import NamedTemporaryFile
 from ..import _version
 
 
+@app.task(shared=False,
+          queue='exttrig')
 def create_combined_skymap(se_id, ext_id):
-    """Creates and uploads the combined LVC-Fermi skymap.
-
-    This also uploads the external trigger skymap to the external trigger
-    GraceDB page.
+    """Creates and uploads the combined LVC-Fermi skymap, uploading to the
+    external trigger GraceDB page.
     """
     se_skymap_filename = get_skymap_filename(se_id)
     ext_skymap_filename = get_skymap_filename(ext_id)
-    new_skymap_filename = re.findall(r'(.*).fits.gz', se_skymap_filename)[0]
+    # Determine whether GW sky map is multiordered or flat
+    gw_moc = '.multiorder.fits' in se_skymap_filename
+
+    new_filename = \
+        ('combined-ext.multiorder.fits' if gw_moc else 'combined-ext.fits.gz')
 
-    #  FIXME: put download functions in canvas
-    se_skymap = gracedb.download(se_skymap_filename, se_id)
-    ext_skymap = gracedb.download(ext_skymap_filename, ext_id)
     message = 'Combined LVC-external sky map using {0} and {1}'.format(
         se_skymap_filename, ext_skymap_filename)
     message_png = (
         'Mollweide projection of <a href="/api/events/{graceid}/files/'
         '{filename}">{filename}</a>').format(
-            graceid=se_id, filename=new_skymap_filename + '-ext.fits.gz')
+            graceid=ext_id,
+            filename=new_filename)
 
     (
-        combine_skymaps.si(se_skymap, ext_skymap)
+        _download_skymaps.si(
+            se_skymap_filename, ext_skymap_filename, se_id, ext_id
+        )
+        |
+        combine_skymaps.s(gw_moc=gw_moc)
         |
         group(
-            gracedb.upload.s(new_skymap_filename + '-ext.fits.gz', se_id,
-                             message, ['sky_loc', 'public']),
+            gracedb.upload.s(new_filename, ext_id,
+                             message, ['sky_loc', 'ext_coinc']),
 
             skymaps.plot_allsky.s()
             |
-            gracedb.upload.s(new_skymap_filename + '-ext.png', se_id,
-                             message_png, ['sky_loc', 'ext_coinc', 'public'])
+            gracedb.upload.s('combined-ext.png', ext_id,
+                             message_png, ['sky_loc', 'ext_coinc'])
         )
+        |
+        gracedb.create_label.si('COMBINEDSKYMAP_READY', ext_id)
     ).delay()
 
 
 @app.task(autoretry_for=(ValueError,), retry_backoff=10,
+          queue='exttrig',
           retry_backoff_max=600)
 def get_skymap_filename(graceid):
     """Get the skymap fits filename.
@@ -68,31 +79,106 @@ def get_skymap_filename(graceid):
     """
     gracedb_log = gracedb.get_log(graceid)
     if 'S' in graceid:
+        # Try first to get a multiordered sky map
         for message in reversed(gracedb_log):
             filename = message['filename']
-            if filename.endswith('.multiorder.fits'):
+            if filename.endswith('.multiorder.fits') and \
+                    "combined-ext." not in filename:
+                return filename
+        # Try next to get a flattened sky map
+        for message in reversed(gracedb_log):
+            filename = message['filename']
+            if filename.endswith('.fits.gz') and \
+                    "combined-ext." not in filename:
                 return filename
     else:
         for message in reversed(gracedb_log):
             filename = message['filename']
             if (filename.endswith('.fits') or filename.endswith('.fit') or
-                    filename.endswith('.fits.gz')):
+                    filename.endswith('.fits.gz')) and \
+                    "combined-ext." not in filename:
                 return filename
     raise ValueError('No skymap available for {0} yet.'.format(graceid))
 
 
-@app.task(shared=False)
-def combine_skymaps(skymap1filebytes, skymap2filebytes):
+@app.task(shared=False, queue='exttrig')
+def _download_skymaps(se_filename, ext_filename, se_id, ext_id):
+    """Download both superevent and external sky map to be combined."""
+    se_skymap = gracedb.download(se_filename, se_id)
+    ext_skymap = gracedb.download(ext_filename, ext_id)
+    return se_skymap, ext_skymap
+
+
+def combine_skymaps_moc_flat(gw_sky, ext_sky, ext_header):
+    """This function combines a multiordered (MOC) GW sky map with a flattened
+    external one by reweighting the MOC sky map using the values of the
+    flattened one.
+
+    Header info is generally inherited from the GW sky map or recalculated
+    using the combined sky map values.
+    """
+    #  Find ra/dec of each GW pixel
+    level, ipix = ah.uniq_to_level_ipix(gw_sky["UNIQ"])
+    nsides = ah.level_to_nside(level)
+    areas = ah.nside_to_pixel_area(nsides)
+    ra_gw, dec_gw = ah.healpix_to_lonlat(ipix, nsides, order='nested')
+    #  Find corresponding external sky map indicies
+    ext_nside = ah.npix_to_nside(len(ext_sky))
+    ext_ind = ah.lonlat_to_healpix(
+        ra_gw, dec_gw, ext_nside,
+        order='nested' if ext_header['nest'] else 'ring')
+    #  Reweight GW prob density by external sky map probabilities
+    gw_sky['PROBDENSITY'] *= ext_sky[ext_ind]
+    gw_sky['PROBDENSITY'] /= \
+        np.sum(gw_sky['PROBDENSITY'] * areas).value
+    #  Modify GW sky map with new data
+    distmean, diststd = parameters_to_marginal_moments(
+        gw_sky['PROBDENSITY'] * areas.value,
+        gw_sky['DISTMU'], gw_sky['DISTSIGMA'])
+    gw_sky.meta['distmean'], gw_sky.meta['diststd'] = distmean, diststd
+    gw_sky.meta['instruments'].update(ext_header['instruments'])
+    gw_sky.meta['HISTORY'].extend([
+        '', 'The values were reweighted by using data from {}'.format(
+            list(ext_header['instruments'])[0])])
+    return gw_sky
+
+
+@app.task(shared=False, queue='exttrig')
+def combine_skymaps(skymapsbytes, gw_moc=True):
     """This task combines the two input skymaps, in this case the external
     trigger skymap and the LVC skymap and writes to a temporary output file. It
     then returns the contents of the file as a byte array.
+
+    There are separate methods in case the GW sky map is multiordered (we just
+    reweight using the external sky map) or flattened (use standard
+    ligo.skymap.combine method).
     """
-    with NamedTemporaryFile(mode='rb', suffix='.fits.gz') as combinedskymap, \
-            NamedTemporaryFile(content=skymap1filebytes) as skymap1file, \
-            NamedTemporaryFile(content=skymap2filebytes) as skymap2file, \
+    gw_skymap_bytes, ext_skymap_bytes = skymapsbytes
+    suffix = ".fits" if gw_moc else ".fits.gz"
+    with NamedTemporaryFile(mode='rb', suffix=suffix) as combinedskymap, \
+            NamedTemporaryFile(content=gw_skymap_bytes) as gw_skymap_file, \
+            NamedTemporaryFile(content=ext_skymap_bytes) as ext_skymap_file, \
             handling_system_exit():
-        ligo_skymap_combine.main([skymap1file.name,
-                                  skymap2file.name, combinedskymap.name])
+
+        # If GW sky map is multiordered, use reweighting method
+        if gw_moc:
+            #  FIXME: Use method that regrids the combined sky map e.g. mhealpy
+            #  once this method is quicker and preserves the header
+            #  Load sky maps
+            gw_skymap = fits.read_sky_map(gw_skymap_file.name, moc=True)
+            ext_skymap, ext_header = fits.read_sky_map(ext_skymap_file.name,
+                                                       moc=False)
+            #  Create and write combined sky map
+            combined_skymap = combine_skymaps_moc_flat(gw_skymap, ext_skymap,
+                                                       ext_header)
+            fits.write_sky_map(combinedskymap.name, combined_skymap, moc=True)
+        # If GW sky map is flattened, use older method
+        else:
+            ligo_skymap_combine.main([gw_skymap_file.name,
+                                      ext_skymap_file.name,
+                                      combinedskymap.name])
+        #  FIXME: Add method for MOC-MOC if there is a switch to MOC external
+        #  skymaps
         return combinedskymap.read()
 
 
@@ -107,7 +193,7 @@ def external_trigger(graceid):
     raise ValueError('No associated GRB EM event(s) for {0}.'.format(graceid))
 
 
-@app.task(shared=False)
+@app.task(shared=False, queue='exttrig')
 def external_trigger_heasarc(external_id):
     """Returns the HEASARC fits file link."""
     gracedb_log = gracedb.get_log(external_id)
@@ -125,6 +211,7 @@ def external_trigger_heasarc(external_id):
 
 
 @app.task(autoretry_for=(urllib.error.HTTPError,), retry_backoff=10,
+          queue='exttrig',
           retry_backoff_max=600)
 def get_external_skymap(link, search):
     """Download the Fermi sky map fits file and return the contents as a byte
@@ -152,6 +239,7 @@ def get_external_skymap(link, search):
 
 
 @app.task(autoretry_for=(urllib.error.HTTPError, urllib.error.URLError,),
+          queue='exttrig',
           retry_backoff=10, retry_backoff_max=1200)
 def get_upload_external_skymap(event, skymap_link=None):
     """If a Fermi sky map is not uploaded yet, tries to download one and upload
@@ -339,7 +427,7 @@ def write_to_fits(skymap, event, notice_type, notice_date):
             return file.read()
 
 
-@app.task(shared=False)
+@app.task(shared=False, queue='exttrig')
 def create_upload_external_skymap(event, notice_type, notice_date):
     """Create and upload external sky map using
     RA, dec, and error radius information.
diff --git a/gwcelery/tasks/external_triggers.py b/gwcelery/tasks/external_triggers.py
index 24bd76a0e..0aa3d4a5e 100644
--- a/gwcelery/tasks/external_triggers.py
+++ b/gwcelery/tasks/external_triggers.py
@@ -17,7 +17,6 @@ log = get_logger(__name__)
 
 REQUIRED_LABELS_BY_TASK = {
     'compare': {'SKYMAP_READY', 'EXT_SKYMAP_READY', 'EM_COINC'},
-    'combine': {'SKYMAP_READY', 'EXT_SKYMAP_READY', 'RAVEN_ALERT'},
     'SoG': {'SKYMAP_READY', 'RAVEN_ALERT', 'ADVOK'}
 }
 """These labels should be present on an external event to consider it to
@@ -233,9 +232,7 @@ def handle_grb_igwn_alert(alert):
     * When both a GW and GRB sky map are available during a coincidence,
       indicated by the labels ``SKYMAP_READY`` and ``EXT_SKYMAP_READY``
       respectively on the external event, this triggers the spacetime coinc
-      FAR to be calculated. If an alert is triggered with these same
-      conditions, indicated by the ``RAVEN_ALERT`` label, a combined GW-GRB
-      sky map is created using
+      FAR to be calculated and a combined GW-GRB sky map is created using
       :meth:`gwcelery.tasks.external_skymaps.create_combined_skymap`.
 
     """
@@ -299,30 +296,35 @@ def handle_grb_igwn_alert(alert):
             raven.coincidence_search(graceid, alert['object'],
                                      group=gw_group, searches=['GRB'],
                                      se_searches=se_searches)
-    # rerun raven pipeline or created combined sky map when sky maps are
-    # available; trigger SoG manuscript once voevent is created
+    # rerun raven pipeline and create combined sky map (if not a Swift event)
+    # when sky maps are available
     elif alert['alert_type'] == 'label_added' and \
             alert['object'].get('group') == 'External':
         if _skymaps_are_ready(alert['object'], alert['data']['name'],
                               'compare'):
-            # if both sky maps present and a coincidence, compare sky maps
-            superevent_id, ext_ids = _get_superevent_ext_ids(
-                graceid, alert['object'], 'compare')
+            # if both sky maps present and a coincidence, rreun RAVEN
+            # pipeline and create combined sky maps
+            event = alert['object']
+            superevent_id, ext_id = _get_superevent_ext_ids(
+                graceid, event)
             superevent = gracedb.get_superevent(superevent_id)
-            preferred_event_id = superevent['preferred_event']
-            gw_group = gracedb.get_group(preferred_event_id)
+            gw_group = superevent['preferred_event_data']['group']
             tl, th = raven._time_window(graceid, gw_group,
-                                        [alert['object']['pipeline']],
-                                        [alert['object']['search']])
-            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
-            superevent_id, ext_id = _get_superevent_ext_ids(
-                graceid, alert['object'], 'combine')
-            external_skymaps.create_combined_skymap(superevent_id, ext_id)
+                                        [event['pipeline']],
+                                        [event['search']])
+            # FIXME: both overlap integral and combined sky map could be
+            # done by the same function since they are so similar
+            group_canvas = ()
+            group_canvas += raven.raven_pipeline.si(
+                                [event], superevent_id,
+                                superevent, tl, th, gw_group),
+            # Swift localizations are incredibly well localized and require
+            # a different method from Fermi/Integral/AGILE
+            # FIXME: Add Swift localization information in the future
+            if event['pipeline'] != 'Swift':
+                group_canvas += external_skymaps.create_combined_skymap.si(
+                                    superevent_id, ext_id),
+            group(group_canvas).delay()
         elif 'EM_COINC' in alert['object']['labels']:
             # if not complete, check if GW sky map; apply label to external
             # event if GW sky map
@@ -406,21 +408,13 @@ def _skymaps_are_ready(event, label, task):
     return required_labels.issubset(label_set) and label in required_labels
 
 
-def _get_superevent_ext_ids(graceid, event, task):
-    if task in {'combine', 'SoG'}:
-        if 'S' in graceid:
-            se_id = event['superevent_id']
-            ext_id = event['em_type']
-        else:
-            se_id = event['superevent']
-            ext_id = event['graceid']
-    elif task == 'compare':
-        if 'S' in graceid:
-            se_id = event['superevent_id']
-            ext_id = event['em_events']
-        else:
-            se_id = event['superevent']
-            ext_id = [event['graceid']]
+def _get_superevent_ext_ids(graceid, event):
+    if 'S' in graceid:
+        se_id = event['superevent_id']
+        ext_id = event['em_type']
+    else:
+        se_id = event['superevent']
+        ext_id = event['graceid']
     return se_id, ext_id
 
 
diff --git a/gwcelery/tasks/orchestrator.py b/gwcelery/tasks/orchestrator.py
index 965395dbb..4d1515856 100644
--- a/gwcelery/tasks/orchestrator.py
+++ b/gwcelery/tasks/orchestrator.py
@@ -393,6 +393,7 @@ def _create_voevent(classification, *args, **kwargs):
     if classification is not None:
         # Merge source classification and source properties into kwargs.
         for text in classification:
+            # Ignore filenames, only load dict in bytes form
             if text is not None:
                 kwargs.update(json.loads(text))
 
@@ -524,9 +525,12 @@ def _unpack_args_and_send_earlywarning_preliminary_alert(input_list, alert):
     [skymap, skymap_filename], [em_bright, em_bright_filename], \
         [p_astro, p_astro_filename] = input_list
 
+    # Update to latest state after downloading files
+    superevent = gracedb.get_superevent(alert['object']['superevent_id'])
+
     earlywarning_preliminary_initial_update_alert.delay(
         [skymap_filename, em_bright_filename, p_astro_filename],
-        alert['object'],
+        superevent,
         ('earlywarning' if superevents.EARLY_WARNING_LABEL in
          alert['object']['labels'] else 'preliminary'),
         filecontents=[skymap, em_bright, p_astro]
@@ -768,10 +772,19 @@ def earlywarning_preliminary_initial_update_alert(
         assert alert_type == 'earlywarning' or alert_type == 'preliminary'
 
     skymap_filename, em_bright_filename, p_astro_filename = filenames
+    combined_skymap_filename = None
+    combined_skymap_needed = False
     skymap_needed = (skymap_filename is None)
     em_bright_needed = (em_bright_filename is None)
     p_astro_needed = (p_astro_filename is None)
-    if skymap_needed or em_bright_needed or p_astro_needed:
+    raven_coinc = ('RAVEN_ALERT' in labels and bool(superevent['em_type']))
+    if raven_coinc:
+        ext_labels = gracedb.get_labels(superevent['em_type'])
+        combined_skymap_needed = \
+            {"RAVEN_ALERT", "COMBINEDSKYMAP_READY"}.issubset(set(ext_labels))
+
+    if skymap_needed or em_bright_needed or p_astro_needed or \
+            combined_skymap_needed:
         for message in gracedb.get_log(superevent_id):
             t = message['tag_names']
             f = message['filename']
@@ -781,7 +794,8 @@ def earlywarning_preliminary_initial_update_alert(
                 continue
             if skymap_needed \
                     and {'sky_loc', 'public'}.issubset(t) \
-                    and f.endswith('.multiorder.fits'):
+                    and f.endswith('.multiorder.fits') \
+                    and 'combined' not in f:
                 skymap_filename = fv
             if em_bright_needed \
                     and 'em_bright' in t \
@@ -791,9 +805,55 @@ def earlywarning_preliminary_initial_update_alert(
                     and 'p_astro' in t \
                     and f.endswith('.json'):
                 p_astro_filename = fv
+            if combined_skymap_needed \
+                    and {'sky_loc', 'ext_coinc'}.issubset(t) \
+                    and f.startswith('combined-ext.') \
+                    and 'fit' in f:
+                combined_skymap_filename = fv
+                # only download first sky map and prevent more downloads
+                # FIXME: remove if there exists a system to recalculate
+                # combined sky maps later
+                combined_skymap_needed = False
+
+    if combined_skymap_needed:
+        # if no combined sky map present and needed, download sky map from
+        # external event
+        # FIXME: use file inheritance once available
+        ext_id = superevent['em_type']
+        combined_skymap_filename = \
+            ('combined-ext.multiorder.fits' if '.multiorder.fits' in
+             skymap_filename else 'combined-ext.fits.gz')
+        message = 'Combined LVC-external sky map using {0} and {1}'.format(
+            superevent_id, ext_id)
+        message_png = (
+            'Mollweide projection of <a href="/api/events/{se_id}/files/'
+            '{filename}">{filename}</a>, using {se_id} and {ext_id}').format(
+               se_id=superevent_id,
+               ext_id=ext_id,
+               filename=combined_skymap_filename)
+
+        combined_skymap_canvas = group(
+            gracedb.download.si(combined_skymap_filename, ext_id)
+            |
+            gracedb.upload.s(
+                combined_skymap_filename, superevent_id,
+                message, ['sky_loc', 'ext_coinc', 'public'])
+            |
+            gracedb.create_label.si('COMBINEDSKYMAP_READY', superevent_id),
+
+            gracedb.download.si('combined-ext.png', ext_id)
+            |
+            gracedb.upload.s(
+                'combined-ext.png', superevent_id,
+                message_png, ['sky_loc', 'ext_coinc', 'public']
+            )
+            |
+            # Pass None to download_anor_expose group
+            identity.si()
+        )
 
     if alert_type in {'earlywarning', 'preliminary', 'initial'}:
-        if 'RAVEN_ALERT' in labels:
+        if raven_coinc:
             circular_task = circulars.create_emcoinc_circular.si(superevent_id)
             circular_filename = '{}-emcoinc-circular.txt'.format(alert_type)
             tags = ['em_follow', 'ext_coinc']
@@ -816,7 +876,7 @@ def earlywarning_preliminary_initial_update_alert(
     else:
         circular_canvas = identity.si()
 
-    if filecontents:
+    if filecontents and not combined_skymap_filename:
         skymap, em_bright, p_astro = filecontents
 
         download_andor_expose_group = []
@@ -828,14 +888,15 @@ def earlywarning_preliminary_initial_update_alert(
             skymap_filename=skymap_filename,
             internal=False,
             open_alert=True,
-            raven_coinc=('RAVEN_ALERT' in labels)
+            raven_coinc=raven_coinc,
+            combined_skymap_filename=combined_skymap_filename
         )
 
         kafka_alert_canvas = alerts.send.si(
             (skymap, em_bright, p_astro),
             superevent,
             alert_type,
-            raven_coinc=('RAVEN_ALERT' in labels)
+            raven_coinc=raven_coinc
         )
     else:
         # Download em_bright and p_astro files here for voevent
@@ -850,7 +911,8 @@ def earlywarning_preliminary_initial_update_alert(
             skymap_filename=skymap_filename,
             internal=False,
             open_alert=True,
-            raven_coinc=('RAVEN_ALERT' in labels)
+            raven_coinc=raven_coinc,
+            combined_skymap_filename=combined_skymap_filename
         )
 
         # The skymap has not been downloaded at this point, so we need to
@@ -859,7 +921,8 @@ def earlywarning_preliminary_initial_update_alert(
             superevent,
             alert_type,
             skymap_filename=skymap_filename,
-            raven_coinc=('RAVEN_ALERT' in labels)
+            raven_coinc=raven_coinc,
+            combined_skymap_filename=combined_skymap_filename
         )
 
     download_andor_expose_group += [
@@ -881,6 +944,9 @@ def earlywarning_preliminary_initial_update_alert(
         gracedb.create_tag.s('public', superevent_id)
     )
 
+    if combined_skymap_needed:
+        download_andor_expose_group += [combined_skymap_canvas]
+
     canvas = (
         group(download_andor_expose_group)
         |
diff --git a/gwcelery/tests/data/gracedb_externaltrigger_log.json b/gwcelery/tests/data/gracedb_externaltrigger_log.json
index 5990e53ed..e92d195b6 100644
--- a/gwcelery/tests/data/gracedb_externaltrigger_log.json
+++ b/gwcelery/tests/data/gracedb_externaltrigger_log.json
@@ -14,5 +14,13 @@
   "filename": "nasa.gsfc.gcn_Fermi#GBM_Gnd_Pos_2017-08-17T12:41:06.47_524666471_57-431.xml",
   "tags": "https://gracedb.ligo.org/api/events/E12345/log/1/tag/",
   "N": 1
+ },
+ {
+  "comment": "Sky map created from GCN RA, dec, and error.",
+  "file_version": 0,
+  "file": "https://gracedb.ligo.org/api/events/E12345/files/fermi_skymap.fits.gz,0",
+  "filename": "fermi_skymap.fits.gz",
+  "tags": "https://gracedb.ligo.org/api/events/E12345/log/2/tag/",
+  "N": 2
  }
 ]
diff --git a/gwcelery/tests/data/gracedb_setrigger_log.json b/gwcelery/tests/data/gracedb_setrigger_log.json
index 66149c1b0..1a4c33fac 100644
--- a/gwcelery/tests/data/gracedb_setrigger_log.json
+++ b/gwcelery/tests/data/gracedb_setrigger_log.json
@@ -8,6 +8,6 @@
     "filename": "bayestar.multiorder.fits",
     "file_version": 0,
     "tag_names": ["sky_loc", "public"],
-    "file": "https://gracedb-dev1.ligo.org/api/superevents/S170817/files/bayestar.fits.gz,0"
+    "file": "https://gracedb-dev1.ligo.org/api/superevents/S170817/files/bayestar.multiorder.fits,0"
   }
 ]
diff --git a/gwcelery/tests/test_tasks_external_skymaps.py b/gwcelery/tests/test_tasks_external_skymaps.py
index 0ebe2d013..27a5dd833 100644
--- a/gwcelery/tests/test_tasks_external_skymaps.py
+++ b/gwcelery/tests/test_tasks_external_skymaps.py
@@ -1,6 +1,7 @@
 from importlib import resources
 from unittest.mock import patch
 
+from astropy.table import Table
 import numpy as np
 import pytest
 
@@ -61,6 +62,23 @@ def mock_get_file_contents(monkeypatch, toy_fits_filecontents):  # noqa: F811
         'astropy.utils.data.get_file_contents', get_file_contents)
 
 
+def get_gw_moc_skymap():
+    array = [np.arange(12, dtype=np.float64)] * 5
+    #  Modify UNIQ table to be allowable values
+    array[4] = array[4] + 4
+    table = Table(
+        array,
+        names=['PROBDENSITY', 'DISTMU', 'DISTSIGMA', 'DISTNORM', 'UNIQ'])
+    table.meta['comment'] = 'This is a comment.'
+    table.meta['HISTORY'] = \
+        ['This is a history line. <This should be escaped.>']
+    table.meta['OBJECT'] = 'T12345'
+    table.meta['LOGBCI'] = 3.5
+    table.meta['ORDERING'] = 'NESTED'
+    table.meta['instruments'] = {'L1', 'H1', 'V1'}
+    return table
+
+
 @patch('gwcelery.tasks.skymaps.plot_allsky.run')
 @patch('gwcelery.tasks.gracedb.upload.run')
 @patch('gwcelery.tasks.external_skymaps.combine_skymaps.run')
@@ -78,10 +96,64 @@ def test_create_combined_skymap(mock_get_skymap_filename,
     mock_upload.assert_called()
 
 
-@patch('gwcelery.tasks.gracedb.get_log', mock_get_log)
-def test_get_skymap_filename():
+def _mock_read_sky_map(filename, moc=True):
+    if moc:
+        return get_gw_moc_skymap()
+    else:
+        ext_sky = np.full(12, 1 / 12)
+        ext_header = {'instruments': set({'Fermi'}), 'nest': True}
+        return ext_sky, ext_header
+
+
+@pytest.mark.parametrize('gw_moc',
+                         [True, False])
+@patch('ligo.skymap.tool.ligo_skymap_combine.main')
+@patch('gwcelery.tasks.external_skymaps.combine_skymaps_moc_flat')
+@patch('ligo.skymap.io.fits.read_sky_map', side_effect=_mock_read_sky_map)
+@patch('ligo.skymap.io.fits.write_sky_map')
+def test_combine_skymaps(mock_write_sky_map,
+                         mock_read_sky_map,
+                         mock_skymap_combine_moc_flat,
+                         mock_skymap_combine_flat_flat,
+                         gw_moc):
+    """Test using our internal MOC-flat sky map combination gives back the
+    input using a uniform sky map, ensuring the test is giving a sane result
+    and is at least running to completion.
+    """
+    external_skymaps.combine_skymaps((b'', b''), gw_moc=gw_moc)
+    if gw_moc:
+        mock_read_sky_map.assert_called()
+        mock_skymap_combine_moc_flat.assert_called_once()
+        mock_write_sky_map.assert_called_once()
+    else:
+        mock_skymap_combine_flat_flat.assert_called()
+
+
+def test_create_combined_skymap_moc_flat():
+    """Test using our internal MOC-flat sky map combination gives back the
+    input using a uniform sky map, ensuring the test is giving a sane result
+    and is at least running to completion.
+    """
+    # Run function under test
+    gw_sky = get_gw_moc_skymap()
+    ext_sky = np.full(12, 1 / 12)
+    ext_header = {'instruments': set({'Fermi'}), 'nest': True}
+    combined_sky = external_skymaps.combine_skymaps_moc_flat(gw_sky, ext_sky,
+                                                             ext_header)
+    assert all(combined_sky['PROBDENSITY'] == gw_sky['PROBDENSITY'])
+    assert 'Fermi' in combined_sky.meta['instruments']
+
+
+@pytest.mark.parametrize('graceid',
+                         ['S12345', 'E12345'])
+@patch('gwcelery.tasks.gracedb.get_log', side_effect=mock_get_log)
+def test_get_skymap_filename(mock_get_logs, graceid):
     """Test getting the LVC skymap fits filename"""
-    external_skymaps.get_skymap_filename('S12345')
+    filename = external_skymaps.get_skymap_filename(graceid)
+    if 'S' in graceid:
+        assert filename == 'bayestar.multiorder.fits'
+    elif 'E' in graceid:
+        assert filename == 'fermi_skymap.fits.gz'
 
 
 @patch('gwcelery.tasks.gracedb.get_event', mock_get_event)
diff --git a/gwcelery/tests/test_tasks_external_triggers.py b/gwcelery/tests/test_tasks_external_triggers.py
index 4637e12e4..a4cd9612b 100644
--- a/gwcelery/tests/test_tasks_external_triggers.py
+++ b/gwcelery/tests/test_tasks_external_triggers.py
@@ -260,20 +260,26 @@ def test_handle_create_skymap_label_from_superevent(mock_create_label,
     mock_create_label.assert_called_once_with('SKYMAP_READY', 'E1212')
 
 
-@patch('gwcelery.tasks.gracedb.get_group', return_value='CBC')
-@patch('gwcelery.tasks.raven.raven_pipeline')
+@pytest.mark.parametrize('pipeline',
+                         ['Fermi', 'Swift'])
+@patch('gwcelery.tasks.raven.raven_pipeline.run')
+@patch('gwcelery.tasks.external_skymaps.create_combined_skymap.run')
 @patch('gwcelery.tasks.gracedb.get_superevent',
        return_value={
            'superevent_id': 'S1234',
-           'preferred_event': 'G1234'
-                    })
-@patch('gwcelery.tasks.gracedb.get_event',
-       return_value={
-           'graceid': 'G1234',
-           'group': 'CBC'
+           'preferred_event': 'G1234',
+           'preferred_event_data':
+               {'group': 'CBC'}
                     })
-def test_handle_skymap_comparison(mock_get_event, mock_get_superevent,
-                                  mock_raven_pipeline, mock_get_group):
+def test_handle_skymaps_ready(mock_get_superevent,
+                              mock_create_combined_skymap,
+                              mock_raven_pipeline,
+                              pipeline):
+    """This test makes sure that once sky maps are available for a coincidence
+    that the RAVEN pipeline is rerun to calculate the joint FAR with sky map
+    information and check publishing conditions, as well as creating a
+    combined sky map.
+    """
     alert = {"uid": "E1212",
              "alert_type": "label_added",
              "data": {"name": "SKYMAP_READY"},
@@ -282,15 +288,22 @@ def test_handle_skymap_comparison(mock_get_event, mock_get_superevent,
                  "group": "External",
                  "labels": ["EM_COINC", "EXT_SKYMAP_READY", "SKYMAP_READY"],
                  "superevent": "S1234",
-                 "pipeline": "Fermi",
+                 "pipeline": pipeline,
                  "search": "GRB"
                        }
              }
     external_triggers.handle_grb_igwn_alert(alert)
     mock_raven_pipeline.assert_called_once_with([alert['object']], 'S1234',
                                                 {'superevent_id': 'S1234',
-                                                 'preferred_event': 'G1234'},
+                                                 'preferred_event': 'G1234',
+                                                 'preferred_event_data':
+                                                     {'group': 'CBC'}},
                                                 -5, 1, 'CBC')
+    if pipeline != 'Swift':
+        mock_create_combined_skymap.assert_called_once_with(
+            'S1234', 'E1212')
+    else:
+        mock_create_combined_skymap.assert_not_called()
 
 
 @patch('gwcelery.tasks.raven.trigger_raven_alert')
@@ -332,22 +345,6 @@ def test_handle_label_removed(mock_get_superevent,
     )
 
 
-@patch('gwcelery.tasks.external_skymaps.create_combined_skymap')
-def test_handle_skymap_combine(mock_create_combined_skymap):
-    alert = {"uid": "E1212",
-             "alert_type": "label_added",
-             "data": {"name": "RAVEN_ALERT"},
-             "object": {
-                 "graceid": "E1212",
-                 "group": "External",
-                 "labels": ["EM_COINC", "EXT_SKYMAP_READY", "SKYMAP_READY",
-                            "RAVEN_ALERT"],
-                 "superevent": "S1234"}
-             }
-    external_triggers.handle_grb_igwn_alert(alert)
-    mock_create_combined_skymap.assert_called_once_with('S1234', 'E1212')
-
-
 @pytest.mark.parametrize('labels',
                          [["EM_COINC", "SKYMAP_READY", "RAVEN_ALERT", "ADVOK"],
                           ["EM_COINC", "SKYMAP_READY", "ADVOK"]])
diff --git a/gwcelery/tests/test_tasks_orchestrator.py b/gwcelery/tests/test_tasks_orchestrator.py
index cfa8697bb..ae7dbad4c 100644
--- a/gwcelery/tests/test_tasks_orchestrator.py
+++ b/gwcelery/tests/test_tasks_orchestrator.py
@@ -13,33 +13,41 @@ from . import data
 
 
 @pytest.mark.parametrize(  # noqa: F811
-    'alert_type,label,group,pipeline,offline,far,instruments',
+    ('alert_type,label,group,pipeline,offline,far,instruments,superevent_id,' +
+     'superevent_labels'),
     [['label_added', 'EM_Selected', 'CBC', 'gstlal', False, 1.e-9,
-        ['H1']],
+        ['H1'], 'S1234', ['EM_Selected']],
      ['label_added', 'EM_Selected', 'CBC', 'gstlal', False, 1.e-9,
-         ['H1', 'L1']],
+         ['H1', 'L1'], 'S1234', ['EM_Selected']],
      ['label_added', 'EM_Selected', 'CBC', 'gstlal', False, 1.e-9,
-         ['H1', 'L1', 'V1']],
+         ['H1', 'L1', 'V1'], 'S1234', ['EM_Selected']],
+     ['label_added', 'EM_Selected', 'CBC', 'gstlal', False, 1.e-9,
+         ['H1', 'L1', 'V1'], 'S2468',
+         ['EM_Selected', 'COMBINEDSKYMAP_READY', 'RAVEN_ALERT', 'EM_COINC']],
      ['label_added', 'EM_Selected', 'Burst', 'CWB', False, 1.e-9,
-         ['H1', 'L1', 'V1']],
+         ['H1', 'L1', 'V1'], 'S1234', ['EM_Selected']],
      ['label_added', 'EM_Selected', 'Burst', 'oLIB', False, 1.e-9,
-         ['H1', 'L1', 'V1']],
+         ['H1', 'L1', 'V1'], 'S1234', ['EM_Selected']],
      ['label_added', 'GCN_PRELIM_SENT', 'CBC', 'gstlal', False, 1.e-9,
-         ['H1', 'L1', 'V1']],
-     ['new', '', 'CBC', 'gstlal', False, 1.e-9, ['H1', 'L1']]])
+         ['H1', 'L1', 'V1'], 'S1234', ['EM_Selected']],
+     ['new', '', 'CBC', 'gstlal', False, 1.e-9, ['H1', 'L1'], 'S1234',
+         ['EM_Selected']]])
 def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
                            alert_type, label, group, pipeline,
-                           offline, far, instruments):
+                           offline, far, instruments, superevent_id,
+                           superevent_labels):
     """Test a superevent is dispatched to the correct annotation task based on
     its preferred event's search group.
     """
     def get_superevent(superevent_id):
-        assert superevent_id == 'S1234'
         return {
             'preferred_event': 'G1234',
             'gw_events': ['G1234'],
             'preferred_event_data': get_event('G1234'),
-            'category': "Production"
+            'category': "Production",
+            'labels': superevent_labels,
+            'superevent_id': superevent_id,
+            'em_type': None if superevent_id == 'S1234' else 'E1234'
         }
 
     def get_event(graceid):
@@ -68,6 +76,9 @@ def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
             event['instruments'] = ','.join(instruments)
         return event
 
+    def get_labels(graceid):
+        return ['COMBINEDSKYMAP_READY']
+
     def download(filename, graceid):
         if '.fits' in filename:
             return toy_3d_fits_filecontents
@@ -86,9 +97,9 @@ def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
 
     alert = {
         'alert_type': alert_type,
-        'uid': 'S1234',
+        'uid': superevent_id,
         'object': {
-            'superevent_id': 'S1234',
+            'superevent_id': superevent_id,
             't_start': 1214714160,
             't_0': 1214714162,
             't_end': 1214714164,
@@ -105,7 +116,13 @@ def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
     else:
         alert['data'] = {'name': label}
 
+    if superevent_id == 'S1234':
+        raven_coinc = False
+    else:
+        raven_coinc = True
+
     create_initial_circular = Mock()
+    create_emcoinc_circular = Mock()
     expose = Mock()
     annotate_fits = Mock(return_value=None)
     proceed_if_no_advocate_action = Mock(
@@ -145,6 +162,7 @@ def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
     monkeypatch.setattr('gwcelery.tasks.gracedb.expose._orig_run', expose)
     monkeypatch.setattr('gwcelery.tasks.gracedb.get_event._orig_run',
                         get_event)
+    monkeypatch.setattr('gwcelery.tasks.gracedb.get_labels', get_labels)
     # FIXME: should test gracedb.create_voevent instead
     monkeypatch.setattr('gwcelery.tasks.orchestrator._create_voevent.run',
                         create_voevent)
@@ -152,6 +170,8 @@ def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
                         get_superevent)
     monkeypatch.setattr('gwcelery.tasks.circulars.create_initial_circular.run',
                         create_initial_circular)
+    monkeypatch.setattr('gwcelery.tasks.circulars.create_emcoinc_circular.run',
+                        create_emcoinc_circular)
     monkeypatch.setattr('gwcelery.tasks.inference.query_data.run',
                         query_data)
     monkeypatch.setattr('gwcelery.tasks.inference._setup_dag_for_bilby.run',
@@ -178,7 +198,7 @@ def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
     if label == 'GCN_PRELIM_SENT':
         dqr_request_label = list(
             filter(
-                lambda x: x.args == ('DQR_REQUEST', 'S1234'),
+                lambda x: x.args == ('DQR_REQUEST', superevent_id),
                 create_label.call_args_list
             )
         )
@@ -189,12 +209,12 @@ def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
         annotate_fits.assert_called_once()
         _event_info = get_event('G1234')  # this gets the preferred event info
         assert superevents.should_publish(_event_info)
-        expose.assert_called_once_with('S1234')
+        expose.assert_called_once_with(superevent_id)
         create_tag.assert_has_calls(
-            [call('S1234-1-Preliminary.xml', 'public', 'S1234'),
-             call('em-bright-filename', 'public', 'S1234'),
-             call('p-astro-filename', 'public', 'S1234'),
-             call('skymap-filename', 'public', 'S1234')],
+            [call('S1234-1-Preliminary.xml', 'public', superevent_id),
+             call('em-bright-filename', 'public', superevent_id),
+             call('p-astro-filename', 'public', superevent_id),
+             call('skymap-filename', 'public', superevent_id)],
             any_order=True
         )
         # FIXME: uncomment block below when patching
@@ -207,7 +227,10 @@ def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
         #         skymap_filename='bayestar.fits.gz', skymap_type='bayestar')
         gcn_send.assert_called_once()
         alerts_send.assert_called_once()
-        create_initial_circular.assert_called_once()
+        if raven_coinc:
+            create_emcoinc_circular.assert_called_once()
+        else:
+            create_initial_circular.assert_called_once()
 
     if alert_type == 'new' and group == 'CBC':
         query_data.assert_called_once()
@@ -220,7 +243,7 @@ def test_handle_superevent(monkeypatch, toy_3d_fits_filecontents,  # noqa: F811
             call_args = [
                 call_args.args[1:] for call_args in start_pe.call_args_list]
             assert all(
-                [(get_event('G1234'), 'S1234', pipeline) in call_args
+                [(get_event('G1234'), superevent_id, pipeline) in call_args
                  for pipeline in ('bilby', 'rapidpe')])
         else:
             start_pe.assert_not_called()
@@ -261,31 +284,57 @@ def superevent_initial_alert_download(filename, graceid):
     elif filename == 'em_bright.json,0':
         return json.dumps({'HasNS': 0.0, 'HasRemnant': 0.0})
     elif filename == 'p_astro.json,0':
-        return json.dumps(
-            dict(BNS=0.94, NSBH=0.03, BBH=0.02, Terrestrial=0.01))
+        return b'{"BNS": 0.94, "NSBH": 0.03, "BBH": 0.02, "Terrestrial": 0.01}'
     elif filename == 'foobar.multiorder.fits,0':
         return 'contents of foobar.multiorder.fits,0'
+    elif 'combined-ext.multiorder.fits' in filename:
+        return 'contents of combined-ext.multiorder.fits'
+    elif 'combined-ext.png' in filename:
+        return 'contents of combined-ext.png'
     else:
         raise ValueError
 
 
+def _mock_get_labels(ext_id):
+    if ext_id == 'E1':
+        return []
+    elif ext_id == 'E2':
+        return ['EM_COINC', 'RAVEN_ALERT']
+    elif ext_id == 'E3':
+        return ['EM_COINC', 'RAVEN_ALERT', 'COMBINEDSKYMAP_READY']
+
+
+def _mock_get_log(se_id):
+    logs = [{'tag_names': ['sky_loc', 'public'],
+             'filename': 'foobar.multiorder.fits',
+             'file_version': 0},
+            {'tag_names': ['em_bright'],
+             'filename': 'em_bright.json',
+             'file_version': 0},
+            {'tag_names': ['p_astro'],
+             'filename': 'p_astro.json',
+             'file_version': 0}]
+    if se_id == 'S2468':
+        logs.append({'tag_names': ['sky_loc', 'ext_coinc'],
+                     'filename': 'combined-ext.multiorder.fits',
+                     'file_version': 0})
+    return logs
+
+
 @pytest.mark.parametrize(  # noqa: F811
-    'labels',
-    [[], ['EM_COINC', 'RAVEN_ALERT']])
+    'labels,superevent_id,ext_id',
+    [[[], 'S1234', 'E1'],
+     [['EM_COINC', 'RAVEN_ALERT'], 'S1234', 'E2'],
+     [['EM_COINC', 'RAVEN_ALERT', 'COMBINEDSKYMAP_READY'], 'S1234', 'E3'],
+     [['EM_COINC', 'RAVEN_ALERT', 'COMBINEDSKYMAP_READY'], 'S2468', 'E3']])
 @patch('gwcelery.tasks.gracedb.expose._orig_run', return_value=None)
 @patch('gwcelery.tasks.gracedb.get_log',
-       return_value=[{'tag_names': ['sky_loc', 'public'],
-                      'filename': 'foobar.multiorder.fits',
-                      'file_version': 0},
-                     {'tag_names': ['em_bright'],
-                      'filename': 'em_bright.json',
-                      'file_version': 0},
-                     {'tag_names': ['p_astro'],
-                      'filename': 'p_astro.json',
-                      'file_version': 0}])
+       side_effect=_mock_get_log)
 @patch('gwcelery.tasks.gracedb.create_tag._orig_run', return_value=None)
 @patch('gwcelery.tasks.gracedb.create_voevent._orig_run',
        return_value='S1234-Initial-1.xml')
+@patch('gwcelery.tasks.gracedb.get_labels',
+       side_effect=_mock_get_labels)
 @patch('gwcelery.tasks.gracedb.download._orig_run',
        superevent_initial_alert_download)
 @patch('gwcelery.tasks.gcn.send.run')
@@ -296,46 +345,63 @@ def test_handle_superevent_initial_alert(mock_create_initial_circular,
                                          mock_create_emcoinc_circular,
                                          mock_alerts_send,
                                          mock_gcn_send,
+                                         mock_get_labels,
                                          mock_create_voevent,
                                          mock_create_tag, mock_get_log,
-                                         mock_expose, labels):
-    """Test that the ``ADVOK`` label triggers an initial alert."""
+                                         mock_expose, labels,
+                                         superevent_id, ext_id):
+    """Test that the ``ADVOK`` label triggers an initial alert.
+    This test varies the labels in the superevent and external event in order
+    to test the non-RAVEN alerts, RAVEN alerts without a combined sky map, and
+    RAVEN alerts with a combined sky map respectively."""
     alert = {
         'alert_type': 'label_added',
-        'uid': 'S1234',
+        'uid': superevent_id,
         'data': {'name': 'ADVOK'},
         'object': {
             'labels': labels,
-            'superevent_id': 'S1234'
-        }
+            'superevent_id': superevent_id,
+            'em_type': ext_id if labels else ''}
     }
+    combined_skymap_needed = ('COMBINEDSKYMAP_READY' in labels)
+    if combined_skymap_needed:
+        combined_skymap_filename = \
+            ('combined-ext.multiorder.fits' +
+             (',0' if superevent_id == 'S2468' else ''))
+    else:
+        combined_skymap_filename = None
 
     # Run function under test
     orchestrator.handle_superevent(alert)
 
     mock_create_voevent.assert_called_once_with(
-        'S1234', 'initial', BBH=0.02, BNS=0.94, NSBH=0.03, ProbHasNS=0.0,
+        superevent_id, 'initial', BBH=0.02, BNS=0.94, NSBH=0.03, ProbHasNS=0.0,
         ProbHasRemnant=0.0, Terrestrial=0.01, internal=False, open_alert=True,
         skymap_filename='foobar.multiorder.fits,0', skymap_type='foobar',
-        raven_coinc='RAVEN_ALERT' in labels)
-    mock_alerts_send.assert_called_once_with((
-        superevent_initial_alert_download('foobar.multiorder.fits,0', 'S1234'),
-        superevent_initial_alert_download('em_bright.json,0', 'S1234'),
-        superevent_initial_alert_download('p_astro.json,0', 'S1234'),
-        None, None, None, None), alert['object'], 'initial',
-        raven_coinc='RAVEN_ALERT' in labels)
+        raven_coinc='RAVEN_ALERT' in labels,
+        combined_skymap_filename=(combined_skymap_filename if
+                                  combined_skymap_needed else None))
+    mock_alerts_send.assert_called_once_with(
+        (superevent_initial_alert_download('foobar.multiorder.fits,0',
+                                           superevent_id),
+         superevent_initial_alert_download('em_bright.json,0', superevent_id),
+         superevent_initial_alert_download('p_astro.json,0', superevent_id)) +
+        ((6 if combined_skymap_needed and superevent_id == 'S1234' else 4)
+         * (None,)),
+        alert['object'], 'initial', raven_coinc='RAVEN_ALERT' in labels,
+        combined_skymap_filename=combined_skymap_filename)
     mock_gcn_send.assert_called_once_with('contents of S1234-Initial-1.xml')
     if 'RAVEN_ALERT' in labels:
-        mock_create_emcoinc_circular.assert_called_once_with('S1234')
+        mock_create_emcoinc_circular.assert_called_once_with(superevent_id)
     else:
-        mock_create_initial_circular.assert_called_once_with('S1234')
+        mock_create_initial_circular.assert_called_once_with(superevent_id)
     mock_create_tag.assert_has_calls(
-        [call('foobar.multiorder.fits,0', 'public', 'S1234'),
-         call('em_bright.json,0', 'public', 'S1234'),
-         call('p_astro.json,0', 'public', 'S1234'),
-         call('S1234-Initial-1.xml', 'public', 'S1234')],
+        [call('foobar.multiorder.fits,0', 'public', superevent_id),
+         call('em_bright.json,0', 'public', superevent_id),
+         call('p_astro.json,0', 'public', superevent_id),
+         call('S1234-Initial-1.xml', 'public', superevent_id)],
         any_order=True)
-    mock_expose.assert_called_once_with('S1234')
+    mock_expose.assert_called_once_with(superevent_id)
 
 
 def superevent_retraction_alert_download(filename, graceid):
-- 
GitLab