Coverage for gwcelery/tasks/superevents.py: 87%

249 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-08-16 20:59 +0000

1"""Module containing the functionality for creation and management of 

2superevents. 

3 

4* There is serial processing of triggers from low latency pipelines. 

5* Dedicated **superevent** queue for this purpose. 

6* Primary logic to respond to low latency triggers contained in 

7 :meth:`process` function. 

8""" 

9from itertools import filterfalse, groupby 

10 

11from celery.utils.log import get_task_logger 

12from ligo.segments import segment, segmentlist 

13 

14from .. import app 

15from . import gracedb, igwn_alert 

16 

17log = get_task_logger(__name__) 

18 

19REQUIRED_LABELS_BY_GROUP_SEARCH = { 

20 'cbc': { 

21 'allsky': {'PASTRO_READY', 'EMBRIGHT_READY', 'SKYMAP_READY'}, 

22 'earlywarning': {'PASTRO_READY', 'EMBRIGHT_READY', 'SKYMAP_READY'}, 

23 'mdc': {'PASTRO_READY', 'EMBRIGHT_READY', 'SKYMAP_READY'}, 

24 'bbh': {'PASTRO_READY', 'EMBRIGHT_READY', 'SKYMAP_READY'}, # required by cwb-bbh # noqa: E501 

25 'ssm': {'EMBRIGHT_READY', 'SKYMAP_READY'} 

26 }, 

27 'burst': { 

28 'allsky': {'SKYMAP_READY'} 

29 } 

30} 

31"""These labels should be present on an event to consider it to 

32be complete. 

33""" 

34 

35FROZEN_LABEL = 'LOW_SIGNIF_LOCKED' 

36"""This label indicates that the superevent manager should make no further 

37changes to the preferred event.""" 

38 

39SIGNIFICANT_LABEL = 'SIGNIF_LOCKED' 

40"""This label indicates that the superevent is elevated to significant""" 

41 

42EARLY_WARNING_LABEL = 'EARLY_WARNING' 

43"""This label indicates that the superevent contains a significant 

44early warning event.""" 

45 

46EARLY_WARNING_SEARCH_NAME = 'EarlyWarning' 

47"""Search name for Early Warning searches. Only significant events 

48result in consideration by the superevent manager.""" 

49 

50READY_LABEL = 'EM_READY' 

51"""This label indicates that a preferred event has been assigned and it 

52has all data products required to make it ready for annotations.""" 

53 

54VT_SEARCH_NAME = 'VTInjection' 

55"""Search name for events uploaded as a part of estimating the spacetime 

56sensitive volume. Events from this search do not form superevents, and 

57are not annotated.""" 

58 

59 

60@igwn_alert.handler('cbc_gstlal', 

61 'cbc_spiir', 

62 'cbc_pycbc', 

63 'cbc_mbta', 

64 'burst_olib', 

65 'burst_cwb', 

66 'burst_mly', 

67 shared=False) 

68def handle(payload): 

69 """Respond to IGWN alert topics from low-latency search pipelines and 

70 delegate to :meth:`process` for superevent management. 

71 """ 

72 alert_type = payload['alert_type'] 

73 gid = payload['object']['graceid'] 

74 alert_search = payload['object']['search'] 

75 

76 ifos = get_instruments(payload['object']) 

77 # Ignore inclusion of events involving KAGRA; revert when policy is changed 

78 if "K1" in ifos: 

79 log.info('Skipping %s because it involves KAGRA data', gid) 

80 return 

81 # Ignore inclusion of events from VT search 

82 if alert_search == VT_SEARCH_NAME: 

83 log.info("Skipping {} event {}".format(VT_SEARCH_NAME, gid)) 

84 return 

85 

86 try: 

87 far = payload['object']['far'] 

88 except KeyError: 

89 log.info('Skipping %s because it lacks a FAR', gid) 

90 return 

91 

92 if far > app.conf['superevent_far_threshold']: 

