Hide keyboard shortcuts

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 

2 

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""" 

9 

10import os 

11from traceback import format_exc 

12import logging 

13import untangle 

14from llama import utils 

15from llama.filehandler import EventTriggeredFileHandler 

16 

17LOGGER = logging.getLogger(__name__) 

18 

19 

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) 

27 

28 

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. 

34 

35 Parameters 

36 ---------- 

37 payload : str 

38 The LVC VOEvent in XML string format. 

39 

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. 

57 

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. 

64 

65 Examples 

66 -------- 

67 Get the IVORN, eventid, and GraceID from a test event file: 

68 

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 

101 

102 

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 """ 

109 

110 FILENAME = 'lvc_gcn.xml' 

111 

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 

120 

121 @property 

122 def alert_type(self): 

123 """Get the alert type for this VOEvent.""" 

124 return f"gcn-{self.get_alert_type().lower()}" 

125 

126 @property 

127 def ivorn(self): 

128 """Get the IVORN, a unique identifier for this GCN Notice.""" 

129 return self.etree.get('ivorn') 

130 

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) 

140 

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) 

146 

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) 

152 

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) 

159 

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) 

165 

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) 

170 

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) 

177 

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) 

182 

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')) 

187 

188 @property 

189 def graceid(self): 

190 """Get the GraceID corresponding to this VOEvent.""" 

191 return self.get_param('GraceID') 

192 

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() 

198 

199 def get_alert_type(self): 

200 """Read the alert type of this GCN Notice.""" 

201 return utils.get_voevent_param(self.fullpath, 'AlertType') 

202 

203 def get_param(self, param): 

204 """Get this VOEventParam for this event.""" 

205 return utils.get_voevent_param(self.fullpath, param) 

206 

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) 

211 

212 

213class LvcRetractionXml(LvcGcnXml): 

214 """ 

215 A VOEvent XML file retracting a GCN notice. 

216 """ 

217 

218 FILENAME = 'lvc_gcn_retraction.xml' 

219 

220 

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'] 

227 

228 

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] 

242 

243 

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] 

259 

260 

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] 

266 

267 

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