Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# (c) Stefan Countryman, August 2017-2018
3"""
4``SkymapInfo`` records basic information about GW triggers/skymaps being used for
5a LLAMA analysis in a uniform, simple format.
6"""
8import os
9import json
10import logging
11import datetime
12import traceback
13from llama.utils import (
14 utc2gps,
15 gps2utc,
16 gps2mjd,
17 GenerationError,
18)
19from llama.filehandler import (
20 EventTriggeredFileHandler,
21 JSONFile,
22)
23from llama.filehandler.mixins import ObservingVetoMixin
24from llama.com.gracedb import GraceDb, HTTPError
25from llama.files.skymap_info.utils import (
26 is_super,
27 gracedb_human_file_url,
28)
29from llama.files.lvc_skymap.utils import (
30 available_skymaps,
31 SkymapFilename,
32)
34# file handlers which can provide the info contained in this alert
35from llama.files.lvc_gcn_xml import LvcGcnXml
36from llama.files.lvalert_json import LVAlertJSON
38LOGGER = logging.getLogger(__name__)
39TEN_PER_DAY_HZ = 10/86400.
40GRACEDB_SUPER_URL_FORMAT = "https://gracedb.ligo.org/superevents/{}"
41GRACEDB_EVENT_URL_FORMAT = "https://gracedb.ligo.org/events/{}/view"
44def event_far_greater_than_10_per_day(eventdir):
45 """Veto if the FAR defined in ``SkymapInfo`` is greater than ten
46 day."""
47 rundir, eventid = os.path.split(eventdir)
48 skyinfo = SkymapInfo(eventid, rundir=rundir)
49 return skyinfo.far > TEN_PER_DAY_HZ
52# pylint: disable=too-few-public-methods
53class FarThresholdMixin(ObservingVetoMixin):
54 """
55 An ``ObservingVetoMixin`` that only triggers on plausible-seeming events
56 whose FAR is less than 10 per day.
57 """
59 class_vetoes = (
60 (event_far_greater_than_10_per_day, None),
61 )
64def is_regular_graceid(graceid):
65 """Check whether this is a normal GraceID (by seeing whether ``is_super``
66 throws an error)."""
67 try:
68 is_super(graceid)
69 return True
70 except ValueError:
71 return False
74def gracedb_url(graceid):
75 """Return the GraceDB URL for this GraceID (assuming it exists and is in a
76 recognized GraceID format). If this doesn't look like a valid GraceID,
77 returns ``None``."""
78 try:
79 if is_super(graceid):
80 return GRACEDB_SUPER_URL_FORMAT.format(graceid)
81 return GRACEDB_EVENT_URL_FORMAT.format(graceid)
82 except ValueError:
83 return None
86@JSONFile.set_class_attributes
87class SkymapInfo(EventTriggeredFileHandler, JSONFile):
88 """
89 A JSON file containing information on where to find the latest and
90 greatest skymap for this event. Any incoming alert file should also
91 generate this file. The resulting file is a simple JSON object with
92 key:value pairs concerning information about the event and the current
93 skymap to be used.
94 """
96 FILENAME = 'skymap_info.json'
98 _required_keys = [
99 'event_time_iso',
100 'graceid',
101 'far',
102 'notice_time_iso',
103 'pipeline',
104 'skymap_filename',
105 'alert_type'
106 ]
108 def generate_from_lvc_gcn_xml(self, voe: LvcGcnXml):
109 """Generate this file from ``LvcGcnXml`` VOEvent. Used for
110 implementation."""
111 self.generate(
112 event_time_iso=voe.event_time_str,
113 graceid=voe.graceid,
114 far=voe.far,
115 notice_time_iso=voe.notice_time_str,
116 pipeline=voe.pipeline,
117 skymap_filename=voe.skymap_filename,
118 alert_type=voe.alert_type,
119 )
121 @property
122 def is_super(self):
123 """Check whether this GraceID corresponds to an event or a
124 superevent."""
125 return is_super(self.graceid)
127 def generate_from_gracedb_superevent(self, graceid: str,
128 skymap_filename: str = None):
129 """Supplement the original filename with data pulled from the GraceDB
130 API on the superevent. Used for manually generating ``skymap_info``
131 from a superevent. If no skymap is provided, pull down the most
132 recently uploaded one defined in
133 ``llama.files.lvc_skymap.SKYMAP_FILENAMES``."""
134 superevent = GraceDb().superevent(graceid).json()
135 preferred = GraceDb().event(superevent['preferred_event']).json()
136 if skymap_filename is None:
137 try:
138 skymap = available_skymaps(graceid)[-1]
139 except IndexError:
140 msg = f"No skymaps available for {graceid}"
141 LOGGER.error(msg)
142 raise GenerationError(msg)
143 LOGGER.info("No skymap selected, using most recent available one: "
144 "%s", skymap)
145 skymap_filename = '{},{}'.format(skymap['filename'],
146 skymap['file_version'])
147 self.generate(
148 graceid=graceid,
149 event_time_iso=gps2utc(preferred['gpstime']),
150 far=preferred['far'],
151 notice_time_iso=datetime.datetime.utcnow().isoformat(),
152 pipeline=preferred['pipeline'].upper(),
153 skymap_filename=skymap_filename,
154 alert_type='manual'
155 )
157 def generate_from_gracedb(self, graceid: str, skymap_filename: str = None):
158 """Supplement the original filename with data pulled from the GraceDB
159 API on the event. Used for manually generating ``skymap_info``."""
160 gdb_event = GraceDb().event(graceid).json()
161 if skymap_filename is None:
162 skymap = available_skymaps(graceid)[-1]
163 LOGGER.info("No skymap selected, using most recent available one: "
164 "%s", skymap)
165 skymap_filename = '{},{}'.format(skymap['filename'],
166 skymap['file_version'])
167 self.generate(
168 graceid=graceid,
169 event_time_iso=gps2utc(gdb_event['gpstime']),
170 far=gdb_event['far'],
171 notice_time_iso=datetime.datetime.utcnow().isoformat(),
172 pipeline=gdb_event['pipeline'].upper(),
173 skymap_filename=skymap_filename,
174 alert_type='manual'
175 )
177 # allowed values of ``alert_type``
178 _alert_types = [
179 'lvalert',
180 'gcn',
181 'gcn-initial',
182 'gcn-preliminary',
183 'gcn-update',
184 'gstlal-online',
185 'gstlal-offline',
186 'manual'
187 ]
189 # pylint: disable=arguments-differ
190 def _generate(self, **kwargs):
191 """Take a dictionary with the key:value pairs required to specify an
192 event and a skymap sufficiently for the pipeline to operate (as defined
193 in the _required_keys property of this class). The arguments are
194 passed as keyword arguments. Extra arguments will be ignored. All
195 required arguments MUST be provided. The ``skymap_filename`` will be
196 canonicalized (if it is not already) to lock in the version of the
197 specified skymap available at creation time (or, if no version is
198 available, the 0th version)."""
199 info = dict()
200 try:
201 for key in self._required_keys:
202 info[key] = kwargs[key]
203 except KeyError:
204 raise ValueError('Must provide keyword arg: {}'.format(key))
205 skymap = SkymapFilename(info['skymap_filename'])
206 # try canonicalizing the filename if possible
207 try:
208 info['skymap_filename'] = skymap.canonicalize(info['graceid'])
209 except (KeyError, HTTPError):
210 LOGGER.warning("Could not canonicalize skymap %s for event %s. "
211 "Traceback:", info['skymap_filename'],
212 info['graceid'])
213 LOGGER.error(traceback.format_exc())
214 # make sure the alert_type is an allowed value
215 if not info['alert_type'] in self._alert_types:
216 msg = ('``alert_type`` must be one of: {}, instead got: '
217 '{}').format(self._alert_types, info['alert_type'])
218 LOGGER.error(msg)
219 raise ValueError(msg)
220 self._write_json(info)
222 @property
223 def event_time_str(self):
224 """Get a unicode string with the ISO date and time of the observed
225 event straight from the VOEvent file. UTC time."""
226 return self.read_json()['event_time_iso']
228 @property
229 def event_time_gps_seconds(self):
230 """Get the time of the observed event in GPS seconds (truncated to the
231 nearest second)."""
232 return int(utc2gps(self.event_time_str))
234 @property
235 def event_time_gps_nanoseconds(self):
236 """Get the number of nanoseconds past the last GPS second at the time
237 of the observed event. Ostensibly provides nanosecond precision, but is
238 less accurate than this in practice."""
239 return self.event_time_gps - self.event_time_gps_seconds
241 @property
242 def event_time_gps(self):
243 """Get the time of the observed event up to nanosecond precision (less
244 accurate than this in practice)."""
245 return utc2gps(self.event_time_str)
247 @property
248 def event_time_mjd(self):
249 """Get the time of the observed event in Modified Julian Day format
250 (UTC time)."""
251 return gps2mjd(self.event_time_gps)
253 @property
254 def notice_time_str(self):
255 """Get a unicode string with the date and time of creation of the
256 notification associated with this VOEvent, rather than the time of
257 detection of the event itself."""
258 return self.read_json()['notice_time_iso']
260 @property
261 def far(self):
262 """Get the false alarm rate of this VOEvent as a float value."""
263 return self.read_json()['far']
265 @property
266 def graceid(self):
267 """Get the GraceID corresponding to this VOEvent."""
268 return self.read_json()['graceid']
270 @property
271 def gracedb_url(self):
272 """Get the GraceDB URL for this GraceID, assuming it is in the expected
273 format for GraceIDs. Will autodetect if this is a superevent or regular
274 event. Returns ``None`` if this doesn't look like a valid GraceID (but
275 will not check on the actual gracedb server)."""
276 return gracedb_url(self.graceid)
278 @property
279 def pipeline(self):
280 """Return an ALL-CAPS name of the pipeline used to generate this event,
281 e.g. GSTLAL or CWB."""
282 return self.read_json()['pipeline']
284 @property
285 def skymap_filename(self):
286 """Get the filename of the skymap as a ``SkymapFilename`` as stored on
287 GraceDB."""
288 return SkymapFilename(self.read_json()['skymap_filename'])
290 @property
291 def human_url(self):
292 """Get the human-viewable URL for this file (i.e. the one that can be
293 accessed from a web-browser using interactive authentication)"""
294 return gracedb_human_file_url(self.graceid, self.skymap_filename)
296 @property
297 def alert_type(self):
298 """Get the type of the alert, i.e. how this trigger was made in the
299 LLAMA pipeline. For example, if this trigger was made as part of a
300 gstlal offline subthreshold search, the value would be
301 "gstlal-offline". Look in ``SkymapInfo._alert_types`` for allowed
302 alert type values.
304 **NB: This is NOT related to the ``alert_type`` key in LVAlerts!**
305 """
306 return self.read_json()['alert_type']
308 @property
309 def alert_file_handler(self):
310 """Get the file handler for this alert file type. For a
311 manually-generated instance of this file, the return value is ``None``;
312 otherwise, it is a FileHandler instance matched to this event."""
313 alert_type = self.alert_type
314 if alert_type.startswith('gcn'):
315 return LvcGcnXml(self)
316 if alert_type == 'lvalert':
317 return LVAlertJSON(self)
318 # default case
319 if alert_type in self._alert_types:
320 return None
321 raise ValueError('Invalid alert_type value: {}'.format(alert_type))