diff --git a/CHANGES.rst b/CHANGES.rst index 55dbe75c9681b67083671ae54999c63af5109eb8..99f5bd330690bd22a8b047f8839abe3602bb618d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -38,6 +38,10 @@ Changelog to the superevent. The automated pipeline is launched and is blocked before sending if ``EM_SelectedConfident`` is found to be applied. +- Add O3 replay MDC testing with RAVEN pipeline. This will run on the + emfollow-playground server, creating mock coincidences with a frequency + given by the ``joint_O3_replay_freq`` variable. + 2.0.3 "Ugly Merman" (2023-02-16) -------------------------------- diff --git a/gwcelery/conf/__init__.py b/gwcelery/conf/__init__.py index 3afe4c63c39a763929e29aba8a66b09d3124cab1..0ca1de48ffafcc5d605db55cf0b40e2ddc0948a3 100644 --- a/gwcelery/conf/__init__.py +++ b/gwcelery/conf/__init__.py @@ -353,6 +353,11 @@ joint_mdc_freq = 2 MDC superevent to test the RAVEN alert pipeline, i.e for every x MDC superevents an external MDC event is created.""" +joint_O3_replay_freq = 10 +"""Determines how often an external replay event will be created near an +superevent to test the RAVEN alert pipeline, i.e for every x +O3 replay superevents an external MDC event is created.""" + bilby_default_mode = 'fast_test' """Sampling mode of bilby""" diff --git a/gwcelery/tasks/first2years_external.py b/gwcelery/tasks/first2years_external.py index e09270a0e6b8de412ec6849c5cfc78f93b7ec8a6..24a40db4dcad043b28bbbc74524f0abc7bf24a0a 100644 --- a/gwcelery/tasks/first2years_external.py +++ b/gwcelery/tasks/first2years_external.py @@ -13,20 +13,31 @@ from . import external_triggers from . import igwn_alert -def create_grb_event(gpstime, pipeline): - +def create_grb_event(gpstime, pipeline, se_search): + """ Create a random GRB event for a certain percentage of MDC or O3-replay + superevents. + + Parameters + ---------- + gpstime : float + Event's gps time + pipeline : str + External trigger pipeline name + se_search : str + Search field for preferred event, 'MDC' or 'AllSky' + """ new_date = str(Time(gpstime, format='gps', scale='utc').isot) + 'Z' new_TrigID = str(int(gpstime)) - fname = str(Path(__file__).parent / '../tests/data/{}_grb_gcn.xml'.format(pipeline.lower())) root = etree.parse(fname) - - # Change ivorn to indicate is an MDC event + # Change ivorn to indicate if this is an MDC event or O3 replay event root.xpath('.')[0].attrib['ivorn'] = \ - 'ivo://lvk.internal/{0}#MDC-test_event{1}'.format( - pipeline if pipeline != 'Swift' else 'SWIFT', new_date).encode() + 'ivo://lvk.internal/{0}#{1}_event{2}'.format( + pipeline if pipeline != 'Swift' else 'SWIFT', + 'MDC-test' if se_search == 'MDC' else 'O3-replay', + new_date).encode() # Change times to chosen time root.find("./Who/Date").text = str(new_date).encode() @@ -56,55 +67,89 @@ def create_grb_event(gpstime, pipeline): pretty_print=True) -def _offset_time(gpstime): - # Reverse when searching around superevents - th_cbc, tl_cbc = app.conf['raven_coincidence_windows']['GRB_CBC'] - return gpstime + random.uniform(-tl_cbc, -th_cbc) +def _offset_time(gpstime, group): + """ This function checks coincident time windows for superevents if they + are of Burst or CBC group. + + Parameters + ---------- + gpstime : float + Event's gps time + group : str + Burst or CBC + """ + if group == 'Burst': + th, tl = app.conf['raven_coincidence_windows']['GRB_Burst'] + elif group == 'CBC': + th, tl = app.conf['raven_coincidence_windows']['GRB_CBC'] + else: + raise AssertionError( + 'Invalid group {}. Use only CBC or Burst.'.format(group)) + return gpstime + random.uniform(-tl, -th) -def _is_joint_mdc(graceid): - """Upload external event to every ten MDCs +def _is_joint_mdc(graceid, se_search): + """Upload external events to the user-defined frequency of MDC or AllSky + superevents. Looks at the ending letters of a superevent (e.g. 'ac' from 'MS190124ac'), converts to a number, and checks if divisible by a number given in the configuration file. - For example, if the configuration number 'joint_mdc_freq' is 10, + For example, if the configuration number + :obj:`~gwcelery.conf.joint_mdc_freq` is 10, this means joint events with superevents ending with 'j', 't', 'ad', etc. """ end_string = re.split(r'\d+', graceid)[-1].lower() val = 0 for i in range(len(end_string)): val += (ord(end_string[i]) - 96) * 26 ** (len(end_string) - i - 1) - return val % int(app.conf['joint_mdc_freq']) == 0 + return val % int(app.conf['joint_mdc_freq']) == 0 if se_search == 'MDC' \ + else val % int(app.conf['joint_O3_replay_freq']) == 0 @igwn_alert.handler('mdc_superevent', + 'superevent', queue='exttrig', shared=False) def upload_external_event(alert): - """Upload a random GRB event for a certain percentage of MDC superevents. - - Every n MDC superevents, upload a Fermi-like GRB candidate within the - standard CBC-GRB search window, where the frequency n is determined by - the configuration variable 'joint_mdc_freq'. + """Upload a random GRB event for a certain percentage of MDC + or O3-replay superevents. + + Notes + ----- + Every n superevents, upload a GRB candidate within the + standard CBC-GRB or Burst-GRB search window, where the frequency n is + determined by the configuration variable + :obj:`~gwcelery.conf.joint_mdc_freq` or + :obj:`~gwcelery.conf.joint_O3_replay_freq`. + + For O3 replay testing with RAVEN pipeline, only run on gracedb-playground. """ - - # Only create external MDC for the occasional MDC superevent - if not _is_joint_mdc(alert['uid']) or alert['alert_type'] != 'new': + if alert['alert_type'] != 'new': return + se_search = alert['object']['preferred_event_data']['search'] + group = alert['object']['preferred_event_data']['group'] + is_gracedb_playground = app.conf['gracedb_host'] \ + == 'gracedb-playground.ligo.org' + joint_mdc_alert = se_search == 'MDC' and _is_joint_mdc(alert['uid'], 'MDC') + joint_allsky_alert = se_search == 'AllSky' and \ + _is_joint_mdc(alert['uid'], 'AllSky') and is_gracedb_playground + if not (joint_mdc_alert or joint_allsky_alert): + return + # Potentially upload 1, 2, or 3 GRB events num = 1 + np.random.choice(np.arange(3), p=[.6, .3, .1]) events = [] pipelines = [] for i in range(num): gpstime = float(alert['object']['t_0']) - new_time = _offset_time(gpstime) + new_time = _offset_time(gpstime, group) # Choose external grb pipeline to simulate pipeline = np.random.choice(['Fermi', 'Swift', 'INTEGRAL', 'AGILE'], p=[.5, .3, .1, .1]) - ext_event = create_grb_event(new_time, pipeline) + ext_event = create_grb_event(new_time, pipeline, se_search) # Upload as from GCN external_triggers.handle_grb_gcn(ext_event) diff --git a/gwcelery/tests/test_tasks_first2years_external.py b/gwcelery/tests/test_tasks_first2years_external.py index a2d1583652e5f2250c75d928c1cdfeb203b0b444..1ec7fcbd076c52eb565b170dce734a0cbd89a7a0 100644 --- a/gwcelery/tests/test_tasks_first2years_external.py +++ b/gwcelery/tests/test_tasks_first2years_external.py @@ -1,40 +1,90 @@ -from unittest.mock import call, patch +import pytest +from unittest.mock import call, Mock from . import data +from .. import app from ..tasks import first2years_external from ..util import read_json -@patch('gwcelery.tasks.external_skymaps.create_upload_external_skymap.run') -@patch('gwcelery.tasks.external_skymaps.get_upload_external_skymap.run') -@patch('gwcelery.tasks.detchar.check_vectors.run') -@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': 10.}}, - 'links': {'self': 'https://gracedb.ligo.org/events/E356793/'}}) -@patch('gwcelery.tasks.gracedb.get_events', return_value=[]) -def test_handle_create_grb_event(mock_get_events, - mock_create_event, - mock_check_vectors, - mock_get_upload_external_skymap, - mock_create_upload_external_skymap): - +@pytest.mark.parametrize( + 'host,se_search,group,superevent_id,expected_result', + [['gracedb-playground.ligo.org', 'MDC', 'CBC', 'MS180616j', True], + ['gracedb-playground.ligo.org', 'AllSky', 'CBC', 'MS180616j', True], + ['gracedb-playground.ligo.org', 'AllSky', 'Burst', 'MS180616j', True], + ['gracedb-playground.ligo.org', 'BBH', 'CBC', 'MS180616j', False], + ['gracedb-playground.ligo.org', 'AllSky', 'Test', 'TS180616j', False], + ['gracedb.ligo.org', 'MDC', 'CBC', 'MS180616j', True], + ['gracedb.ligo.org', 'AllSky', 'CBC', 'MS180616j', False], + ['gracedb.ligo.org', 'AllSky', 'Burst', 'MS180616j', False], + ['gracedb.ligo.org', 'MDC', 'CBC', 'MS180616k', False]]) +def test_handle_create_grb_event(monkeypatch, + host, + se_search, + group, + superevent_id, + expected_result): # Test IGWN alert payload. alert = read_json(data, 'igwn_alert_superevent_creation.json') - - alert['uid'] = 'MS180616j' + alert['uid'] = superevent_id alert['object']['superevent_id'] = alert['uid'] - alert['object']['preferred_event_data']['search'] = 'MDC' - events, pipelines = first2years_external.upload_external_event(alert) + alert['object']['preferred_event_data']['search'] = se_search + alert['object']['preferred_event_data']['group'] = group + + mock_create_upload_external_skymap = Mock() + mock_get_upload_external_skymap = Mock() + mock_check_vectors = Mock() + mock_create_event = Mock( + 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': 10.}}, + 'links': { + 'self': + 'https://gracedb.ligo.org/events/E356793/'}}) + mock_get_events = Mock(return_value=[]) + + monkeypatch.setattr( + 'gwcelery.tasks.external_skymaps.create_upload_external_skymap.run', + mock_create_upload_external_skymap) + monkeypatch.setattr( + 'gwcelery.tasks.external_skymaps.get_upload_external_skymap.run', + mock_get_upload_external_skymap) + monkeypatch.setattr('gwcelery.tasks.detchar.check_vectors.run', + mock_check_vectors) + monkeypatch.setattr('gwcelery.tasks.gracedb.create_event.run', + mock_create_event) + monkeypatch.setattr('gwcelery.tasks.gracedb.get_events', + mock_get_events) + monkeypatch.setattr(app.conf, 'gracedb_host', host) + if group == 'Test': + with pytest.raises(AssertionError): + first2years_external.upload_external_event(alert) + res = None + else: + res = first2years_external.upload_external_event(alert) + if not expected_result: + assert res is None + events, pipelines = [], [] + else: + events, pipelines = res calls = [] for i in range(len(events)): calls.append(call(filecontents=events[i], - search='MDC', + search='MDC' if se_search == 'MDC' else 'GRB', pipeline=pipelines[i], group='External', labels=None)) - mock_create_event.assert_has_calls(calls) - mock_create_upload_external_skymap.assert_called() + if expected_result: + mock_create_event.assert_has_calls(calls) + mock_create_upload_external_skymap.assert_called() + else: + mock_create_event.assert_not_called() + mock_create_upload_external_skymap.assert_not_called()