Coverage for gwcelery/tasks/external_skymaps.py: 100%

257 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-11-14 05:52 +0000

1"""Create and upload external sky maps.""" 

2import io 

3import re 

4import ssl 

5import urllib 

6from base64 import b64decode 

7from urllib.error import HTTPError 

8 

9import astropy_healpix as ah 

10import gcn 

11import lxml.etree 

12# import astropy.utils.data 

13import numpy as np 

14from astropy import units as u 

15from astropy.coordinates import SkyCoord 

16from astropy.io.fits import getheader 

17from celery import group 

18from hpmoc import PartialUniqSkymap 

19from hpmoc.utils import reraster, uniq_intersection 

20from ligo.skymap.distance import parameters_to_marginal_moments 

21from ligo.skymap.io import fits 

22from ligo.skymap.moc import bayestar_adaptive_grid 

23from ligo.skymap.plot.bayes_factor import plot_bayes_factor 

24 

25from .. import _version, app 

26from ..util.cmdline import handling_system_exit 

27from ..util.tempfile import NamedTemporaryFile 

28from . import gracedb, skymaps 

29 

30COMBINED_SKYMAP_FILENAME_MULTIORDER = 'combined-ext.multiorder.fits' 

31"""Filename of combined sky map in a multiordered format""" 

32 

33COMBINED_SKYMAP_FILENAME_PNG = 'combined-ext.png' 

34"""Filename of combined sky map plot""" 

35 

36FERMI_OFFICIAL_SKYMAP_FILENAME = 'glg_healpix_all_bn_v00' 

37"""Filename of sky map from official Fermi GBM analysis""" 

38 

39NOTICE_TYPE_DICT = { 

40 '53': 'INTEGRAL_WAKEUP', 

41 '54': 'INTEGRAL_REFINED', 

42 '55': 'INTEGRAL_OFFLINE', 

43 '60': 'SWIFT_BAT_GRB_ALERT', 

44 '61': 'SWIFT_BAT_GRB_POSITION', 

45 '110': 'FERMI_GBM_ALERT', 

46 '111': 'FERMI_GBM_FLT_POS', 

47 '112': 'FERMI_GBM_GND_POS', 

48 '115': 'FERMI_GBM_FINAL_POS', 

49 '131': 'FERMI_GBM_SUBTHRESHOLD' 

50} 

51 

52 

53@app.task(shared=False) 

54def create_combined_skymap(se_id, ext_id, preferred_event=None): 

55 """Creates and uploads a combined LVK-external skymap, uploading to the 

56 external trigger GraceDB page. The filename used for the combined sky map 

57 will be 'combined-ext.multiorder.fits' if the external sky map is 

58 multi-ordered or 'combined-ext.fits.gz' if the external sky map is not. 

59 Will use the GW sky map in from the preferred event if given. 

60 

61 Parameters 

62 ---------- 

63 se_id : str 

64 Superevent GraceDB ID 

65 ext_id : str 

66 External event GraceDB ID 

67 preferred_event : str 

68 Preferred event GraceDB ID. If given, use sky map from preferred event 

69 

70 """ 

71 # gw_id is either superevent or preferred event graceid 

72 gw_id = preferred_event if preferred_event else se_id 

73 gw_skymap_filename = get_skymap_filename(gw_id, is_gw=True) 

74 ext_skymap_filename = get_skymap_filename(ext_id, is_gw=False) 

75 # Determine whether external sky map is multiordered or flat 

76 ext_moc = '.multiorder.fits' in ext_skymap_filename 

77 

78 message = \ 

79 ('Combined LVK-external sky map using {0} and {1}, with {2} and ' 

80 '{3}'.format(gw_id, ext_id, gw_skymap_filename, ext_skymap_filename)) 

81 message_png = ( 

82 'Mollweide projection of <a href="/api/events/{graceid}/files/' 

83 '{filename}">{filename}</a>').format( 

84 graceid=ext_id, 

85 filename=COMBINED_SKYMAP_FILENAME_MULTIORDER) 

86 

