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, September 8, 2016
3"""
4Class for saving, parsing, and conveniently retrieving data from VOEvent XML
5files distributed by the GCN network and created by LVC describing GW triggers.
6Includes functionality work with multiple schemas of LVC VOEvents from the
7Advanced LIGO era.
8"""
10import os
11from traceback import format_exc
12import logging
13import untangle
14from llama import utils
15from llama.filehandler import EventTriggeredFileHandler
17LOGGER = logging.getLogger(__name__)
20def _payload_error(msg, payload, tb=None):
21 """Log an error message ``msg`` with the unparseable ``payload`` (and
22 traceback ``tb`` if provided) and raise a ``ValueError`` with ``msg``."""
23 LOGGER.exception('%s Payload:\n%s', msg, payload)
24 if tb is not None:
25 LOGGER.exception('Stack trace:\n%s', utils.tbhighlight(tb))
26 raise ValueError(msg)
29def parse_ivorn(payload):
30 """Get identifying information about an LVC VOEvent, raising a
31 ``ValueError`` if the IVORN cannot be parsed. Provides a safe way of
32 handling errors for the GCN listener, which should to the greatest extent
33 possible handle errors due to malformed data from partners.
35 Parameters
36 ----------
37 payload : str
38 The LVC VOEvent in XML string format.
40 Returns
41 -------
42 ivorn : str
43 The event's unique ID, called an "IVORN".
44 eventid : str
45 A combination of GraceID, serial GCN notice number for the event, and
46 notice type for this event that will be used for OPA events in the
47 pipeline.
48 graceid : str
49 The GraceID of the gravitational wave event.
50 serial_no : int
51 The serial number of this GCN Notice, e.g. ``2`` for the second notice
52 related to this particular GraceID.
53 notice_type : str
54 The LVC GCN Notice type for this notice; expected to be one of
55 ``Preliminary``, ``Initial``, ``Update``, or ``Retraction``, though
56 this is not checked.
58 Raises
59 ------
60 ValueError
61 A descriptive error raised if ``root`` is not a ``lxml.etree._Element``
62 describing a valid LVC GCN Notice. The message will describe what part
63 of parsing failed.
65 Examples
66 --------
67 Get the IVORN, eventid, and GraceID from a test event file:
69 >>> from llama.utils import get_test_file
70 >>> with open(get_test_file('lvc_gcn.xml',
71 ... 'MS181101ab-2-Initial')) as xmlfile:
72 ... payload = xmlfile.read()
73 >>> parse_ivorn(payload)
74 ('ivo://gwnet/LVC#MS181101ab-2-Initial', 'MS181101ab-2-Initial', 'MS181101ab', 2, 'Initial')
75 """
76 from lxml.etree import fromstring, XMLSyntaxError
77 try:
78 root = fromstring(payload)
79 except XMLSyntaxError:
80 _payload_error('Could not parse XML payload.', payload, format_exc())
81 ivorn = root.get('ivorn')
82 if ivorn is None:
83 _payload_error('No ``ivorn`` found in VOEvent.', payload)
84 try:
85 _, eventid = ivorn.split('LVC#')
86 except ValueError:
87 _payload_error(f'Unexpected IVORN format: {ivorn}, could not parse '
88 'eventid.', payload, format_exc())
89 try:
90 graceid, serial_no, notice_type = eventid.split('-')
91 except ValueError:
92 _payload_error('Unexpected ``eventid`` format: {eventid} could not '
93 'parse ``graceid``, ``serial_no``, ``notice_type``.',
94 payload, format_exc())
95 try:
96 serial_no = int(serial_no)
97 except ValueError:
98 _payload_error(f'Could not parse serial number as int: {serial_no}',
99 payload, format_exc())
100 return ivorn, eventid, graceid, serial_no, notice_type
103@EventTriggeredFileHandler.set_class_attributes
104class LvcGcnXml(EventTriggeredFileHandler):
105 """
106 A VOEvent XML file received from GCN corresponding to an LVC event
107 notice.
108 """
110 FILENAME = 'lvc_gcn.xml'
112 @property
113 def etree(self):
114 """Return an ``lxml.etree`` for this VOEvent."""
115 if not hasattr(self, '_etree'):
116 from lxml.etree import fromstring
117 with self.open() as voefile:
118 setattr(self, '_etree', fromstring(voefile.read))
119 return self._etree
121 @property
122 def alert_type(self):
123 """Get the alert type for this VOEvent."""
124 return f"gcn-{self.get_alert_type().lower()}"
126 @property
127 def ivorn(self):
128 """Get the IVORN, a unique identifier for this GCN Notice."""
129 return self.etree.get('ivorn')
131 # pylint: disable=W0221
132 def _generate(self, payload):
133 """Take the payload text from a GCN handler and write it to file. Since
134 this might be the first file in an event directory, create the event
135 directory if it does not exist."""
136 if not os.path.exists(self.eventdir):
137 os.makedirs(self.eventdir, mode=755)
138 with open(self.fullpath, "wb") as outfile:
139 outfile.write(payload)
141 @property
142 def event_time_str(self):
143 """Get a unicode string with the ISO date and time of the observed
144 event straight from the VOEvent file. UTC time."""
145 return utils.get_voevent_time(self.fullpath)
147 @property
148 def event_time_gps_seconds(self):
149 """Get the time of the observed event in GPS seconds (truncated to the
150 nearest second)."""
151 return utils.getVOEventGPSSeconds(self.fullpath)
153 @property
154 def event_time_gps_nanoseconds(self):
155 """Get the number of nanoseconds past the last GPS second at the time
156 of the observed event. Ostensibly provides nanosecond precision, but is
157 less accurate than this in practice."""
158 return utils.getVOEventGPSNanoseconds(self.fullpath)
160 @property
161 def event_time_gps(self):
162 """Get the time of the observed event up to nanosecond precision (less
163 accurate than this in practice) in GPS time format."""
164 return utils.get_voevent_gps_time(self.fullpath)
166 @property
167 def event_time_mjd(self):
168 """Get the time of the observed event in UTC MJD format."""
169 return utils.gps2mjd(self.event_time_gps)
171 @property
172 def notice_time_str(self):
173 """Get a unicode string with the date and time of creation of the
174 notification associated with this VOEvent, rather than the time of
175 detection of the event itself. Should be in ISO format."""
176 return utils.get_voevent_notice_time(self.fullpath)
178 @property
179 def role(self):
180 """Get the role of this VOEvent as specified in the header."""
181 return utils.get_voevent_role(self.fullpath)
183 @property
184 def far(self):
185 """Get the false alarm rate of this VOEvent as a float value."""
186 return float(self.get_param('FAR'))
188 @property
189 def graceid(self):
190 """Get the GraceID corresponding to this VOEvent."""
191 return self.get_param('GraceID')
193 @property
194 def pipeline(self):
195 """Return an ALL-CAPS name of the pipeline used to generate this event,
196 e.g. GSTLAL or CWB."""
197 return self.get_param('Pipeline').upper()
199 def get_alert_type(self):
200 """Read the alert type of this GCN Notice."""
201 return utils.get_voevent_param(self.fullpath, 'AlertType')
203 def get_param(self, param):
204 """Get this VOEventParam for this event."""
205 return utils.get_voevent_param(self.fullpath, param)
207 @property
208 def skymap_filename(self):
209 """Get the filename for this skymap as it appears on GraceDB."""
210 return get_skymap_filename(self.fullpath)
213class LvcRetractionXml(LvcGcnXml):
214 """
215 A VOEvent XML file retracting a GCN notice.
216 """
218 FILENAME = 'lvc_gcn_retraction.xml'
221# The VOEvent format keeps changing, and the name of the key for
222# the URL field changes with it. Add updated keys to this array.
223POSSIBLE_URL_KEYS = ['SKYMAP_FITS_BASIC', 'SKYMAP_URL_FITS_BASIC',
224 'skymap_fits']
225POSSIBLE_GROUP_URL_KEYS = ['skymap_fits_basic']
226POSSIBLE_GROUP_TYPE_NAMES = ['GW_SKYMAP']
229def get_filename_from_group(filename, paramname):
230 """old VOEvent format had the skymap URL buried in a Param which
231 itself is contained in a group:
232 VOEvent/What/Group/Param
233 This is mostly the same as get VOEventParam in utils and is
234 rewritten here only to add support for an edge case."""
235 if not os.path.isfile(filename):
236 raise IOError(filename + ': file does not exist.')
237 event = untangle.parse(filename)
238 params = event.voe_VOEvent.What.Group.Param
239 # param = filter(lambda a: a['name'] == paramname, params)[0]['value']
240 param = [p for p in params if p['name'] == paramname][0]['value']
241 return param.split('/')[-1]
244def get_filename_from_group_name(filename, grouptypename, paramname):
245 """some stupid VOEvent format has things nested under
246 filter(lambda a: a['type'] == 'GW_SKYMAP', event.voe_VOEvent.What.Group)[0]
247 so go ahead and look under different group type names to find the right
248 skymap URL."""
249 if not os.path.isfile(filename):
250 raise IOError(filename + ': file does not exist.')
251 event = untangle.parse(filename)
252 # group = filter(lambda a: a['type'] == grouptypename,
253 # event.voe_VOEvent.What.Group)[0]
254 group = [g for g in event.voe_VOEvent.What.Group
255 if g['type'] == grouptypename][0]
256 # param = filter(lambda a: a['name'] == paramname, group.Param)[0]['value']
257 param = [p for p in group.Param if p['name'] == paramname][0]['value']
258 return param.split('/')[-1]
261def get_filename_from_param(filename, paramname):
262 """get the skymap URL for newer style VOEvents, which have the
263 skymap param directly under the What element:
264 VOEvent/What/Param"""
265 return utils.get_voevent_param(filename, paramname).split('/')[-1]
268def get_skymap_filename(filename):
269 """Get the skymap URL regardless of the current VOEvent format from the
270 VOEvent file with the given filename."""
271 gracedbfilename = None
272 # try finding URL from current format VOEvent
273 for key in POSSIBLE_URL_KEYS:
274 try:
275 gracedbfilename = get_filename_from_param(filename, key)
276 except IndexError:
277 pass
278 # try finding URL from stupid intermediate VOEvent format
279 for grouptypename in POSSIBLE_GROUP_TYPE_NAMES:
280 for key in POSSIBLE_URL_KEYS:
281 try:
282 gracedbfilename = get_filename_from_group_name(
283 filename,
284 grouptypename,
285 key
286 )
287 except IndexError:
288 pass
289 # try finding URL from old format VOEvent
290 for key in POSSIBLE_GROUP_URL_KEYS:
291 try:
292 gracedbfilename = get_filename_from_group(filename, key)
293 except AttributeError:
294 pass
295 except IndexError:
296 pass
297 if gracedbfilename is None:
298 LOGGER.critical('VOEvent format changed, none of ' +
299 '{} {} or {} work'.format(POSSIBLE_URL_KEYS,
300 POSSIBLE_GROUP_TYPE_NAMES,
301 POSSIBLE_GROUP_URL_KEYS))
302 raise IndexError('VOEvent format changed for ' +
303 '{}, none of '.format(filename) +
304 '{} {} or {} work'.format(POSSIBLE_URL_KEYS,
305 POSSIBLE_GROUP_TYPE_NAMES,
306 POSSIBLE_GROUP_URL_KEYS))
307 return gracedbfilename