93 log.info("Skipping processing of %s because of high FAR", gid) 

94 return 

95 

96 priority = 1 

97 if alert_type == 'label_added': 

98 priority = 0 

99 label = payload['data']['name'] 

100 group = payload['object']['group'].lower() 

101 search = payload['object']['search'].lower() 

102 pipeline = payload['object']['pipeline'].lower() 

103 # Special case of cWB-BBH that require the same label of CBC 

104 if search == 'bbh' and pipeline == 'cwb': 

105 group = 'cbc' 

106 if label == 'RAVEN_ALERT': 

107 log.info('Label %s added to %s', label, gid) 

108 elif label not in REQUIRED_LABELS_BY_GROUP_SEARCH[group][search]: 

109 return 

110 elif not is_complete(payload['object']): 

111 log.info("Ignoring since %s has %s labels. " 

112 "It is not complete", gid, payload['object']['labels']) 

113 return 

114 elif alert_type != 'new': 

115 return 

116 process.si(payload).apply_async(priority=priority) 

117 

118 

119@gracedb.task(queue='superevent', shared=False, time_limit=600) 

120@gracedb.catch_retryable_http_errors 

121def process(payload): 

122 """ 

123 Respond to `payload` and serially processes them to create new superevents, 

124 add events to existing ones. 

125 

126 Parameters 

127 ---------- 

128 payload : dict 

129 IGWN alert payload 

130 

131 """ 

132 event_info = payload['object'] 

133 gid = event_info['graceid'] 

134 category = get_category(event_info) 

135 t_0, t_start, t_end = get_ts(event_info) 

136 

137 if event_info.get('superevent'): 

138 sid = event_info['superevent'] 

139 log.info('Event %s already belongs to superevent %s', gid, sid) 

140 # superevent_neighbours has current and nearby superevents 

141 s = event_info['superevent_neighbours'][sid] 

142 superevent = _SuperEvent(s['t_start'], 

143 s['t_end'], 

144 s['t_0'], 

145 s['superevent_id']) 

146 _update_superevent(superevent.superevent_id, 

147 event_info, 

148 t_0=t_0, 

149 t_start=None, 

150 t_end=None) 

151 else: 

152 log.info('Event %s does not yet belong to a superevent', gid) 

153 superevents = gracedb.get_superevents('category: {} {} .. {}'.format( 

154 category, 

155 event_info['gpstime'] - app.conf['superevent_query_d_t_start'], 

156 event_info['gpstime'] + app.conf['superevent_query_d_t_end'])) 

157 for s in superevents: 

158 if gid in s['gw_events']: 

159 sid = s['superevent_id'] 

160 log.info('Event %s found assigned to superevent %s', gid, sid) 

161 if payload['alert_type'] == 'label_added': 

162 log.info('Label %s added to %s', 

163 payload['data']['name'], gid) 

164 elif payload['alert_type'] == 'new': 

165 log.info('new alert type for %s with ' 

166 'existing superevent %s. ' 

167 'No action required', gid, sid) 

168 return 

169 superevent = _SuperEvent(s['t_start'], 

170 s['t_end'], 

171 s['t_0'], 

172 s['superevent_id']) 

173 _update_superevent(superevent.superevent_id, 

174 event_info, 

175 t_0=t_0, 

176 t_start=None, 

177 t_end=None) 

178 break 

179 else: # s not in superevents 

180 event_segment = _Event(t_start, t_end, t_0, event_info['graceid']) 

181 

182 superevent = _partially_intersects(superevents, 

183 event_segment) 

184 

185 if superevent: 

186 sid = superevent.superevent_id 

187 log.info('Event %s in window of %s. ' 

188 'Adding event to superevent', gid, sid) 

189 gracedb.add_event_to_superevent(sid, event_segment.gid) 

190 # extend the time window of the superevent 

191 new_superevent = superevent | event_segment 

192 if new_superevent != superevent: 