87 ( 

88 _download_skymaps.si( 

89 gw_skymap_filename, ext_skymap_filename, gw_id, ext_id 

90 ) 

91 | 

92 combine_skymaps.s(ext_moc=ext_moc) 

93 | 

94 group( 

95 gracedb.upload.s( 

96 COMBINED_SKYMAP_FILENAME_MULTIORDER, 

97 ext_id, message, ['sky_loc', 'ext_coinc'] 

98 ), 

99 

100 skymaps.plot_allsky.s() 

101 | 

102 gracedb.upload.s(COMBINED_SKYMAP_FILENAME_PNG, ext_id, 

103 message_png, ['sky_loc', 'ext_coinc']) 

104 ) 

105 | 

106 gracedb.create_label.si('COMBINEDSKYMAP_READY', ext_id) 

107 ).delay() 

108 

109 

110@app.task(autoretry_for=(ValueError,), retry_backoff=10, 

111 retry_backoff_max=600) 

112def get_skymap_filename(graceid, is_gw): 

113 """Get the skymap FITS filename. 

114 

115 If not available, will try again 10 seconds later, then 20, then 40, etc. 

116 up to a max 10 minutes retry delay. 

117 

118 Parameters 

119 ---------- 

120 graceid : str 

121 GraceDB ID 

122 is_gw : bool 

123 If True, uses method for superevent or preferred event. Otherwise uses 

124 method for external event. 

125 

126 Returns 

127 ------- 

128 filename : str 

129 Filename of latest sky map 

130 

131 """ 

132 gracedb_log = gracedb.get_log(graceid) 

133 if is_gw: 

134 # Try first to get a multiordered sky map 

135 for message in reversed(gracedb_log): 

136 filename = message['filename'] 

137 v = message['file_version'] 

138 fv = '{},{}'.format(filename, v) 

139 if filename.endswith('.multiorder.fits') and \ 

140 "combined-ext." not in filename: 

141 return fv 

142 # Try next to get a flattened sky map 

143 for message in reversed(gracedb_log): 

144 filename = message['filename'] 

145 v = message['file_version'] 

146 fv = '{},{}'.format(filename, v) 

147 if filename.endswith('.fits.gz') and \ 

148 "combined-ext." not in filename: 

149 return fv 

150 else: 

151 for message in reversed(gracedb_log): 

152 filename = message['filename'] 

153 v = message['file_version'] 

154 fv = '{},{}'.format(filename, v) 

155 if (filename.endswith('.fits') or filename.endswith('.fit') or 

156 filename.endswith('.fits.gz')) and \ 

157 "combined-ext." not in filename: 

158 return fv 

159 raise ValueError('No skymap available for {0} yet.'.format(graceid)) 

160 

161 

162def is_skymap_moc(skymap_bytes): 

163 with NamedTemporaryFile(content=skymap_bytes) as skymap_file: 

164 # If ordering is EXPLICIT, should be multiordered (MOC) 

165 return getheader(skymap_file.name, ext=1)['INDXSCHM'] == 'EXPLICIT' 

166 

167 

168@app.task(shared=False) 

169def _download_skymaps(gw_filename, ext_filename, gw_id, ext_id): 

170 """Download both superevent and external sky map to be combined. 

171 

172 Parameters 

173 ---------- 

174 gw_filename : str 

175 GW sky map filename 

176 ext_filename : str 

177 External sky map filename 

178 gw_id : str 

179 GraceDB ID of GW candidate, either superevent or preferred event 

180 ext_id : str 

181 GraceDB ID of external candidate 

182 

183 Returns 

184 ------- 

185 gw_skymap, ext_skymap : tuple 

186 Tuple of gw_skymap and ext_skymap bytes 

187 

188 """ 

189 gw_skymap = gracedb.download(gw_filename, gw_id) 

190 ext_skymap = gracedb.download(ext_filename, ext_id) 

191 return gw_skymap, ext_skymap 

192 

193 

194def combine_skymaps_moc_moc(gw_sky, ext_sky): 

195 """This function combines a multi-ordered (MOC) GW sky map with a MOC 

196 external skymap. 

197 """ 

198 gw_sky_hpmoc = PartialUniqSkymap(gw_sky["PROBDENSITY"], gw_sky["UNIQ"], 

199 name="PROBDENSITY", meta=gw_sky.meta) 

200 # Determine the column name in ext_sky and rename it as PROBDENSITY. 

201 ext_sky_hpmoc = PartialUniqSkymap(ext_sky["PROBDENSITY"], ext_sky["UNIQ"], 

202 name="PROBDENSITY", meta=ext_sky.meta) 

203 

204 comb_sky_hpmoc = gw_sky_hpmoc * ext_sky_hpmoc 

