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, August 2017-2018 

2 

3""" 

4``SkymapInfo`` records basic information about GW triggers/skymaps being used for 

5a LLAMA analysis in a uniform, simple format. 

6""" 

7 

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) 

33 

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 

37 

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" 

42 

43 

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 

50 

51 

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

58 

59 class_vetoes = ( 

60 (event_far_greater_than_10_per_day, None), 

61 ) 

62 

63 

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 

72 

73 

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 

84 

85 

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

95 

96 FILENAME = 'skymap_info.json' 

97 

98 _required_keys = [ 

99 'event_time_iso', 

100 'graceid', 

101 'far', 

102 'notice_time_iso', 

103 'pipeline', 

104 'skymap_filename', 

105 'alert_type' 

106 ] 

107 

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 ) 

120 

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) 

126 

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 ) 

156 

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 ) 

176 

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 ] 

188 

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) 

221 

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

227 

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

233 

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 

240 

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) 

246 

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) 

252 

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

259 

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

264 

265 @property 

266 def graceid(self): 

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

268 return self.read_json()['graceid'] 

269 

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) 

277 

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

283 

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

289 

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) 

295 

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. 

303 

304 **NB: This is NOT related to the ``alert_type`` key in LVAlerts!** 

305 """ 

306 return self.read_json()['alert_type'] 

307 

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