193 log.info('%s not completely contained in %s, ' 

194 'extending superevent window', 

195 event_segment.gid, sid) 

196 new_t_start, new_t_end = new_superevent 

197 

198 else: # new_superevent == superevent 

199 log.info('%s is completely contained in %s', 

200 event_segment.gid, sid) 

201 new_t_start = new_t_end = None 

202 _update_superevent(superevent.superevent_id, 

203 event_info, 

204 t_0=t_0, 

205 t_start=new_t_start, 

206 t_end=new_t_end) 

207 else: # not superevent 

208 log.info('New event %s with no superevent in GraceDB, ' 

209 'creating new superevent', gid) 

210 sid = gracedb.create_superevent(event_info['graceid'], 

211 t_0, t_start, t_end) 

212 

213 if should_publish(event_info, significant=True): 

214 gracedb.create_label.delay('ADVREQ', sid) 

215 if is_complete(event_info): 

216 if EARLY_WARNING_LABEL in event_info['labels']: 

217 gracedb.create_label(EARLY_WARNING_LABEL, sid) 

218 else: 

219 gracedb.create_label(SIGNIFICANT_LABEL, sid) 

220 

221 if should_publish(event_info, significant=False): 

222 if is_complete(event_info): 

223 gracedb.create_label(FROZEN_LABEL, sid) 

224 

225 

226def get_category(event): 

227 """Get the superevent category for an event. 

228 

229 Parameters 

230 ---------- 

231 event : dict 

232 Event dictionary (e.g., the return value from 

233 :meth:`gwcelery.tasks.gracedb.get_event`). 

234 

235 Returns 

236 ------- 

237 {'mdc', 'test', 'production'} 

238 

239 """ 

240 if event.get('search') == 'MDC': 

241 return 'mdc' 

242 elif event['group'] == 'Test': 

243 return 'test' 

244 else: 

245 return 'production' 

246 

247 

248def get_ts(event): 

249 """Get time extent of an event, depending on pipeline-specific parameters. 

250 

251 * For CWB and MLy, use the event's ``duration`` field. 

252 * For oLIB, use the ratio of the event's ``quality_mean`` and 

253 ``frequency_mean`` fields. 

254 * For all other pipelines, use the 

255 :obj:`~gwcelery.conf.superevent_d_t_start` and 

256 :obj:`~gwcelery.conf.superevent_d_t_end` configuration values. 

257 

258 Parameters 

259 ---------- 

260 event : dict 

261 Event dictionary (e.g., the return value from 

262 :meth:`gwcelery.tasks.gracedb.get_event` or 

263 ``preferred_event_data`` in igwn-alert packet.) 

264 

265 Returns 

266 ------- 

267 t_0: float 

268 Segment center time in GPS seconds. 

269 t_start : float 

270 Segment start time in GPS seconds. 

271 

272 t_end : float 

273 Segment end time in GPS seconds. 

274 

275 """ 

276 pipeline = event['pipeline'].lower() 

277 if pipeline == 'cwb': 

278 attribs = event['extra_attributes']['MultiBurst'] 

279 d_t_start = d_t_end = attribs['duration'] 

280 elif pipeline == 'olib': 

281 attribs = event['extra_attributes']['LalInferenceBurst'] 

282 d_t_start = d_t_end = (attribs['quality_mean'] / 

283 attribs['frequency_mean']) 

284 elif pipeline == 'mly': 

285 attribs = event['extra_attributes']['MLyBurst'] 

286 d_t_start = d_t_end = attribs['duration'] 

287 else: 

288 d_t_start = app.conf['superevent_d_t_start'].get( 

289 pipeline, app.conf['superevent_default_d_t_start']) 

290 d_t_end = app.conf['superevent_d_t_end'].get( 

291 pipeline, app.conf['superevent_default_d_t_end']) 

292 return (event['gpstime'], event['gpstime'] - d_t_start, 

293 event['gpstime'] + d_t_end) 

294 

295 

296def get_snr(event): 