205 comb_sky_hpmoc /= np.sum(comb_sky_hpmoc.s * comb_sky_hpmoc.area()) 

206 comb_sky = comb_sky_hpmoc.to_table(name='PROBDENSITY') 

207 

208 # Modify GW sky map with new data, ensuring they exist first 

209 if 'DISTMU' in gw_sky.keys() and 'DISTSIGMA' in gw_sky.keys(): 

210 UNIQ = comb_sky['UNIQ'] 

211 UNIQ_ORIG = gw_sky['UNIQ'] 

212 intersection = uniq_intersection(UNIQ_ORIG, UNIQ) 

213 DIST_MU = reraster(UNIQ_ORIG, 

214 gw_sky["DISTMU"], 

215 UNIQ, 

216 method='copy', 

217 intersection=intersection) 

218 DIST_SIGMA = reraster(UNIQ_ORIG, 

219 gw_sky["DISTSIGMA"], 

220 UNIQ, 

221 method='copy', 

222 intersection=intersection) 

223 DIST_NORM = reraster(UNIQ_ORIG, 

224 gw_sky["DISTNORM"], 

225 UNIQ, 

226 method='copy', 

227 intersection=intersection) 

228 comb_sky.add_columns([DIST_MU, DIST_SIGMA, DIST_NORM], 

229 names=['DISTMU', 'DISTSIGMA', 'DISTNORM']) 

230 

231 distmean, diststd = parameters_to_marginal_moments( 

232 comb_sky['PROBDENSITY'] * comb_sky_hpmoc.area().value, 

233 comb_sky['DISTMU'], comb_sky['DISTSIGMA']) 

234 comb_sky.meta['distmean'], comb_sky.meta['diststd'] = distmean, diststd 

235 if 'instruments' not in ext_sky.meta: 

236 ext_sky.meta.update({'instruments': {'external instrument'}}) 

237 if 'instruments' in comb_sky.meta: 

238 comb_sky.meta['instruments'].update(ext_sky.meta['instruments']) 

239 if 'HISTORY' in comb_sky.meta: 

240 ext_instrument = list(ext_sky.meta['instruments'])[0] 

241 comb_sky.meta['HISTORY'].extend([ 

242 '', 'The values were reweighted by using data from {0}{1}'.format( 

243 ('an ' if ext_instrument == 'external instrument' 

244 else ''), 

245 ext_instrument)]) 

246 

247 # Remove redundant field 

248 if 'ORDERING' in comb_sky.meta: 

249 del comb_sky.meta['ORDERING'] 

250 return comb_sky 

251 

252 

253def combine_skymaps_moc_flat(gw_sky, ext_sky, ext_header): 

254 """This function combines a multi-ordered (MOC) GW sky map with a flattened 

255 external one by re-weighting the MOC sky map using the values of the 

256 flattened one. 

257 

258 Header info is generally inherited from the GW sky map or recalculated 

259 using the combined sky map values. 

260 

261 Parameters 

262 ---------- 

263 gw_sky : Table 

264 GW sky map astropy Table 

265 ext_sky : array 

266 External sky map array 

267 ext_header : dict 

268 Header of external sky map 

269 

270 Returns 

271 ------- 

272 comb_sky : Table 

273 Table of combined sky map 

274 

275 """ 

276 # Find ra/dec of each GW pixel 

277 level, ipix = ah.uniq_to_level_ipix(gw_sky["UNIQ"]) 

278 nsides = ah.level_to_nside(level) 

279 areas = ah.nside_to_pixel_area(nsides) 

280 ra_gw, dec_gw = ah.healpix_to_lonlat(ipix, nsides, order='nested') 

281 # Find corresponding external sky map indicies 

282 ext_nside = ah.npix_to_nside(len(ext_sky)) 

283 ext_ind = ah.lonlat_to_healpix( 

284 ra_gw, dec_gw, ext_nside, 

285 order='nested' if ext_header['nest'] else 'ring') 

286 # Fix any negative values and normalize 

287 ext_sky[ext_sky < 0.] = 0. 

288 ext_sky /= np.sum(ext_sky) 

289 # Reweight GW prob density by external sky map probabilities 

290 gw_sky['PROBDENSITY'] *= ext_sky[ext_ind] 

291 gw_sky['PROBDENSITY'] /= \ 

292 np.sum(gw_sky['PROBDENSITY'] * areas).value 

293 # Modify GW sky map with new data, ensuring they exist first 

