Coverage for ligo/followup_advocate/__init__.py: 91%
415 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-03 18:30 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-03 18:30 +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 }
41 try:
42 return skyloc_pipelines_dict[filename.split('.')[0].lower()]
43 except KeyError:
44 return filename.split('.')[0]
47def text_width(remove_text_wrap):
48 """Return width of text wrap based on whether we wish to wrap the lines or
49 not."""
50 return 9999 if remove_text_wrap else 79
53def main_dict(gracedb_id, client, raven_coinc=False):
54 """Create general dictionary to pass to compose circular"""
56 superevent = client.superevent(gracedb_id).json()
57 preferred_event = superevent['preferred_event_data']
58 pipeline = preferred_event['pipeline']
59 search = preferred_event['search']
60 preferred_pipeline_search = f'{pipeline}_{search}'
61 early_warning_pipeline_searches = []
62 pipeline_searches = []
63 gw_events = superevent['gw_events']
64 early_warning_alert = False
66 for gw_event in gw_events:
67 gw_event_dict = client.event(gw_event).json()
68 pipeline = gw_event_dict['pipeline']
69 search = gw_event_dict['search']
70 # Remap MDC to allsky to not double count
71 if search == 'MDC':
72 search = 'AllSky'
73 pipeline_search = f'{pipeline}_{search}'
74 # Create separates lists of post merger and early warning pipelines
75 if pipeline_search not in pipeline_searches and \
76 'EarlyWarning' not in pipeline_search:
77 pipeline_searches.append(pipeline_search)
78 if pipeline_search not in early_warning_pipeline_searches and \
79 'EarlyWarning' in pipeline_search:
80 # Remap MBTA early warning to the all sky citation
81 if 'mbta' in pipeline_search.lower():
82 pipeline_search = 'MBTA_AllSky'
83 early_warning_pipeline_searches.append(pipeline_search)
84 # Sort to get alphabetical order
85 pipeline_searches.sort(key=str.lower)
86 early_warning_pipeline_searches.sort(key=str.lower)
88 voevents = client.voevents(gracedb_id).json()['voevents']
89 if not voevents:
90 raise ValueError(
91 "{} has no VOEvent to generate circulars from.".format(
92 gracedb_id))
94 citation_index = {
95 pipeline_search.lower(): pipeline_searches.index(pipeline_search) + 1
96 for pipeline_search in pipeline_searches}
97 for pipeline_search in early_warning_pipeline_searches:
98 if pipeline_search.lower() not in citation_index:
99 citation_index[pipeline_search.lower()] = \
100 max(citation_index.values()) + 1
102 gpstime = float(preferred_event['gpstime'])
103 event_time = astropy.time.Time(gpstime, format='gps').utc
105 # Grab latest p_astro and em_bright values from lastest VOEvent
106 voevent_text_latest = \
107 client.files(gracedb_id, voevents[-1]['filename']).read()
108 root = lxml.etree.fromstring(voevent_text_latest)
109 p_astros = root.find('./What/Group[@name="Classification"]')
110 em_brights = root.find('./What/Group[@name="Properties"]')
111 classifications = {}
112 source_classification = {}
113 mchirp_bin = {}
114 # Only try to load if present to prevent errors with .getchildren()
115 p_astro_pipeline = None
117 # Grab logs for either RapidPE checks or chirp mass bins
118 if p_astros or em_brights:
119 logs = client.logs(gracedb_id).json()['log']
121 if p_astros:
122 for p_astro in p_astros.getchildren():
123 if p_astro.attrib.get('value'):
124 classifications[p_astro.attrib['name']] = \
125 float(p_astro.attrib['value']) * 100
126 # Figure out which pipeline uploaded p_astro, usually the first one
127 # FIXME: Replace with more efficient method in the future
128 for message in reversed(logs):
129 filename = message['filename']
130 if filename and '.p_astro.json' in filename and \
131 filename != 'p_astro.json':
132 p_astro = client.files(gracedb_id, filename).json()
133 if all(mth.isclose(p_astro[key] * 100, classifications[key])
134 for key in classifications.keys()):
135 p_astro_pipeline = filename.split('.')[0].lower()
136 break
137 if p_astro_pipeline == 'rapidpe_rift':
138 citation_index['rapidpe_rift'] = max(citation_index.values()) + 1
139 if em_brights:
140 for em_bright in em_brights.getchildren():
141 if em_bright.attrib.get('value'):
142 source_classification[em_bright.attrib['name']] = \
143 float(em_bright.attrib['value']) * 100
144 if len(source_classification) > 0:
145 citation_index['em_bright'] = max(citation_index.values()) + 1
147 # chirp mass estimates included when em-bright is
148 for message in reversed(logs):
149 filename = message['filename']
150 # use most recent mchirp estimate
151 if filename and 'mchirp_source' in filename:
152 cgmi_json = client.files(gracedb_id, filename).json()
153 mchirp_bin_edges = cgmi_json['bin_edges']
154 mchirp_probs = cgmi_json['probabilities']
155 # find the highest probability bin
156 max_prob_idx = np.argmax(mchirp_probs)
157 left_bin = mchirp_bin_edges[max_prob_idx]
158 right_bin = mchirp_bin_edges[max_prob_idx+1]
159 mchirp_bin = left_bin, right_bin
160 break
162 skymaps = {}
163 voevents_text = []
164 for voevent in voevents:
165 # Don't load latest voevent since already loaded from before
166 if voevent == voevents[-1]:
167 voevent_text = voevent_text_latest
168 # If earlier voevent, load
169 else:
170 voevent_text = client.files(gracedb_id, voevent['filename']).read()
171 root = lxml.etree.fromstring(voevent_text)
172 alert_type = root.find(
173 './What/Param[@name="AlertType"]').attrib['value'].lower()
174 if alert_type == 'earlywarning':
175 # Add text for early warning detection if one early warning alert
176 early_warning_alert = True
177 url = root.find('./What/Group/Param[@name="skymap_fits"]')
178 if url is None:
179 continue
180 url = url.attrib['value']
181 _, filename = os.path.split(url)
182 skyloc_pipeline = guess_skyloc_pipeline(filename)
183 issued_time = astropy.time.Time(root.find('./Who/Date').text).gps
184 if filename not in skymaps:
185 skymaps[filename] = dict(
186 alert_type=alert_type,
187 pipeline=skyloc_pipeline,
188 filename=filename,
189 latency=issued_time-event_time.gps)
190 if skyloc_pipeline.lower() not in citation_index:
191 citation_index[skyloc_pipeline.lower()] = \
192 max(citation_index.values()) + 1
193 voevents_text.append(voevent_text)
194 skymaps = list(skymaps.values())
196 o = urllib.parse.urlparse(client.service_url)
198 kwargs = dict(
199 subject='Identification',
200 gracedb_id=gracedb_id,
201 gracedb_service_url=urllib.parse.urlunsplit(
202 (o.scheme, o.netloc, '/superevents/', '', '')),
203 group=preferred_event['group'],
204 pipeline_search=preferred_pipeline_search,
205 all_pipeline_searches=pipeline_searches,
206 early_warning_alert=early_warning_alert,
207 early_warning_pipeline_searches=early_warning_pipeline_searches,
208 gpstime='{0:.03f}'.format(round(float(preferred_event['gpstime']), 3)),
209 search=preferred_event.get('search', ''),
210 far=preferred_event['far'],
211 utctime=event_time.iso,
212 instruments=preferred_event['instruments'].split(','),
213 skymaps=skymaps,
214 prob_has_ns=source_classification.get('HasNS'),
215 prob_has_remnant=source_classification.get('HasRemnant'),
216 prob_has_massgap=source_classification.get('HasMassGap'),
217 prob_has_ssm=source_classification.get('HasSSM'),
218 source_classification=source_classification,
219 mchirp_bin=mchirp_bin,
220 include_ellipse=None,
221 classifications=classifications,
222 p_astro_pipeline=p_astro_pipeline,
223 citation_index=citation_index)
225 if skymaps:
226 preferred_skymap = skymaps[-1]['filename']
227 cls = [50, 90]
228 include_ellipse, ra, dec, a, b, pa, area, greedy_area = \
229 uncertainty_ellipse(gracedb_id, preferred_skymap, client, cls=cls)
230 kwargs.update(
231 preferred_skymap=preferred_skymap,
232 cl=cls[-1],
233 include_ellipse=include_ellipse,
234 ra=coord.Longitude(ra*u.deg),
235 dec=coord.Latitude(dec*u.deg),
236 a=coord.Angle(a*u.deg),
237 b=coord.Angle(b*u.deg),
238 pa=coord.Angle(pa*u.deg),
239 ellipse_area=area,
240 greedy_area=greedy_area)
241 try:
242 distmu, distsig = get_distances_skymap_gracedb(gracedb_id,
243 preferred_skymap,
244 client)
245 kwargs.update(
246 distmu=distmu,
247 distsig=distsig)
248 except TypeError:
249 pass
251 if raven_coinc:
252 kwargs = _update_raven_parameters(superevent, kwargs, client,
253 voevents_text)
254 return kwargs
257def compose(gracedb_id, authors=(), mailto=False, remove_text_wrap=False,
258 service=rest.DEFAULT_SERVICE_URL, client=None):
259 """Compose GCN Circular draft"""
261 if client is None:
262 client = rest.GraceDb(service)
264 kwargs = main_dict(gracedb_id, client=client)
265 kwargs.update(authors=authors)
266 kwargs.update(change_significance_statement=False)
267 kwargs.update(text_width=text_width(remove_text_wrap))
269 subject = env.get_template('subject.jinja2').render(**kwargs).strip()
270 body = env.get_template('initial_circular.jinja2').render(**kwargs).strip()
272 if mailto:
273 pattern = 'mailto:emfollow@ligo.org,dac@ligo.org?subject={0}&body={1}'
274 url = pattern.format(
275 urllib.parse.quote(subject),
276 urllib.parse.quote(body))
277 webbrowser.open(url)
278 else:
279 return '{0}\n\n{1}'.format(subject, body)
282def compose_raven(gracedb_id, authors=(), remove_text_wrap=False,
283 service=rest.DEFAULT_SERVICE_URL, client=None):
284 """Compose EM_COINC RAVEN GCN Circular draft"""
286 if client is None:
287 client = rest.GraceDb(service)
289 kwargs = dict()
290 kwargs.update(main_dict(gracedb_id, client=client, raven_coinc=True))
291 kwargs.update(update_alert=False)
292 kwargs.update(text_width=text_width(remove_text_wrap))
293 # Add RAVEN citation
294 citation_index = kwargs['citation_index']
295 citation_index['raven'] = max(citation_index.values()) + 1
296 kwargs['citation_index'] = citation_index
298 subject = (
299 env.get_template('RAVEN_subject.jinja2').render(**kwargs)
300 .strip())
301 body = (
302 env.get_template('RAVEN_circular.jinja2').render(**kwargs)
303 .strip())
304 return '{0}\n\n{1}'.format(subject, body)
307def compose_llama(
308 gracedb_id, authors=(), service=rest.DEFAULT_SERVICE_URL,
309 icecube_alert=None, remove_text_wrap=False,
310 client=None):
311 """Compose GRB LLAMA GCN Circular draft.
312 Here, gracedb_id will be a GRB superevent ID in GraceDb."""
314 if client is None:
315 client = rest.GraceDb(service)
317 superevent = client.superevent(gracedb_id).json()
319 gpstime = float(superevent['t_0'])
320 tl, th = gpstime - 500, gpstime + 500
321 event_time = astropy.time.Time(gpstime, format='gps').utc
322 tl_datetime = str(astropy.time.Time(
323 tl, format='gps').isot).replace('T', ' ')
324 th_datetime = str(astropy.time.Time(
325 th, format='gps').isot).replace('T', ' ')
327 o = urllib.parse.urlparse(client.service_url)
328 kwargs = dict(
329 gracedb_service_url=urllib.parse.urlunsplit(
330 (o.scheme, o.netloc, '/superevents/', '', '')),
331 gracedb_id=gracedb_id,
332 llama=True,
333 icecube_alert=icecube_alert,
334 event_time=event_time,
335 tl_datetime=tl_datetime,
336 th_datetime=th_datetime,
337 authors=authors)
338 kwargs.update(text_width=text_width(remove_text_wrap))
340 citation_index = {'llama': 1, 'llama_stat': 2}
341 kwargs.update(citation_index=citation_index)
343 files = client.files(gracedb_id).json()
345 llama_stat_filename = 'significance_subthreshold_lvc-i3.json'
346 if llama_stat_filename in files:
347 llama_stat_file = client.files(gracedb_id, llama_stat_filename).json()
348 llama_fap = llama_stat_file["p_value"]
349 neutrinos = llama_stat_file["inputs"]["neutrino_info"]
350 lines = []
351 for neutrino in neutrinos:
352 # Build aligned string
353 vals = []
354 dt = (event_time -
355 astropy.time.Time(neutrino['mjd'],
356 format='mjd')).to(u.s).value
357 vals.append('{:.2f}'.format(dt))
358 vals.append('{:.2f}'.format(neutrino['ra']))
359 vals.append('{:.2f}'.format(neutrino['dec']))
360 vals.append('{:.2f}'.format(neutrino['sigma']))
361 vals.append('{:.4f}'.format(llama_fap))
362 lines.append(align_number_string(vals, [0, 11, 21, 40, 59]))
364 kwargs.update(llama_fap=llama_fap,
365 neutrinos=lines)
367 subject = (
368 env.get_template('llama_subject.jinja2').render(**kwargs)
369 .strip())
370 if icecube_alert:
371 body = (
372 env.get_template('llama_alert_circular.jinja2').render(**kwargs)
373 .strip())
374 else:
375 body = (
376 env.get_template('llama_track_circular.jinja2').render(**kwargs)
377 .strip())
378 return '{0}\n\n{1}'.format(subject, body)
381def compose_grb_medium_latency(
382 gracedb_id, authors=(), service=rest.DEFAULT_SERVICE_URL,
383 use_detection_template=None, remove_text_wrap=False, client=None):
384 """Compose GRB Medium Latency GCN Circular draft.
385 Here, gracedb_id will be a GRB external trigger ID in GraceDb."""
387 if client is None:
388 client = rest.GraceDb(service)
390 event = client.event(gracedb_id).json()
391 search = event['search']
393 if search != 'GRB':
394 return
396 group = event['group']
397 pipeline = event['pipeline']
398 external_trigger = event['extra_attributes']['GRB']['trigger_id']
400 o = urllib.parse.urlparse(client.service_url)
401 kwargs = dict(
402 gracedb_service_url=urllib.parse.urlunsplit(
403 (o.scheme, o.netloc, '/events/', '', '')),
404 gracedb_id=gracedb_id,
405 group=group,
406 grb=True,
407 pipeline=pipeline,
408 external_trigger=external_trigger,
409 exclusions=[],
410 detections=[])
411 kwargs.update(text_width=text_width(remove_text_wrap))
413 files = client.files(gracedb_id).json()
415 citation_index = {}
416 index = 1
417 xpipeline_fap_file = 'false_alarm_probability_x.json'
418 if xpipeline_fap_file in files:
419 xpipeline_fap = client.files(gracedb_id, xpipeline_fap_file).json()
420 online_xpipeline_fap = xpipeline_fap.get('Online Xpipeline')
421 # Create detection/exclusion circular based on given argument
422 # Use default cutoff if not provided
423 xpipeline_detection = (use_detection_template if use_detection_template
424 is not None else online_xpipeline_fap < 0.001)
425 if xpipeline_detection:
426 kwargs['detections'].append('xpipeline')
427 kwargs.update(online_xpipeline_fap=online_xpipeline_fap)
428 else:
429 kwargs['exclusions'].append('xpipeline')
430 xpipeline_distances_file = 'distances_x.json'
431 xpipeline_distances = client.files(gracedb_id,
432 xpipeline_distances_file).json()
433 burst_exclusion = xpipeline_distances.get('Burst Exclusion')
434 kwargs.update(burst_exclusion=burst_exclusion)
435 citation_index['xpipeline'] = index
436 index += 1
438 pygrb_fap_file = 'false_alarm_probability_pygrb.json'
439 if pygrb_fap_file in files:
440 pygrb_fap = client.files(gracedb_id, pygrb_fap_file).json()
441 online_pygrb_fap = pygrb_fap.get('Online PyGRB')
442 # Create detection/exclusion circular based on given argument
443 # Use default cutoff if not provided
444 pygrb_detection = (use_detection_template if use_detection_template
445 is not None else online_pygrb_fap < 0.01)
446 if pygrb_detection:
447 kwargs['detections'].append('pygrb')
448 kwargs.update(online_pygrb_fap=online_pygrb_fap)
449 else:
450 kwargs['exclusions'].append('pygrb')
451 pygrb_distances_file = 'distances_pygrb.json'
452 pygrb_distances = client.files(gracedb_id,
453 pygrb_distances_file).json()
454 nsns_exclusion = pygrb_distances.get('NSNS Exclusion')
455 nsbh_exclusion = pygrb_distances.get('NSBH Exclusion')
456 kwargs.update(nsbh_exclusion=nsbh_exclusion,
457 nsns_exclusion=nsns_exclusion)
458 citation_index['pygrb'] = index
460 kwargs.update(citation_index=citation_index)
462 subject = (
463 env.get_template('medium_latency_grb_subject.jinja2').render(**kwargs)
464 .strip())
465 body = (
466 env.get_template('medium_latency_grb_circular.jinja2').render(**kwargs)
467 .strip())
468 return '{0}\n\n{1}'.format(subject, body)
471def compose_update(gracedb_id, authors=(),
472 service=rest.DEFAULT_SERVICE_URL,
473 update_types=['sky_localization', 'p_astro',
474 'em_bright', 'raven'],
475 remove_text_wrap=False,
476 client=None):
477 """Compose GCN update circular"""
478 if client is None:
479 client = rest.GraceDb(service)
481 kwargs = main_dict(gracedb_id, client=client,
482 raven_coinc='raven' in update_types)
483 kwargs.pop('citation_index', None)
484 kwargs.update(text_width=text_width(remove_text_wrap))
486 if isinstance(update_types, str):
487 update_types = update_types.split(',')
489 # Adjust files for update type alert
490 citation_index = {}
491 skymaps = []
492 index = 1
494 updated_skymap = kwargs.get('skymaps')[-1]
495 kwargs.update(updated_skymap=updated_skymap)
496 skymaps = [updated_skymap]
497 if 'sky_localization' in update_types:
498 citation_index[updated_skymap['pipeline'].lower()] = index
499 index += 1
500 if 'p_astro' in update_types and \
501 kwargs.get('p_astro_pipeline') == 'rapidpe_rift':
502 citation_index['rapidpe_rift'] = index
503 index += 1
504 if 'em_bright' in update_types:
505 # If not already cited, cite sky map pipeline for em_bright
506 if updated_skymap['pipeline'].lower() not in citation_index.keys():
507 citation_index[updated_skymap['pipeline'].lower()] = index
508 index += 1
509 citation_index['em_bright'] = index
510 index += 1
511 if 'raven' in update_types:
512 citation_index['raven'] = index
514 kwargs.update(skymaps=skymaps,
515 citation_index=citation_index,
516 all_pipeline_searches=[],
517 update_alert=True)
519 kwargs.update(authors=authors)
520 kwargs.update(change_significance_statement=False)
521 kwargs.update(subject='Update')
522 kwargs.update(update_types=update_types)
524 subject = env.get_template('subject.jinja2').render(**kwargs).strip()
525 body = env.get_template(
526 'update_circular.jinja2').render(**kwargs).strip()
527 return '{0}\n\n{1}'.format(subject, body)
530def compose_retraction(gracedb_id, authors=(), remove_text_wrap=False,
531 service=rest.DEFAULT_SERVICE_URL, client=None):
532 """Compose GCN retraction circular"""
533 if client is None:
534 client = rest.GraceDb(service)
535 event = client.superevent(gracedb_id).json()
536 preferred_event = event['preferred_event_data']
537 labels = event['labels']
538 earlywarning = \
539 ('EARLY_WARNING' in labels and
540 {'EM_SelectedConfident', 'SIGNIF_LOCKED'}.isdisjoint(labels))
542 kwargs = dict(
543 subject='Retraction',
544 gracedb_id=gracedb_id,
545 group=preferred_event['group'],
546 earlywarning=earlywarning,
547 authors=authors
548 )
549 kwargs.update(text_width=text_width(remove_text_wrap))
551 subject = env.get_template('subject.jinja2').render(**kwargs).strip()
552 body = env.get_template('retraction.jinja2').render(**kwargs).strip()
553 return '{0}\n\n{1}'.format(subject, body)
556def read_map_gracedb(graceid, filename, client):
557 with tempfile.NamedTemporaryFile(mode='w+b') as localfile:
558 remotefile = client.files(graceid, filename, raw=True)
559 shutil.copyfileobj(remotefile, localfile)
560 localfile.flush()
561 return read_sky_map(localfile.name, moc=True)
564def get_distances_skymap_gracedb(graceid, filename, client):
565 with tempfile.NamedTemporaryFile(mode='w+b') as localfile:
566 remotefile = client.files(graceid, filename, raw=True)
567 shutil.copyfileobj(remotefile, localfile)
568 localfile.flush()
569 header = getheader(localfile.name, 1)
570 try:
571 return header['distmean'], header['diststd']
572 except KeyError:
573 pass
576def read_map_from_path(path, client):
577 return read_map_gracedb(*path.split('/'), client)[0]
580def align_number_string(nums, positions):
581 positions.append(len(nums[-1]))
582 gen = (val + ' ' * (positions[i+1]-positions[i]-len(val))
583 for i, val in enumerate(nums))
584 return ''.join(gen)
587def mask_cl(p, level=90):
588 pflat = p.ravel()
589 i = np.flipud(np.argsort(p))
590 cs = np.cumsum(pflat[i])
591 cls = np.empty_like(pflat)
592 cls[i] = cs
593 cls = cls.reshape(p.shape)
594 return cls <= 1e-2 * level
597def compare_skymaps(paths, service=rest.DEFAULT_SERVICE_URL, client=None):
598 """Produce table of sky map overlaps"""
599 if client is None:
600 client = rest.GraceDb(service)
601 filenames = [path.split('/')[1] for path in paths]
602 pipelines = [guess_skyloc_pipeline(filename) for filename in filenames]
603 probs = [read_map_from_path(path, client) for path in paths]
604 npix = max(len(prob) for prob in probs)
605 nside = hp.npix2nside(npix)
606 deg2perpix = hp.nside2pixarea(nside, degrees=True)
607 probs = [hp.ud_grade(prob, nside, power=-2) for prob in probs]
608 masks = [mask_cl(prob) for prob in probs]
609 areas = [mask.sum() * deg2perpix for mask in masks]
610 joint_areas = [(mask & masks[-1]).sum() * deg2perpix for mask in masks]
612 kwargs = dict(params=zip(filenames, pipelines, areas, joint_areas))
614 return env.get_template('compare_skymaps.jinja2').render(**kwargs)
617def uncertainty_ellipse(graceid, filename, client, cls=[50, 90],
618 ratio_ellipse_cl_areas=1.35):
619 """Compute uncertainty ellipses for a given sky map
621 Parameters
622 ----------
623 graceid: str
624 ID of the trigger used by GraceDB
625 filename: str
626 File name of sky map
627 client: class
628 REST API client for HTTP connection
629 cls: array-like
630 List of percentage of minimal credible area used to check whether the
631 areas are close to an ellipse, returning the values of the final item
632 ratio_ellipse_cl_areas: float
633 Ratio between ellipse area and minimal credible area from cl
634 """
635 if filename.endswith('.gz'):
636 # Try using the multi-res sky map if it exists
637 try:
638 new_filename = filename.replace('.fits.gz', '.multiorder.fits')
639 skymap = read_map_gracedb(graceid, new_filename, client)
640 except (IOError, rest.HTTPError):
641 skymap = read_map_gracedb(graceid, filename, client)
642 else:
643 skymap = read_map_gracedb(graceid, filename, client)
645 # Convert to an array if necessary
646 if np.isscalar(cls):
647 cls = [cls]
648 cls = np.asarray(cls)
650 # Pass array of contour inteverals to get areas
651 result = crossmatch(skymap, contours=cls / 100)
652 greedy_areas = np.asarray(result.contour_areas)
653 ra, dec, a, b, pa, ellipse_areas = find_ellipse(skymap, cl=cls)
654 a, b = np.asarray(a), np.asarray(b)
656 # Only use ellipse if every confidence interval passes
657 use_ellipse = \
658 np.all(ellipse_areas <= ratio_ellipse_cl_areas * greedy_areas)
659 return (use_ellipse, ra, dec, a[-1], b[-1], pa, ellipse_areas[-1],
660 greedy_areas[-1])
663def _update_raven_parameters(superevent, kwargs, client, voevents_text):
664 """Update kwargs with parameters for RAVEN coincidence"""
666 gracedb_id = superevent['superevent_id']
668 if 'EM_COINC' not in superevent['labels']:
669 raise ValueError("No EM_COINC label for {}".format(
670 gracedb_id))
672 preferred_event = superevent['preferred_event_data']
673 group = preferred_event['group']
674 gpstime = float(preferred_event['gpstime'])
675 event_time = astropy.time.Time(gpstime, format='gps').utc
676 em_event_id = superevent['em_type']
678 # FIXME: Grab more info from the latest VOEvent if deemed suitable
679 em_event = client.event(em_event_id).json()
680 external_pipeline = em_event['pipeline']
681 # Get all other pipelines
682 ext_events = [client.event(id).json() for id
683 in superevent['em_events']]
684 # Remove duplicates and vetoed events
685 other_ext_pipelines = \
686 [*set(event['pipeline'] for event in ext_events
687 if 'NOT_GRB' not in event['labels'])]
688 # Remove preferred pipeline
689 other_ext_pipelines.remove(external_pipeline)
690 # FIXME in GraceDb: Even SNEWS triggers have an extra attribute GRB.
691 external_trigger_id = em_event['extra_attributes']['GRB']['trigger_id']
692 snews = (em_event['pipeline'] == 'SNEWS')
693 grb = (em_event['search'] in ['GRB', 'SubGRB', 'SubGRBTargeted', 'MDC']
694 and not snews)
695 subthreshold = em_event['search'] in ['SubGRB', 'SubGRBTargeted']
696 subthreshold_targeted = em_event['search'] == 'SubGRBTargeted'
697 far_grb = em_event['far']
699 voevent_text_latest = voevents_text[-1]
700 root = lxml.etree.fromstring(voevent_text_latest)
701 time_diff = \
702 root.find('./What/Group/Param[@name="Time_Difference"]')
703 time_diff = float(time_diff.attrib['value'])
705 o = urllib.parse.urlparse(client.service_url)
706 kwargs.update(
707 gracedb_service_url=urllib.parse.urlunsplit(
708 (o.scheme, o.netloc, '/superevents/', '', '')),
709 gracedb_id=gracedb_id,
710 group=group,
711 external_pipeline=external_pipeline,
712 external_trigger=external_trigger_id,
713 snews=snews,
714 grb=grb,
715 subthreshold=subthreshold,
716 subthreshold_targeted=subthreshold_targeted,
717 other_ext_pipelines=sorted(other_ext_pipelines),
718 far_grb=far_grb,
719 latency=abs(round(time_diff, 1)),
720 beforeafter='before' if time_diff < 0 else 'after')
722 if grb:
723 # Grab GRB coincidence FARs
724 time_coinc_far = superevent['time_coinc_far']
725 space_time_coinc_far = superevent['space_coinc_far']
726 kwargs.update(
727 time_coinc_far=time_coinc_far,
728 space_time_coinc_far=space_time_coinc_far,
729 ext_ra=em_event['extra_attributes']['GRB']['ra'],
730 ext_dec=em_event['extra_attributes']['GRB']['dec'],
731 ext_error=em_event['extra_attributes']['GRB']['error_radius'])
733 # Find combined sky maps for GRB
734 combined_skymaps = {}
735 for i, voevent_text in enumerate(voevents_text):
736 root = lxml.etree.fromstring(voevent_text)
737 alert_type = root.find(
738 './What/Param[@name="AlertType"]').attrib['value'].lower()
739 # Check if significant GW alert already queued or sent
740 change_significance_statement = \
741 {'EM_SelectedConfident', 'SIGNIF_LOCKED'}.isdisjoint(
742 superevent['labels'])
743 url = root.find('./What/Group/Param[@name="joint_skymap_fits"]')
744 if url is None:
745 continue
746 url = url.attrib['value']
747 _, filename = os.path.split(url)
748 issued_time = astropy.time.Time(
749 root.find('./Who/Date').text).gps
750 if filename not in combined_skymaps:
751 combined_skymaps[filename] = dict(
752 alert_type=alert_type,
753 filename=filename,
754 latency=issued_time-event_time.gps)
756 if combined_skymaps:
757 combined_skymaps = list(combined_skymaps.values())
758 combined_skymap = combined_skymaps[-1]['filename']
759 cls = [50, 90]
760 include_ellipse, ra, dec, a, b, pa, area, greedy_area = \
761 uncertainty_ellipse(gracedb_id, combined_skymap, client,
762 cls=cls)
763 kwargs.update(
764 combined_skymap=combined_skymap,
765 combined_skymaps=combined_skymaps,
766 cl=cls[-1],
767 combined_skymap_include_ellipse=include_ellipse,
768 combined_skymap_ra=coord.Longitude(ra*u.deg),
769 combined_skymap_dec=coord.Latitude(dec*u.deg),
770 combined_skymap_a=coord.Angle(a*u.deg),
771 combined_skymap_b=coord.Angle(b*u.deg),
772 combined_skymap_pa=coord.Angle(pa*u.deg),
773 combined_skymap_ellipse_area=area,
774 combined_skymap_greedy_area=greedy_area,
775 change_significance_statement=change_significance_statement)
777 return kwargs