297 """Get the SNR from the LVAlert packet. 

298 

299 Different groups and pipelines store the SNR in different fields. 

300 

301 Parameters 

302 ---------- 

303 event : dict 

304 Event dictionary (e.g., the return value from 

305 :meth:`gwcelery.tasks.gracedb.get_event`, or 

306 ``preferred_event_data`` in igwn-alert packet.) 

307 

308 Returns 

309 ------- 

310 snr : float 

311 The SNR. 

312 

313 """ 

314 group = event['group'].lower() 

315 pipeline = event['pipeline'].lower() 

316 if group == 'cbc': 

317 attribs = event['extra_attributes']['CoincInspiral'] 

318 return attribs['snr'] 

319 elif pipeline == 'cwb': 

320 attribs = event['extra_attributes']['MultiBurst'] 

321 return attribs['snr'] 

322 elif pipeline == 'olib': 

323 attribs = event['extra_attributes']['LalInferenceBurst'] 

324 return attribs['omicron_snr_network'] 

325 elif pipeline == 'mly': 

326 attribs = event['extra_attributes']['MLyBurst'] 

327 return attribs['SNR'] 

328 else: 

329 raise NotImplementedError('SNR attribute not found') 

330 

331 

332def get_instruments(event): 

333 """Get the instruments that contributed data to an event. 

334 

335 Parameters 

336 ---------- 

337 event : dict 

338 Event dictionary (e.g., the return value from 

339 :meth:`gwcelery.tasks.gracedb.get_event`, or 

340 ``preferred_event_data`` in igwn-alert packet.) 

341 

342 Returns 

343 ------- 

344 set 

345 The set of instruments that contributed to the event. 

346 The instruments that contributed to the event are 

347 generally stored in the instrument field of the G-event 

348 as a comma separated list. For pipeline that 

349 provide the 'SingleInspiral' use the list 

350 of the detector there. 

351 """ 

352 if (('extra_attributes' in event) and 

353 ('SingleInspiral' in event['extra_attributes'])): 

354 attribs = event['extra_attributes']['SingleInspiral'] 

355 ifos = {single['ifo'] for single in attribs} 

356 else: 

357 ifos = set(event['instruments'].split(',')) 

358 return ifos 

359 

360 

361def get_instruments_in_ranking_statistic(event): 

362 """Get the instruments that contribute to the false alarm rate. 

363 

364 Parameters 

365 ---------- 

366 event : dict 

367 Event dictionary (e.g., the return value from 

368 :meth:`gwcelery.tasks.gracedb.get_event`, or 

369 ``preferred_event_data`` in igwn-alert packet.) 

370 

371 Returns 

372 ------- 

373 set 

374 The set of instruments that contributed to the ranking statistic for 

375 the event. 

376 

377 Notes 

378 ----- 

379 The number of instruments that contributed *data* to an event is given by 

380 the ``instruments`` key of the GraceDB event JSON structure. However, some 

381 pipelines (e.g. gstlal) have a distinction between which instruments 

382 contributed *data* and which were considered in the *ranking* of the 

383 candidate. All the pipeline should conform to this rule. 

384 

385 Unmodeled searches do not provide a SingleInspiral table, In such case 

386 use the event information. 

387 """ 

388 if (('extra_attributes' in event) and 

389 ('SingleInspiral' in event['extra_attributes'])): 

390 attribs = event['extra_attributes']['SingleInspiral'] 

391 ifos = {single['ifo'] for single in attribs 

392 if single.get('chisq') is not None} 

393 else: 

394 ifos = set(event['instruments'].split(',')) 

395 return ifos 

396 

397 

398@app.task(shared=False) 

399def select_pipeline_preferred_event(events): 

400 """Group list of G events by pipeline, and apply :meth:`keyfunc` 

401 to select the pipeline preferred events. 

402 

403 Parameters 

404 ---------- 

405 events : list 

406 list of event dictionaries 

407 

408 Returns 

409 ------- 

410 dict 

411 pipeline, graceid pairs 

412 """ 