294 if 'DISTMU' in gw_sky.keys() and 'DISTSIGMA' in gw_sky.keys(): 

295 distmean, diststd = parameters_to_marginal_moments( 

296 gw_sky['PROBDENSITY'] * areas.value, 

297 gw_sky['DISTMU'], gw_sky['DISTSIGMA']) 

298 gw_sky.meta['distmean'], gw_sky.meta['diststd'] = distmean, diststd 

299 if 'instruments' not in ext_header: 

300 ext_header.update({'instruments': {'external instrument'}}) 

301 if 'instruments' in gw_sky.meta: 

302 gw_sky.meta['instruments'].update(ext_header['instruments']) 

303 if 'HISTORY' in gw_sky.meta: 

304 ext_instrument = list(ext_header['instruments'])[0] 

305 gw_sky.meta['HISTORY'].extend([ 

306 '', 'The values were reweighted by using data from {0}{1}'.format( 

307 ('an ' if ext_instrument == 'external instrument' 

308 else ''), 

309 ext_instrument)]) 

310 return gw_sky 

311 

312 

313@app.task(shared=False) 

314def combine_skymaps(skymapsbytes, ext_moc=True): 

315 """This task combines the two input sky maps, in this case the external 

316 trigger skymap and the LVK skymap and writes to a temporary output file. It 

317 then returns the contents of the file as a byte array. 

318 

319 There are separate methods in case the GW sky map is multiordered (we just 

320 reweight using the external sky map) or flattened (use standard 

321 ligo.skymap.combine method). 

322 

323 Parameters 

324 ---------- 

325 skymapbytes : tuple 

326 Tuple of gw_skymap and ext_skymap bytes 

327 gw_moc : bool 

328 If True, assumes the GW sky map is a multi-ordered format 

329 

330 Returns 

331 ------- 

332 combinedskymap : bytes 

333 Bytes of combined sky map 

334 """ 

335 gw_skymap_bytes, ext_skymap_bytes = skymapsbytes 

336 with NamedTemporaryFile(mode='rb', suffix=".fits") as combinedskymap, \ 

337 NamedTemporaryFile(content=gw_skymap_bytes) as gw_skymap_file, \ 

338 NamedTemporaryFile(content=ext_skymap_bytes) as ext_skymap_file, \ 

339 handling_system_exit(): 

340 

341 gw_skymap = fits.read_sky_map(gw_skymap_file.name, moc=True) 

342 # If GW sky map is multiordered, use reweighting method 

343 if ext_moc: 

344 # Load external sky map 

345 ext_skymap = fits.read_sky_map(ext_skymap_file.name, moc=True) 

346 # Create and write combined sky map 

347 combined_skymap = combine_skymaps_moc_moc(gw_skymap, 

348 ext_skymap) 

349 # If GW sky map is flattened, use older method 

350 else: 

351 # Load external sky map 

352 ext_skymap, ext_header = fits.read_sky_map(ext_skymap_file.name, 

353 moc=False, nest=True) 

354 # Create and write combined sky map 

355 combined_skymap = combine_skymaps_moc_flat(gw_skymap, ext_skymap, 

356 ext_header) 

357 fits.write_sky_map(combinedskymap.name, combined_skymap, moc=True) 

358 return combinedskymap.read() 

359 

360 

361@app.task(shared=False) 

362def external_trigger_heasarc(external_id): 

363 """Returns the HEASARC FITS file link. 

364 

365 Parameters 

366 ---------- 

367 external_id : str 

368 GraceDB ID of external event 

369 

370 Returns 

371 ------- 

372 heasarc_link : str 

373 Guessed HEASARC URL link 

374 

375 """ 

376 gracedb_log = gracedb.get_log(external_id) 

377 for message in gracedb_log: 

378 if 'Original Data' in message['comment']: 

379 filename = message['filename'] 

380 xmlfile = gracedb.download(urllib.parse.quote(filename), 

381 external_id) 

382 root = lxml.etree.fromstring(xmlfile) 

383 heasarc_url = root.find('./What/Param[@name="LightCurve_URL"]' 

384 ).attrib['value'] 

385 return re.sub(r'quicklook(.*)', 'current/', heasarc_url) 

386 raise ValueError('Not able to retrieve HEASARC link for {0}.'.format( 

387 external_id)) 

388 

389 

390@app.task(autoretry_for=(gracedb.RetryableHTTPError,), retry_backoff=30, 

391 max_retries=8) 

