Coverage for ligo/followup_advocate/__init__.py: 91%
428 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-08 21:23 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-08 21:23 +0000
1import math as mth
2import os
3import shutil
4import tempfile
5import urllib
6import webbrowser
8import astropy.coordinates as coord
9import astropy.time
10import astropy.units as u
11import healpy as hp
12import lxml.etree
13import numpy as np
14from astropy.io.fits import getheader
15from ligo.gracedb import rest
16from ligo.skymap.io.fits import read_sky_map
17from ligo.skymap.postprocess.ellipse import find_ellipse
18from ligo.skymap.postprocess.crossmatch import crossmatch
20from .jinja import env
21import importlib.metadata
22__version__ = importlib.metadata.version(__name__)
25def authors(authors, service=rest.DEFAULT_SERVICE_URL):
26 """Write GCN Circular author list"""
27 return env.get_template('authors.jinja2').render(authors=authors).strip()
30def guess_skyloc_pipeline(filename):
31 skyloc_pipelines_dict = {
32 'cwb': 'cWB_AllSky',
33 'bayestar': 'BAYESTAR',
34 'bilby': 'Bilby',
35 'lib': 'LIB',
36 'lalinference': 'LALInference',
37 'olib': 'oLIB_AllSky',
38 'mly': 'MLy_AllSky',
39 'rapidpe_rift': 'RapidPE-RIFT',
40 'amplfi': 'AMPLFI'
41 }
42 try:
43 return skyloc_pipelines_dict[filename.split('.')[0].lower()]
44 except KeyError:
45 return filename.split('.')[0]
48def text_width(remove_text_wrap):
49 """Return width of text wrap based on whether we wish to wrap the lines or
50 not."""
51 return 9999 if remove_text_wrap else 79
54def main_dict(gracedb_id, client, raven_coinc=False, update_alert=False,
55 cgmi_filename=None):
56 """Create general dictionary to pass to compose circular"""
58 superevent = client.superevent(gracedb_id).json()
59 preferred_event = superevent['preferred_event_data']
60 pipeline = preferred_event['pipeline']
61 search = preferred_event['search']
62 preferred_pipeline_search = f'{pipeline}_{search}'
63 early_warning_pipeline_searches = []
64 pipeline_searches = []
65 gw_events = superevent['gw_events']
66 early_warning_alert = False
68 for gw_event in gw_events:
69 gw_event_dict = client.event(gw_event).json()
70 pipeline = gw_event_dict['pipeline']
71 search = gw_event_dict['search']
72 # Remap MDC to allsky to not double count
73 if search == 'MDC':
74 search = 'AllSky'
75 pipeline_search = f'{pipeline}_{search}'
76 # Create separates lists of post merger and early warning pipelines
77 if pipeline_search not in pipeline_searches and \
78 'EarlyWarning' not in pipeline_search:
79 pipeline_searches.append(pipeline_search)
80 if 'EarlyWarning' in pipeline_search:
81 # Remap MBTA early warning to the all sky citation prior to
82 # checking if there is a previous entry
83 if 'mbta' in pipeline_search.lower():
84 pipeline_search = 'MBTA_AllSky'
85 if pipeline_search not in early_warning_pipeline_searches:
86 early_warning_pipeline_searches.append(pipeline_search)
88 if not pipeline_searches:
89 raise ValueError(
90 "{} has no post-merger events to generate circulars from.".format(
91 gracedb_id))
93 # Sort to get alphabetical order
94 pipeline_searches.sort(key=str.lower)
95 early_warning_pipeline_searches.sort(key=str.lower)
97 voevents = client.voevents(gracedb_id).json()['voevents']
98 if not voevents:
99 raise ValueError(
100 "{} has no VOEvent to generate circulars from.".format(
101 gracedb_id))
103 citation_index = {
104 pipeline_search.lower(): pipeline_searches.index(pipeline_search) + 1
105 for pipeline_search in pipeline_searches}
106 for pipeline_search in early_warning_pipeline_searches:
107 if pipeline_search.lower() not in citation_index:
108 citation_index[pipeline_search.lower()] = \
109 max(citation_index.values()) + 1
111 gpstime = float(preferred_event['gpstime'])
112 event_time = astropy.time.Time(gpstime, format='gps').utc
114 # Grab latest p_astro and em_bright values from lastest VOEvent
115 voevent_text_latest = \
116 client.files(gracedb_id, voevents[-1]['filename']).read()
117 root = lxml.etree.fromstring(voevent_text_latest)
118 p_astros = root.find('./What/Group[@name="Classification"]')
119 em_brights = root.find('./What/Group[@name="Properties"]')
120 classifications = {}
121 source_classification = {}
122 mchirp_bin = {}
123 # Only try to load if present to prevent errors with .getchildren()
124 p_astro_pipeline = None
125 search_for_p_astro = False
126 search_for_cgmi_file = False
127 logs = []
129 # Grab em_bright values if present
130 for em_bright in em_brights.getchildren():
131 if em_bright.attrib.get('value'):
132 source_classification[em_bright.attrib['name']] = \
133 float(em_bright.attrib['value']) * 100
135 # Try to look for CGMI file if there are actual em bright values for a
136 # preliminary/initial alert, and there is no manual filename given
137 search_for_cgmi_file = len(source_classification) > 0 and not \
138 update_alert and cgmi_filename is None
140 # Grab p_astro values if present
141 for p_astro in p_astros.getchildren():
142 if p_astro.attrib.get('value'):
143 classifications[p_astro.attrib['name']] = \
144 float(p_astro.attrib['value']) * 100
145 search_for_p_astro = True
147 # Grab logs for either RapidPE checks or chirp mass bins
148 if search_for_p_astro or search_for_cgmi_file:
149 logs = client.logs(gracedb_id).json()['log']
151 # Figure out which pipeline uploaded p_astro, usually the first one
152 # FIXME: Replace with more efficient method in the future
153 for message in reversed(logs):
154 filename = message['filename']
155 if filename and '.p_astro.json' in filename and \
156 filename != 'p_astro.json':
157 p_astro = client.files(gracedb_id, filename).json()
158 if all(mth.isclose(p_astro[key] * 100, classifications[key])
159 for key in classifications.keys()):
160 p_astro_pipeline = filename.split('.')[0].lower()
161 break
163 # Adjust citations if needed
164 if p_astro_pipeline == 'rapidpe_rift':
165 citation_index['rapidpe_rift'] = max(citation_index.values()) + 1
166 if len(source_classification) > 0:
167 citation_index['em_bright'] = max(citation_index.values()) + 1
169 # Look for CGMI file if a preliminary/inital alert with embright info or
170 # if given a filename, usually for an update alert
171 if search_for_cgmi_file or cgmi_filename is not None:
172 cgmi_json = {}
173 # chirp mass estimates included when em-bright is
174 if cgmi_filename is not None:
175 cgmi_json = client.files(gracedb_id, cgmi_filename).json()
176 else:
177 for message in reversed(logs):
178 filename = message['filename']
179 # use most recent mchirp estimate
180 if filename and 'mchirp_source' in filename and \
181 '.json' in filename:
182 cgmi_json = client.files(gracedb_id, filename).json()
183 break
184 # if CGMI file found either way, include in results
185 if cgmi_json:
186 mchirp_bin_edges = cgmi_json['bin_edges']
187 mchirp_probs = cgmi_json['probabilities']
188 # find the highest probability bin
189 max_prob_idx = np.argmax(mchirp_probs)
190 left_bin = mchirp_bin_edges[max_prob_idx]
191 right_bin = mchirp_bin_edges[max_prob_idx+1]
192 mchirp_bin = left_bin, right_bin
194 skymaps = {}
195 voevents_text = []
196 for voevent in voevents:
197 # Don't load latest voevent since already loaded from before
198 if voevent == voevents[-1]:
199 voevent_text = voevent_text_latest
200 # If earlier voevent, load
201 else:
202 voevent_text = client.files(gracedb_id, voevent['filename']).read()
203 root = lxml.etree.fromstring(voevent_text)
204 alert_type = root.find(
205 './What/Param[@name="AlertType"]').attrib['value'].lower()
206 if alert_type == 'earlywarning':
207 # Add text for early warning detection if one early warning alert
208 early_warning_alert = True
209 url = root.find('./What/Group/Param[@name="skymap_fits"]')
210 if url is None:
211 continue
212 url = url.attrib['value']
213 _, filename = os.path.split(url)
214 skyloc_pipeline = guess_skyloc_pipeline(filename)
215 issued_time = astropy.time.Time(root.find('./Who/Date').text).gps
216 if filename not in skymaps:
217 skymaps[filename] = dict(
218 alert_type=alert_type,
219 pipeline=skyloc_pipeline,
220 filename=filename,
221 latency=issued_time-event_time.gps)
222 if skyloc_pipeline.lower() not in citation_index:
223 citation_index[skyloc_pipeline.lower()] = \
224 max(citation_index.values()) + 1
225 voevents_text.append(voevent_text)
226 skymaps = list(skymaps.values())
228 o = urllib.parse.urlparse(client.service_url)
230 kwargs = dict(
231 subject='Identification',
232 gracedb_id=gracedb_id,
233 gracedb_service_url=urllib.parse.urlunsplit(
234 (o.scheme, o.netloc, '/superevents/', '', '')),
235 update_alert=update_alert,
236 cgmi_filename=cgmi_filename,
237 group=preferred_event['group'],
238 pipeline_search=preferred_pipeline_search,
239 post_merger_pipeline_searches=pipeline_searches,
240 early_warning_alert=early_warning_alert,
241 early_warning_pipeline_searches=early_warning_pipeline_searches,
242 gpstime='{0:.03f}'.format(round(float(preferred_event['gpstime']), 3)),
243 search=preferred_event.get('search', ''),
244 far=preferred_event['far'],
245 utctime=event_time.iso,
246 instruments=preferred_event['instruments'].split(','),
247 skymaps=skymaps,
248 prob_has_ns=source_classification.get('HasNS'),
249 prob_has_remnant=source_classification.get('HasRemnant'),
250 prob_has_massgap=source_classification.get('HasMassGap'),
251 prob_has_ssm=source_classification.get('HasSSM'),
252 source_classification=source_classification,
253 mchirp_bin=mchirp_bin,
254 include_ellipse=None,
255 classifications=classifications,
256 p_astro_pipeline=p_astro_pipeline,
257 citation_index=citation_index)
259 if skymaps:
260 preferred_skymap = skymaps[-1]['filename']
261 cls = [50, 90]
262 include_ellipse, ra, dec, a, b, pa, area, greedy_area = \
263 uncertainty_ellipse(gracedb_id, preferred_skymap, client, cls=cls)
264 kwargs.update(
265 preferred_skymap=preferred_skymap,
266 cl=cls[-1],
267 include_ellipse=include_ellipse,
268 ra=coord.Longitude(ra*u.deg),
269 dec=coord.Latitude(dec*u.deg),
270 a=coord.Angle(a*u.deg),
271 b=coord.Angle(b*u.deg),
272 pa=coord.Angle(pa*u.deg),
273 ellipse_area=area,
274 greedy_area=greedy_area)
275 try:
276 distmu, distsig = get_distances_skymap_gracedb(gracedb_id,
277 preferred_skymap,
278 client)
279 kwargs.update(
280 distmu=distmu,
281 distsig=distsig)
282 except TypeError:
283 pass
285 if raven_coinc:
286 kwargs = _update_raven_parameters(superevent, kwargs, client,
287 voevents_text)
288 return kwargs
291def compose(gracedb_id, authors=(), mailto=False, remove_text_wrap=False,
292 service=rest.DEFAULT_SERVICE_URL, client=None):
293 """Compose GCN Circular draft"""
295 if client is None:
296 client = rest.GraceDb(service)
298 kwargs = main_dict(gracedb_id, client=client)
299 kwargs.update(authors=authors)
300 kwargs.update(gw_is_subthreshold=False)
301 kwargs.update(text_width=text_width(remove_text_wrap))
303 subject = env.get_template('subject.jinja2').render(**kwargs).strip()
304 body = env.get_template('initial_circular.jinja2').render(**kwargs).strip()
306 if mailto:
307 pattern = 'mailto:emfollow@ligo.org,dac@ligo.org?subject={0}&body={1}'
308 url = pattern.format(
309 urllib.parse.quote(subject),
310 urllib.parse.quote(body))
311 webbrowser.open(url)
312 else:
313 return '{0}\n\n{1}'.format(subject, body)
316def compose_raven(gracedb_id, authors=(), remove_text_wrap=False,
317 service=rest.DEFAULT_SERVICE_URL, client=None,
318 gw_is_subthreshold=False):
319 """Compose EM_COINC RAVEN GCN Circular draft"""
321 if client is None:
322 client = rest.GraceDb(service)
324 kwargs = dict()
325 kwargs.update(main_dict(gracedb_id, client=client, raven_coinc=True))
326 kwargs.update(text_width=text_width(remove_text_wrap))
327 kwargs.update(gw_is_subthreshold=gw_is_subthreshold)
328 # Add RAVEN citation
329 citation_index = kwargs['citation_index']
330 citation_index['raven'] = max(citation_index.values()) + 1
331 kwargs['citation_index'] = citation_index
333 subject = (
334 env.get_template('RAVEN_subject.jinja2').render(**kwargs)
335 .strip())
336 body = (
337 env.get_template('RAVEN_circular.jinja2').render(**kwargs)
338 .strip())
339 return '{0}\n\n{1}'.format(subject, body)
342def compose_llama(
343 gracedb_id, authors=(), service=rest.DEFAULT_SERVICE_URL,
344 icecube_alert=None, remove_text_wrap=False,
345 client=None):
346 """Compose GRB LLAMA GCN Circular draft.
347 Here, gracedb_id will be a GRB superevent ID in GraceDb."""
349 if client is None:
350 client = rest.GraceDb(service)
352 superevent = client.superevent(gracedb_id).json()
354 gpstime = float(superevent['t_0'])
355 tl, th = gpstime - 500, gpstime + 500
356 event_time = astropy.time.Time(gpstime, format='gps').utc
357 tl_datetime = str(astropy.time.Time(
358 tl, format='gps').isot).replace('T', ' ')
359 th_datetime = str(astropy.time.Time(
360 th, format='gps').isot).replace('T', ' ')
362 o = urllib.parse.urlparse(client.service_url)
363 kwargs = dict(
364 gracedb_service_url=urllib.parse.urlunsplit(
365 (o.scheme, o.netloc, '/superevents/', '', '')),
366 gracedb_id=gracedb_id,
367 llama=True,
368 icecube_alert=icecube_alert,
369 event_time=event_time,
370 tl_datetime=tl_datetime,
371 th_datetime=th_datetime,
372 authors=authors)
373 kwargs.update(text_width=text_width(remove_text_wrap))
375 citation_index = {'llama': 1, 'llama_stat': 2}
376 kwargs.update(citation_index=citation_index)
378 files = client.files(gracedb_id).json()
380 llama_stat_filename = 'significance_subthreshold_lvc-i3.json'
381 if llama_stat_filename in files:
382 llama_stat_file = client.files(gracedb_id, llama_stat_filename).json()
383 llama_fap = llama_stat_file["p_value"]
384 neutrinos = llama_stat_file["inputs"]["neutrino_info"]
385 lines = []
386 for neutrino in neutrinos:
387 # Build aligned string
388 vals = []
389 dt = (event_time -
390 astropy.time.Time(neutrino['mjd'],
391 format='mjd')).to(u.s).value
392 vals.append('{:.2f}'.format(dt))
393 vals.append('{:.2f}'.format(neutrino['ra']))
394 vals.append('{:.2f}'.format(neutrino['dec']))
395 vals.append('{:.2f}'.format(neutrino['sigma']))
396 vals.append('{:.4f}'.format(llama_fap))
397 lines.append(align_number_string(vals, [0, 11, 21, 40, 59]))
399 kwargs.update(llama_fap=llama_fap,
400 neutrinos=lines)
402 subject = (
403 env.get_template('llama_subject.jinja2').render(**kwargs)
404 .strip())
405 if icecube_alert:
406 body = (
407 env.get_template('llama_alert_circular.jinja2').render(**kwargs)
408 .strip())
409 else:
410 body = (
411 env.get_template('llama_track_circular.jinja2').render(**kwargs)
412 .strip())
413 return '{0}\n\n{1}'.format(subject, body)
416def compose_grb_medium_latency(
417 gracedb_id, authors=(), service=rest.DEFAULT_SERVICE_URL,
418 use_detection_template=None, remove_text_wrap=False, client=None):
419 """Compose GRB Medium Latency GCN Circular draft.
420 Here, gracedb_id will be a GRB external trigger ID in GraceDb."""
422 if client is None:
423 client = rest.GraceDb(service)
425 event = client.event(gracedb_id).json()
426 search = event['search']
428 if search != 'GRB':
429 return
431 group = event['group']
432 pipeline = event['pipeline']
433 external_trigger = event['extra_attributes']['GRB']['trigger_id']
435 o = urllib.parse.urlparse(client.service_url)
436 kwargs = dict(
437 gracedb_service_url=urllib.parse.urlunsplit(
438 (o.scheme, o.netloc, '/events/', '', '')),
439 gracedb_id=gracedb_id,
440 group=group,
441 grb=True,
442 pipeline=pipeline,
443 external_trigger=external_trigger,
444 exclusions=[],
445 detections=[])
446 kwargs.update(text_width=text_width(remove_text_wrap))
448 files = client.files(gracedb_id).json()
450 citation_index = {}
451 index = 1
452 xpipeline_fap_file = 'false_alarm_probability_x.json'
453 if xpipeline_fap_file in files:
454 xpipeline_fap = client.files(gracedb_id, xpipeline_fap_file).json()
455 online_xpipeline_fap = xpipeline_fap.get('Online Xpipeline')
456 # Create detection/exclusion circular based on given argument
457 # Use default cutoff if not provided
458 xpipeline_detection = (use_detection_template if use_detection_template
459 is not None else online_xpipeline_fap < 0.001)
460 if xpipeline_detection:
461 kwargs['detections'].append('xpipeline')
462 kwargs.update(online_xpipeline_fap=online_xpipeline_fap)
463 else:
464 kwargs['exclusions'].append('xpipeline')
465 xpipeline_distances_file = 'distances_x.json'
466 xpipeline_distances = client.files(gracedb_id,
467 xpipeline_distances_file).json()
468 burst_exclusion = xpipeline_distances.get('Burst Exclusion')
469 kwargs.update(burst_exclusion=burst_exclusion)
470 citation_index['xpipeline'] = index
471 index += 1
473 pygrb_fap_file = 'false_alarm_probability_pygrb.json'
474 if pygrb_fap_file in files:
475 pygrb_fap = client.files(gracedb_id, pygrb_fap_file).json()
476 online_pygrb_fap = pygrb_fap.get('Online PyGRB')
477 # Create detection/exclusion circular based on given argument
478 # Use default cutoff if not provided
479 pygrb_detection = (use_detection_template if use_detection_template
480 is not None else online_pygrb_fap < 0.01)
481 if pygrb_detection:
482 kwargs['detections'].append('pygrb')
483 kwargs.update(online_pygrb_fap=online_pygrb_fap)
484 else:
485 kwargs['exclusions'].append('pygrb')
486 pygrb_distances_file = 'distances_pygrb.json'
487 pygrb_distances = client.files(gracedb_id,
488 pygrb_distances_file).json()
489 nsns_exclusion = pygrb_distances.get('NSNS Exclusion')
490 nsbh_exclusion = pygrb_distances.get('NSBH Exclusion')
491 kwargs.update(nsbh_exclusion=nsbh_exclusion,
492 nsns_exclusion=nsns_exclusion)
493 citation_index['pygrb'] = index
495 kwargs.update(citation_index=citation_index)
497 subject = (
498 env.get_template('medium_latency_grb_subject.jinja2').render(**kwargs)
499 .strip())
500 body = (
501 env.get_template('medium_latency_grb_circular.jinja2').render(**kwargs)
502 .strip())
503 return '{0}\n\n{1}'.format(subject, body)
506def compose_update(gracedb_id, authors=(),
507 service=rest.DEFAULT_SERVICE_URL,
508 update_types=['sky_localization', 'p_astro',
509 'em_bright', 'raven'],
510 remove_text_wrap=False,
511 client=None,
512 cgmi_filename=None):
513 """Compose GCN update circular"""
514 if client is None:
515 client = rest.GraceDb(service)
517 kwargs = main_dict(gracedb_id, client=client,
518 raven_coinc='raven' in update_types,
519 update_alert=True,
520 cgmi_filename=cgmi_filename)
521 kwargs.pop('citation_index', None)
522 kwargs.update(text_width=text_width(remove_text_wrap))
524 if isinstance(update_types, str):
525 update_types = update_types.split(',')
527 # Adjust files for update type alert
528 citation_index = {}
529 skymaps = []
530 index = 1
532 updated_skymap = kwargs.get('skymaps')[-1]
533 kwargs.update(updated_skymap=updated_skymap)
534 skymaps = [updated_skymap]
535 if cgmi_filename:
536 update_types.append('cgmi')
537 if 'sky_localization' in update_types:
538 citation_index[updated_skymap['pipeline'].lower()] = index
539 index += 1
540 if 'p_astro' in update_types and \
541 kwargs.get('p_astro_pipeline') == 'rapidpe_rift':
542 citation_index['rapidpe_rift'] = index
543 index += 1
544 if 'em_bright' in update_types:
545 # If not already cited, cite sky map pipeline for em_bright
546 if updated_skymap['pipeline'].lower() not in citation_index.keys():
547 citation_index[updated_skymap['pipeline'].lower()] = index
548 index += 1
549 citation_index['em_bright'] = index
550 index += 1
551 if 'raven' in update_types:
552 citation_index['raven'] = index
554 kwargs.update(skymaps=skymaps,
555 citation_index=citation_index,
556 post_merger_pipeline_searches=[],
557 update_alert=True)
559 kwargs.update(authors=authors)
560 kwargs.update(gw_is_subthreshold=False)
561 kwargs.update(subject='Update')
562 kwargs.update(update_types=update_types)
564 subject = env.get_template('subject.jinja2').render(**kwargs).strip()
565 body = env.get_template(
566 'update_circular.jinja2').render(**kwargs).strip()
567 return '{0}\n\n{1}'.format(subject, body)
570def compose_retraction(gracedb_id, authors=(), remove_text_wrap=False,
571 service=rest.DEFAULT_SERVICE_URL, client=None):
572 """Compose GCN retraction circular"""
573 if client is None:
574 client = rest.GraceDb(service)
575 event = client.superevent(gracedb_id).json()
576 preferred_event = event['preferred_event_data']
577 labels = event['labels']
578 earlywarning = \
579 ('EARLY_WARNING' in labels and
580 {'EM_SelectedConfident', 'SIGNIF_LOCKED'}.isdisjoint(labels))
582 kwargs = dict(
583 subject='Retraction',
584 gracedb_id=gracedb_id,
585 group=preferred_event['group'],
586 earlywarning=earlywarning,
587 authors=authors
588 )
589 kwargs.update(text_width=text_width(remove_text_wrap))
591 subject = env.get_template('subject.jinja2').render(**kwargs).strip()
592 body = env.get_template('retraction.jinja2').render(**kwargs).strip()
593 return '{0}\n\n{1}'.format(subject, body)
596def read_map_gracedb(graceid, filename, client):
597 with tempfile.NamedTemporaryFile(mode='w+b') as localfile:
598 remotefile = client.files(graceid, filename, raw=True)
599 shutil.copyfileobj(remotefile, localfile)
600 localfile.flush()
601 return read_sky_map(localfile.name, moc=True)
604def get_distances_skymap_gracedb(graceid, filename, client):
605 with tempfile.NamedTemporaryFile(mode='w+b') as localfile:
606 remotefile = client.files(graceid, filename, raw=True)
607 shutil.copyfileobj(remotefile, localfile)
608 localfile.flush()
609 header = getheader(localfile.name, 1)
610 try:
611 return header['distmean'], header['diststd']
612 except KeyError:
613 pass
616def read_map_from_path(path, client):
617 return read_map_gracedb(*path.split('/'), client)[0]
620def align_number_string(nums, positions):
621 positions.append(len(nums[-1]))
622 gen = (val + ' ' * (positions[i+1]-positions[i]-len(val))
623 for i, val in enumerate(nums))
624 return ''.join(gen)
627def mask_cl(p, level=90):
628 pflat = p.ravel()
629 i = np.flipud(np.argsort(p))
630 cs = np.cumsum(pflat[i])
631 cls = np.empty_like(pflat)
632 cls[i] = cs
633 cls = cls.reshape(p.shape)
634 return cls <= 1e-2 * level
637def compare_skymaps(paths, service=rest.DEFAULT_SERVICE_URL, client=None):
638 """Produce table of sky map overlaps"""
639 if client is None:
640 client = rest.GraceDb(service)
641 filenames = [path.split('/')[1] for path in paths]
642 pipelines = [guess_skyloc_pipeline(filename) for filename in filenames]
643 probs = [read_map_from_path(path, client) for path in paths]
644 npix = max(len(prob) for prob in probs)
645 nside = hp.npix2nside(npix)
646 deg2perpix = hp.nside2pixarea(nside, degrees=True)
647 probs = [hp.ud_grade(prob, nside, power=-2) for prob in probs]
648 masks = [mask_cl(prob) for prob in probs]
649 areas = [mask.sum() * deg2perpix for mask in masks]
650 joint_areas = [(mask & masks[-1]).sum() * deg2perpix for mask in masks]
652 kwargs = dict(params=zip(filenames, pipelines, areas, joint_areas))
654 return env.get_template('compare_skymaps.jinja2').render(**kwargs)
657def uncertainty_ellipse(graceid, filename, client, cls=[50, 90],
658 ratio_ellipse_cl_areas=1.35):
659 """Compute uncertainty ellipses for a given sky map
661 Parameters
662 ----------
663 graceid: str
664 ID of the trigger used by GraceDB
665 filename: str
666 File name of sky map
667 client: class
668 REST API client for HTTP connection
669 cls: array-like
670 List of percentage of minimal credible area used to check whether the
671 areas are close to an ellipse, returning the values of the final item
672 ratio_ellipse_cl_areas: float
673 Ratio between ellipse area and minimal credible area from cl
674 """
675 if filename.endswith('.gz'):
676 # Try using the multi-res sky map if it exists
677 try:
678 new_filename = filename.replace('.fits.gz', '.multiorder.fits')
679 skymap = read_map_gracedb(graceid, new_filename, client)
680 except (IOError, rest.HTTPError):
681 skymap = read_map_gracedb(graceid, filename, client)
682 else:
683 skymap = read_map_gracedb(graceid, filename, client)
685 # Convert to an array if necessary
686 if np.isscalar(cls):
687 cls = [cls]
688 cls = np.asarray(cls)
690 # Pass array of contour inteverals to get areas
691 result = crossmatch(skymap, contours=cls / 100)
692 greedy_areas = np.asarray(result.contour_areas)
693 ra, dec, a, b, pa, ellipse_areas = find_ellipse(skymap, cl=cls)
694 a, b = np.asarray(a), np.asarray(b)
696 # Only use ellipse if every confidence interval passes
697 use_ellipse = \
698 np.all(ellipse_areas <= ratio_ellipse_cl_areas * greedy_areas)
699 return (use_ellipse, ra, dec, a[-1], b[-1], pa, ellipse_areas[-1],
700 greedy_areas[-1])
703def _update_raven_parameters(superevent, kwargs, client, voevents_text):
704 """Update kwargs with parameters for RAVEN coincidence"""
706 gracedb_id = superevent['superevent_id']
708 if 'EM_COINC' not in superevent['labels']:
709 raise ValueError("No EM_COINC label for {}".format(
710 gracedb_id))
712 preferred_event = superevent['preferred_event_data']
713 group = preferred_event['group']
714 gpstime = float(preferred_event['gpstime'])
715 event_time = astropy.time.Time(gpstime, format='gps').utc
716 em_event_id = superevent['em_type']
718 # FIXME: Grab more info from the latest VOEvent if deemed suitable
719 em_event = client.event(em_event_id).json()
720 external_pipeline = em_event['pipeline']
721 # Get all other pipelines
722 ext_events = [client.event(id).json() for id
723 in superevent['em_events']]
724 # Remove duplicates and vetoed events
725 other_ext_pipelines = \
726 [*set(event['pipeline'] for event in ext_events
727 if 'NOT_GRB' not in event['labels'])]
728 # Remove preferred pipeline if present
729 # This is to cover a corner case where NOT_GRB gets added to a preferred
730 # external event after RAVEN_ALERT is applied
731 if external_pipeline in other_ext_pipelines:
732 other_ext_pipelines.remove(external_pipeline)
733 # FIXME in GraceDb: Even SNEWS triggers have an extra attribute GRB.
734 external_trigger_id = em_event['extra_attributes']['GRB']['trigger_id']
735 snews = (em_event['pipeline'] == 'SNEWS')
736 grb = (em_event['search'] in ['GRB', 'SubGRB', 'SubGRBTargeted', 'MDC']
737 and not snews)
738 subthreshold = em_event['search'] in ['SubGRB', 'SubGRBTargeted']
739 subthreshold_targeted = em_event['search'] == 'SubGRBTargeted'
740 far_grb = em_event['far']
742 voevent_text_latest = voevents_text[-1]
743 root = lxml.etree.fromstring(voevent_text_latest)
744 time_diff = \
745 root.find('./What/Group/Param[@name="Time_Difference"]')
746 time_diff = float(time_diff.attrib['value'])
748 o = urllib.parse.urlparse(client.service_url)
749 kwargs.update(
750 gracedb_service_url=urllib.parse.urlunsplit(
751 (o.scheme, o.netloc, '/superevents/', '', '')),
752 gracedb_id=gracedb_id,
753 group=group,
754 external_pipeline=external_pipeline,
755 external_trigger=external_trigger_id,
756 snews=snews,
757 grb=grb,
758 subthreshold=subthreshold,
759 subthreshold_targeted=subthreshold_targeted,
760 other_ext_pipelines=sorted(other_ext_pipelines),
761 far_grb=far_grb,
762 latency=abs(round(time_diff, 1)),
763 beforeafter='before' if time_diff < 0 else 'after')
765 if grb:
766 # Grab GRB coincidence FARs
767 time_coinc_far = superevent['time_coinc_far']
768 space_time_coinc_far = superevent['space_coinc_far']
769 kwargs.update(
770 time_coinc_far=time_coinc_far,
771 space_time_coinc_far=space_time_coinc_far,
772 ext_ra=em_event['extra_attributes']['GRB']['ra'],
773 ext_dec=em_event['extra_attributes']['GRB']['dec'],
774 ext_error=em_event['extra_attributes']['GRB']['error_radius'])
776 # Find combined sky maps for GRB
777 combined_skymaps = {}
778 for i, voevent_text in enumerate(voevents_text):
779 root = lxml.etree.fromstring(voevent_text)
780 alert_type = root.find(
781 './What/Param[@name="AlertType"]').attrib['value'].lower()
782 url = root.find('./What/Group/Param[@name="joint_skymap_fits"]')
783 if url is None:
784 continue
785 url = url.attrib['value']
786 _, filename = os.path.split(url)
787 issued_time = astropy.time.Time(
788 root.find('./Who/Date').text).gps
789 if filename not in combined_skymaps:
790 combined_skymaps[filename] = dict(
791 alert_type=alert_type,
792 filename=filename,
793 latency=issued_time-event_time.gps)
795 if combined_skymaps:
796 combined_skymaps = list(combined_skymaps.values())
797 combined_skymap = combined_skymaps[-1]['filename']
798 cls = [50, 90]
799 include_ellipse, ra, dec, a, b, pa, area, greedy_area = \
800 uncertainty_ellipse(gracedb_id, combined_skymap, client,
801 cls=cls)
802 kwargs.update(
803 combined_skymap=combined_skymap,
804 combined_skymaps=combined_skymaps,
805 cl=cls[-1],
806 combined_skymap_include_ellipse=include_ellipse,
807 combined_skymap_ra=coord.Longitude(ra*u.deg),
808 combined_skymap_dec=coord.Latitude(dec*u.deg),
809 combined_skymap_a=coord.Angle(a*u.deg),
810 combined_skymap_b=coord.Angle(b*u.deg),
811 combined_skymap_pa=coord.Angle(pa*u.deg),
812 combined_skymap_ellipse_area=area,
813 combined_skymap_greedy_area=greedy_area)
815 return kwargs