413 g_events = list( 

414 filterfalse(lambda x: x['group'] == "External", events)) 

415 g_events_by_pipeline = groupby( 

416 sorted(g_events, key=lambda x: x['pipeline']), 

417 key=lambda x: x['pipeline'] 

418 ) 

419 

420 return dict( 

421 (k, select_preferred_event(g)) for k, g in g_events_by_pipeline) 

422 

423 

424@app.task(shared=False) 

425def select_preferred_event(events): 

426 """Select the preferred event out of a list of G events, typically 

427 contents of a superevent, based on :meth:`keyfunc`. 

428 

429 Parameters 

430 ---------- 

431 events : list 

432 list of event dictionaries 

433 

434 """ 

435 g_events = list( 

436 filterfalse(lambda x: x['group'] == "External", events)) 

437 return max(g_events, key=keyfunc) 

438 

439 

440def is_complete(event): 

441 """ 

442 Determine if a G event is complete in the sense of the event 

443 has its data products complete i.e. labels mentioned under 

444 :data:`REQUIRED_LABELS_BY_GROUP_SEARCH`. Test events are not 

445 processed by low-latency infrastructure and are always labeled 

446 complete. 

447 

448 Parameters 

449 ---------- 

450 event : dict 

451 Event dictionary (e.g., the return value from 

452 :meth:`gwcelery.tasks.gracedb.get_event`, or 

453 ``preferred_event_data`` in igwn-alert packet.) 

454 

455 """ 

456 group = event['group'].lower() 

457 pipeline = event['pipeline'].lower() 

458 search = event['search'].lower() 

459 label_set = set(event['labels']) 

460 # Define the special case burst-cwb-bbh that is a CBC 

461 if pipeline == 'cwb' and search == 'bbh': 

462 group = 'cbc' 

463 

464 required_labels = REQUIRED_LABELS_BY_GROUP_SEARCH[group][search] 

465 return required_labels.issubset(label_set) 

466 

467 

468def should_publish(event, significant=True): 

469 """Determine whether an event should be published as a public alert. 

470 

471 All of the following conditions must be true for a public alert: 

472 

473 * The event's ``offline`` flag is not set. 

474 * The event's false alarm rate, weighted by the group-specific trials 

475 factor as specified by the 

476 :obj:`~gwcelery.conf.preliminary_alert_trials_factor` 

477 (:obj:`~gwcelery.conf.significant_alert_trials_factor`) 

478 configuration setting, is less than or equal to 

479 :obj:`~gwcelery.conf.preliminary_alert_far_threshold` 

480 (:obj:`~gwcelery.conf.significant_alert_far_threshold`) 

481 

482 Parameters 

483 ---------- 

484 event : dict 

485 Event dictionary (e.g., the return value from 

486 :meth:`gwcelery.tasks.gracedb.get_event`, or 

487 ``preferred_event_data`` in igwn-alert packet.) 

488 significant : bool 

489 Flag to use significant 

490 (:obj:`~gwcelery.conf.significant_alert_far_threshold`), 

491 or less-significant 

492 (:obj:`~gwcelery.conf.preliminary_alert_far_threshold`) 

493 FAR threshold. 

494 

495 Returns 

496 ------- 

497 should_publish : bool 

498 :obj:`True` if the event meets the criteria for a public alert or 

499 :obj:`False` if it does not. 

500 

501 """ 

502 return all(_should_publish(event, significant=significant)) 

503 

504 

505def _should_publish(event, significant=False): 

506 """Wrapper around :meth:`should_publish`. Returns the boolean returns of 

507 the publishability criteria as a tuple for later use. 

508 """ 

509 group = event['group'].lower() 

510 search = event.get('search', '').lower() 

511 if search in app.conf['significant_alert_far_threshold'][group]: 

512 low_signif_far_threshold = \ 