392def get_external_skymap(link, search): 

393 """Download the Fermi sky map FITS file and return the contents as a byte 

394 array. If GRB, will construct a HEASARC url, while if SubGRB, will use the 

395 link directly. 

396 

397 If not available, will try again 10 seconds later, then 20, then 40, etc. 

398 until up to 15 minutes after initial attempt. 

399 

400 Parameters 

401 ---------- 

402 link : str 

403 HEASARC URL link 

404 search : str 

405 Search field of external event 

406 

407 Returns 

408 ------- 

409 external_skymap : bytes 

410 Bytes of external sky map 

411 

412 """ 

413 if search == 'GRB': 

414 # if Fermi GRB, determine final HEASARC link 

415 trigger_id = re.sub(r'.*\/(\D+?)(\d+)(\D+)\/.*', r'\2', link) 

416 skymap_name = 'glg_healpix_all_bn{0}_v00.fit'.format(trigger_id) 

417 skymap_link = link + skymap_name 

418 elif search in {'SubGRB', 'FromURL'}: 

419 skymap_link = link 

420 # FIXME: Under Anaconda on the LIGO Caltech computing cluster, Python 

421 # (and curl, for that matter) fail to negotiate TLSv1.2 with 

422 # heasarc.gsfc.nasa.gov 

423 context = ssl.create_default_context() 

424 context.options |= ssl.OP_NO_TLSv1_3 

425 # return astropy.utils.data.get_file_contents( 

426 # (skymap_link), encoding='binary', cache=False) 

427 try: 

428 response = urllib.request.urlopen(skymap_link, context=context) 

429 return response.read() 

430 except HTTPError as e: 

431 if e.code == 404: 

432 raise gracedb.RetryableHTTPError("Failed to download the sky map." 

433 "Retrying...") 

434 else: 

435 raise 

436 

437 

438@app.task(shared=False) 

439def read_upload_skymap_from_base64(event, skymap_str): 

440 """Decode and upload 64base encoded sky maps from Kafka alerts. 

441 

442 Parameters 

443 ---------- 

444 event : dict 

445 External event dictionary 

446 skymap_str : str 

447 Base 64 encoded sky map string 

448 

449 """ 

450 

451 graceid = event['graceid'] 

452 ra = event['extra_attributes']['GRB']['ra'] 

453 dec = event['extra_attributes']['GRB']['dec'] 

454 

455 # Decode base64 encoded string to bytes string 

456 skymap_data = b64decode(skymap_str) 

457 

458 # Determine filename based on whether is multiordered or flattened 

459 skymap_filename = event['pipeline'].lower() + '_skymap.' 

460 skymap_filename += ('multiorder.fits' if is_skymap_moc(skymap_data) 

461 else 'fits.gz') 

462 

463 message = ( 

464 'Mollweide projection of <a href="/api/events/{graceid}/files/' 

465 '{filename}">{filename}</a>').format( 

466 graceid=graceid, filename=skymap_filename) 

467 

468 ( 

469 group( 

470 gracedb.upload.si( 

471 skymap_data, 

472 skymap_filename, 

473 graceid, 

474 'Sky map uploaded from {} via a kafka notice'.format( 

475 event['pipeline']), 

476 ['sky_loc']), 

477 

478 skymaps.plot_allsky.si(skymap_data, ra=ra, dec=dec) 

479 | 

480 gracedb.upload.s(event['pipeline'].lower() + '_skymap.png', 

481 graceid, 

482 message, 

483 ['sky_loc']) 

484 ) 

485 | 

486 gracedb.create_label.si('EXT_SKYMAP_READY', graceid) 

487 ).delay() 

488 

489 

490@app.task(autoretry_for=(urllib.error.HTTPError, urllib.error.URLError,), 

491 retry_backoff=10, retry_backoff_max=1200) 

492def get_upload_external_skymap(event, skymap_link=None): 

493 """If a Fermi sky map is not uploaded yet, tries to download one and upload 

494 to external event. If sky map is not available, passes so that this can be 

495 re-run the next time an update GCN notice is received. If GRB, will 

496 construct a HEASARC url, while if SubGRB, will use the link directly. 

497 If SubGRB or FromURL, downloads a skymap using the provided URL rather 

498 than construct one. 

499 

500 Parameters 

501 ---------- 

502 event : dict 

503 External event dictionary 

504 skymap_link : str 

505 HEASARC URL link 

506 

507 """ 

508 graceid = event['graceid'] 

509 search = event['search'] 

510 

511 if search == 'GRB': 

512 skymap_data = \ 

513 get_external_skymap(external_trigger_heasarc(graceid), search) 

514 elif search in {'SubGRB', 'SubGRBTargeted', 'FromURL'}: 

515 skymap_data = get_external_skymap(skymap_link, search) 

516 

517 skymap_filename_base = \ 

518 ('external_from_url' if search == 'FromURL' 

519 else FERMI_OFFICIAL_SKYMAP_FILENAME) 

520 skymap_filename = skymap_filename_base + \ 

521 ('.multiorder.fits' if is_skymap_moc(skymap_data) else '.fits.gz') 

522 

523 fits_message = \ 

524 ('Downloaded from {}.'.format(skymap_link) if search == 'FromURL' 

525 else 'Official sky map from Fermi analysis.') 

526 png_message = ( 

527 'Mollweide projection of <a href="/api/events/{graceid}/files/' 

528 '{filename}">{filename}</a>').format( 

529 graceid=graceid, filename=skymap_filename) 

530 

531 ( 

532 group( 

533 gracedb.upload.si( 

534 skymap_data, 

535 skymap_filename, 

536 graceid, 

537 fits_message, 

538 ['sky_loc']), 

539 

540 skymaps.plot_allsky.si(skymap_data) 

541 | 

542 gracedb.upload.s(skymap_filename_base + '.png', 

543 graceid, 

544 png_message, 

545 ['sky_loc']) 

546 ) 

547 | 

548 gracedb.create_label.si('EXT_SKYMAP_READY', graceid) 

549 ).delay() 

550 

551 

552def from_cone(pts, ra, dec, error): 

553 """ 

554 Based on the given RA, DEC, and error radius of the center points, 

555 it calculates the gaussian pdf. 

556 """ 

557 ras, decs = pts.T 

558 center = SkyCoord(ra * u.deg, dec * u.deg) 

559 error_radius = error * u.deg 

560 

561 pts_loc = SkyCoord(np.rad2deg(ras) * u.deg, np.rad2deg(decs) * u.deg) 

562 

563 distance = pts_loc.separation(center) 

564 skymap = np.exp(-0.5 * np.square(distance / error_radius).to_value( 

565 u.dimensionless_unscaled)) 

566 return skymap 

567 

568 

569def _fisher(distance, error_stat, error_sys): 

570 """ 

571 Calculates the Fisher distribution from Eq. 1 and Eq. 2 of Connaughton 

572 et al. 2015 (doi: 10.1088/0067-0049/216/2/32). 

573 """ 

574 

575 error_tot2 = ( 

576 np.square(np.radians(error_stat)) + 

577 np.square(np.radians(error_sys)) 

578 ) 

579 kappa = 1 / (0.4356 * error_tot2) 

580 return kappa / (2 * np.pi * (np.exp(kappa) - np.exp(-kappa))) \ 

581 * np.exp(kappa * np.cos(distance)) 

582 

583 

584def fermi_error_model(pts, ra, dec, error, core, tail, core_weight): 

585 """ 

586 Calculate the Fermi GBM error model from Connaughton et al. 2015 based 

587 on the given RA, Dec and error radius of the center point using the model 

588 parameters of core radii, tail radii, and core proportion (f in the paper). 

589 """ 

590 ras, decs = pts.T 

591 center = SkyCoord(ra * u.deg, dec * u.deg) 

592 

593 pts_loc = SkyCoord(np.rad2deg(ras) * u.deg, np.rad2deg(decs) * u.deg) 

594 distance = pts_loc.separation(center).rad # Ensure the output is in radian 

595 

596 core_component = core_weight * _fisher(distance, error, core) 

597 tail_component = (1 - core_weight) * _fisher(distance, error, tail) 

598 

599 return core_component + tail_component 

600 

601 

602def create_external_skymap(ra, dec, error, pipeline, notice_type=111): 