513 app.conf['preliminary_alert_far_threshold'][group][search] 

514 low_signif_trials_factor = \ 

515 app.conf['preliminary_alert_trials_factor'][group][search] 

516 signif_far_threshold = \ 

517 app.conf['significant_alert_far_threshold'][group][search] 

518 signif_trials_factor = \ 

519 app.conf['significant_alert_trials_factor'][group][search] 

520 

521 low_signif_far = low_signif_trials_factor * event['far'] 

522 signif_far = signif_trials_factor * event['far'] 

523 else: 

524 # Fallback in case an event is uploaded to an unlisted search 

525 low_signif_far = -1 * float('inf') 

526 signif_far = -1 * float('inf') 

527 

528 raven_coincidence = ('RAVEN_ALERT' in event['labels']) 

529 

530 # Ensure that anything that returns True for significant=True also returns 

531 # True for significant=False. For example, a significant EarlyWarning event 

532 # should return True for both significant=True and significant=False. 

533 if significant or signif_far < signif_far_threshold: 

534 far = signif_far 

535 far_threshold = signif_far_threshold 

536 else: 

537 far = low_signif_far 

538 far_threshold = low_signif_far_threshold 

539 

540 return (not event['offline'] and 'INJ' not in event['labels'], 

541 far <= far_threshold or raven_coincidence) 

542 

543 

544def keyfunc(event): 

545 """Key function for selection of the preferred event. 

546 

547 Return a value suitable for identifying the preferred event. Given events 

548 ``a`` and ``b``, ``a`` is preferred over ``b`` if 

549 ``keyfunc(a) > keyfunc(b)``, else ``b`` is preferred. 

550 

551 Parameters 

552 ---------- 

553 event : dict 

554 Event dictionary (e.g., the return value from 

555 :meth:`gwcelery.tasks.gracedb.get_event`). 

556 

557 Returns 

558 ------- 

559 key : tuple 

560 The comparison key. 

561 

562 Notes 

563 ----- 

564 Tuples are compared lexicographically in Python: they are compared 

565 element-wise until an unequal pair of elements is found. 

566 

567 """ 

568 group = event['group'].lower() 

569 search = event['search'].lower() 

570 try: 

571 group_rank = app.conf['superevent_candidate_preference'][group].get( 

572 search, -1 

573 ) 

574 except KeyError: 

575 group_rank = -1 

576 if group == 'cbc': 

577 n_ifos = len(get_instruments(event)) 

578 snr_or_far_ranking = get_snr(event) 

579 else: 

580 # We don't care about the number of detectors for burst events. 

581 n_ifos = -1 

582 # Smaller FAR -> higher IFAR -> more significant. 

583 # Use -FAR instead of IFAR=1/FAR so that rank for FAR=0 is defined. 

584 snr_or_far_ranking = -event['far'] 

585 

586 # Conditions that determine choice of the preferred event 

587 # event completeness comes first 

588 # then, publishability criteria for significant events 

589 # then, publishability criteria for less-significant events 

590 # then, CBC group is given higher priority over Burst 

591 # then, prioritize more number of detectors 

592 # finally, use SNR (FAR) between two CBC (Burst) events 

593 # See https://rtd.igwn.org/projects/gwcelery/en/latest/gwcelery.tasks.superevents.html#selection-of-the-preferred-event # noqa: E501 

594 return ( 

595 is_complete(event), 

596 *_should_publish(event, significant=True), 

597 *_should_publish(event, significant=False), 

598 group_rank, 

599 n_ifos, 

600 snr_or_far_ranking 

601 ) 

602 

603 

604def _update_superevent(superevent_id, new_event_dict, 

605 t_0, t_start, t_end): 