603 """Create a sky map, either a gaussian or a single 

604 pixel sky map, given an RA, dec, and error radius. 

605 

606 If from Fermi, convolves the sky map with both a core and 

607 tail Gaussian and then sums these to account for systematic 

608 effects as measured in :doi:`10.1088/0067-0049/216/2/32` 

609 

610 If from Swift, converts the error radius from that containing 90% of the 

611 credible region to ~68% (see description of Swift error 

612 here:`https://gcn.gsfc.nasa.gov/swift.html#tc7`) 

613 

614 Parameters 

615 ---------- 

616 ra : float 

617 Right ascension in deg 

618 dec : float 

619 Declination in deg 

620 error : float 

621 Error radius in deg 

622 pipeline : str 

623 External trigger pipeline name 

624 notice_type : int 

625 GCN notice type integer 

626 

627 Returns 

628 ------- 

629 skymap : array 

630 Sky map array 

631 

632 """ 

633 # Dictionary definitions for core_weight, core, and tail values 

634 # for different notice types. 

635 # Flight notice: Values from first row of Table 7 

636 # Ground notice: Values from first row of Table 3 

637 # Final notice: Values from second row of Table 3 

638 fermi_params = { 

639 gcn.NoticeType.FERMI_GBM_FLT_POS: {"core_weight": 0.897, 

640 "core_width": 7.52, 

641 "tail_width": 55.6}, 

642 gcn.NoticeType.FERMI_GBM_GND_POS: {"core_weight": 0.804, 

643 "core_width": 3.72, 

644 "tail_width": 13.7}, 

645 gcn.NoticeType.FERMI_GBM_FIN_POS: {"core_weight": 0.900, 

646 "core_width": 3.71, 

647 "tail_width": 14.3}, 

648 } 

649 

650 # Correct 90% containment to 1-sigma for Swift 

651 if pipeline == 'Swift': 

652 error /= np.sqrt(-2 * np.log1p(-.9)) 

653 # Set minimum error radius so function does not return void 

654 # FIXME: Lower this when fixes are made to ligo.skymap to fix nans when 

655 # the error radius is too low 

656 error = max(error, .08) 

657 # This function adaptively refines the grid based on the given gaussian 

658 # pdf to create the multi-ordered skymap. 

659 if pipeline == 'Fermi' and notice_type is not None: 

660 # Correct for Fermi systematics based on recommendations from GBM team 

661 # Convolve with both a narrow core and wide tail Gaussian with error 

662 # radius determined by the scales respectively, each comprising a 

663 # fraction determined by the weights respectively. 

664 if notice_type not in fermi_params: 

665 raise AssertionError('Provide a supported Fermi notice type') 

666 core_weight = fermi_params[notice_type]["core_weight"] 

667 # Note that tail weight = 1 - core_weight 

668 core_width = fermi_params[notice_type]["core_width"] 

669 tail_width = fermi_params[notice_type]["tail_width"] 

670 

671 # Integrate the fermi_error_model using bayestar_adaptive_grid 

672 skymap = bayestar_adaptive_grid(fermi_error_model, ra, dec, error, 

673 core_width, tail_width, core_weight, 

674 rounds=8) 

675 else: 

676 # Use generic cone method for Swift, INTEGRAL, etc. 

677 skymap = bayestar_adaptive_grid(from_cone, ra, dec, error, rounds=8) 

678 

679 return skymap 

680 

681 

682def write_to_fits(skymap, event, notice_type, notice_date): 

683 """Write external sky map FITS file, populating the 

684 header with relevant info. 

685 

686 Parameters 

687 ---------- 

688 skymap : array 

689 Sky map array 

690 event : dict 

691 Dictionary of external event 

692 notice_type : int 

693 GCN notice type integer 

694 notice_date : str 

695 External event trigger time in ISO format 

696 

697 Returns 

698 ------- 

699 skymap_fits : str 

700 Bytes string of sky map 

701 

702 """ 

703 

704 if notice_type is None: 

705 msgtype = event['pipeline'] + '_LVK_TARGETED_SEARCH' 

706 else: 

707 msgtype = NOTICE_TYPE_DICT[str(notice_type)] 

708 

709 gcn_id = event['extra_attributes']['GRB']['trigger_id'] 

710 with NamedTemporaryFile(suffix='.fits') as f: 

711 fits.write_sky_map(f.name, skymap, 

712 objid=gcn_id, 

713 url=event['links']['self'], 

714 instruments=event['pipeline'], 

715 gps_time=event['gpstime'], 

716 msgtype=msgtype, 

717 msgdate=notice_date, 

718 creator='gwcelery', 

719 origin='LIGO-VIRGO-KAGRA', 

720 vcs_version=_version.get_versions()['version'], 

721 history='file only for internal use') 

722 with open(f.name, 'rb') as file: 

723 return file.read() 

724 

725 

726@app.task(shared=False) 

727def create_upload_external_skymap(event, notice_type, notice_date): 

728 """Create and upload external sky map using 

729 RA, dec, and error radius information. 

730 

731 Parameters 

732 ---------- 

733 event : dict 

734 Dictionary of external event 

735 notice_type : int 

736 GCN notice type integer 

737 notice_date : str 

738 External event trigger time in ISO format 

739 

740 """ 

741 graceid = event['graceid'] 

742 skymap_filename = event['pipeline'].lower() + '_skymap.multiorder.fits' 

743 

744 ra = event['extra_attributes']['GRB']['ra'] 

745 dec = event['extra_attributes']['GRB']['dec'] 

746 error = event['extra_attributes']['GRB']['error_radius'] 

747 pipeline = event['pipeline'] 

748 

749 if not (ra or dec or error): 

750 # Don't create sky map if notice only contains zeros, lacking info 

751 return 

752 skymap = create_external_skymap(ra, dec, error, pipeline, notice_type) 

753 

754 skymap_data = write_to_fits(skymap, event, notice_type, notice_date) 

755 

756 if notice_type is None: 

757 extra_sentence = ' from {} via our joint targeted search'.format( 

758 pipeline) 

759 else: 

760 msgtype = NOTICE_TYPE_DICT[str(notice_type)] 

761 extra_sentence = ' from a {} type GCN notice'.format(msgtype) 

762 

763 message = ( 

764 'Mollweide projection of <a href="/api/events/{graceid}/files/' 

765 '{filename}">{filename}</a>{extra_sentence}').format( 

766 graceid=graceid, filename=skymap_filename, 

767 extra_sentence=extra_sentence) 

768 

769 ( 

770 gracedb.upload.si( 

771 skymap_data, 

772 skymap_filename, 

773 graceid, 

774 'Sky map created from GCN RA, dec, and error{}.'.format( 

775 extra_sentence), 

776 ['sky_loc']) 

777 | 

778 skymaps.plot_allsky.si(skymap_data, ra=ra, dec=dec) 

779 | 

780 gracedb.upload.s(event['pipeline'].lower() + '_skymap.png', 

781 graceid, 

782 message, 

783 ['sky_loc']) 

784 | 

785 gracedb.create_label.si('EXT_SKYMAP_READY', graceid) 

786 ).delay() 

787 

788 

789@app.task(shared=False) 

790def plot_overlap_integral(coinc_far_dict, superevent, ext_event, 

791 var_label=r"\mathcal{I}_{\Omega}"): 

792 """Plot and upload visualization of the sky map overlap integral computed 

793 by ligo.search.overlap_integral. 

794 

795 Parameters 

796 ---------- 

797 coinc_far_dict : dict 

798 Dictionary containing coincidence false alarm rate results from 

799 RAVEN 

800 superevent : dict 

801 Superevent dictionary 

802 ext_event : dict 

803 External event dictionary 

804 var_label : str 

805 The variable symbol used in plotting 

806 

807 """ 

808 if coinc_far_dict.get('skymap_overlap') is None: 

809 return 

810 if superevent['em_type'] != ext_event['graceid'] and \ 

811 'RAVEN_ALERT' in superevent['labels']: 

812 return 

813 

814 superevent_id = superevent['superevent_id'] 

815 ext_id = ext_event['graceid'] 

816 

817 log_overlap = np.log(coinc_far_dict['skymap_overlap']) 

818 logI_string = np.format_float_positional(log_overlap, 1, trim='0', 

819 sign=True) 

820 # Create plot 

821 fig, _ = plot_bayes_factor( 

822 log_overlap, values=(1, 3, 5), xlim=7, var_label=var_label, 

823 title=(r'Sky Map Overlap between %s and %s [$\ln\,%s = %s$]' % 

824 (superevent_id, ext_id, var_label, logI_string))) 

825 # Convert to bytes 

826 outfile = io.BytesIO() 

827 fig.savefig(outfile, format='png') 

828 # Upload file 

829 gracedb.upload.si( 

830 outfile.getvalue(), 

831 'overlap_integral.png', 

832 superevent_id, 

833 message='Sky map overlap integral between {0} and {1}'.format( 

834 superevent_id, ext_id), 

835 tags=['ext_coinc'] 

836 ).delay()