606 """Update preferred event and/or change time window. Events with multiple 

607 detectors take precedence over single-detector events, then CBC events take 

608 precedence over burst events, and any remaining tie is broken by SNR/FAR 

609 values for CBC/Burst. Single detector are not promoted to preferred event 

610 status, if existing preferred event is multi-detector 

611 

612 Parameters 

613 ---------- 

614 superevent_id : str 

615 the superevent_id 

616 new_event_dict : dict 

617 event info of the new trigger as a dictionary 

618 t_0 : float 

619 center time of `superevent_id`, None for no change 

620 t_start : float 

621 start time of `superevent_id`, None for no change 

622 t_end : float 

623 end time of `superevent_id`, None for no change 

624 

625 """ 

626 # labels and preferred event in the IGWN alert are not the latest 

627 superevent_dict = gracedb.get_superevent(superevent_id) 

628 

629 superevent_labels = superevent_dict['labels'] 

630 preferred_event_dict = superevent_dict['preferred_event_data'] 

631 kwargs = {} 

632 if t_start is not None: 

633 kwargs['t_start'] = t_start 

634 if t_end is not None: 

635 kwargs['t_end'] = t_end 

636 if FROZEN_LABEL not in superevent_labels: 

637 if keyfunc(new_event_dict) > keyfunc(preferred_event_dict): 

638 # update preferred event when LOW_SIGNIF_LOCKED is not applied 

639 kwargs['t_0'] = t_0 

640 kwargs['preferred_event'] = new_event_dict['graceid'] 

641 

642 if kwargs: 

643 gracedb.update_superevent(superevent_id, **kwargs) 

644 

645 # completeness takes first precedence in deciding preferred event 

646 # necessary and suffiecient condition to superevent as ready 

647 if is_complete(new_event_dict): 

648 gracedb.create_label.delay(READY_LABEL, superevent_id) 

649 

650 

651def _superevent_segment_list(superevents): 

652 """Ingests a list of superevent dictionaries, and returns a segmentlist 

653 with start and end times as the duration of each segment. 

654 

655 Parameters 

656 ---------- 

657 superevents : list 

658 List of superevent dictionaries (e.g., the values 

659 of field ``superevent_neighbours`` in igwn-alert packet). 

660 

661 Returns 

662 ------- 

663 superevent_list : segmentlist 

664 superevents as a segmentlist object 

665 

666 """ 

667 return segmentlist( 

668 [_SuperEvent(s['t_start'], s['t_end'], s['t_0'], s['superevent_id']) 

669 for s in superevents]) 

670 

671 

672def _partially_intersects(superevents, event_segment): 

673 """Similar to :meth:`segmentlist.find` except it also returns the segment 

674 of `superevents` which partially intersects argument. If there are more 

675 than one intersections, first occurence is returned. 

676 

677 Parameters 

678 ---------- 

679 superevents : list 

680 list of superevents. Typical value of 

681 ``superevent_neighbours.values()``. 

682 event_segment : segment 

683 segment object whose index is wanted 

684 

685 Returns 

686 ------- 

687 match_segment : segment 

688 segment in `self` which intersects. `None` if not found 

689 

690 """ 

691 # create a segmentlist using start and end times 

692 superevents = _superevent_segment_list(superevents) 

693 for s in superevents: 

694 if s.intersects(event_segment): 

695 return s 

696 return None 

697 

698 

699class _Event(segment): 

700 """An event implemented as an extension of :class:`segment`.""" 

701 

702 def __new__(cls, t_start, t_end, *args, **kwargs): 

703 return super().__new__(cls, t_start, t_end) 

704 

705 def __init__(self, t_start, t_end, t_0, gid): 

706 self.t_0 = t_0 

707 self.gid = gid 

708 

709 

710class _SuperEvent(segment): 

711 """An superevent implemented as an extension of :class:`segment`.""" 

712 

713 def __new__(cls, t_start, t_end, *args, **kwargs): 

714 return super().__new__(cls, t_start, t_end) 

715 

716 def __init__(self, t_start, t_end, t_0, sid): 

717 self.t_start = t_start 

718 self.t_end = t_end 

719 self.t_0 = t_0 

720 self.superevent_id = sid