Coverage for event_normalizer/models.py: 98%

242 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-28 13:07 +0000

1""" 

2Unified NormalizedEvent model for gravitational wave events. 

3""" 

4 

5import math 

6from datetime import datetime 

7from typing import Literal 

8 

9from pydantic import BaseModel, ConfigDict, Field, computed_field, model_validator 

10 

11 

12class StrictModel(BaseModel): 

13 model_config = ConfigDict(extra="ignore") 

14 

15 

16class EventCoinc(StrictModel): 

17 coinc_mass: float | None = Field(None, description="Total mass (solar masses)") 

18 coinc_mchirp: float | None = Field(None, description="Chirp mass (solar masses)") 

19 coinc_minimum_duration: float | None = Field( 

20 None, description="Minimum duration (seconds)" 

21 ) 

22 coinc_snr: float | None = Field(None, description="Signal-to-Noise Ratio") 

23 coinc_ifos: str | None = Field(None, description="Interferometers (H1,L1,V1)") 

24 coinc_end_time: int | None = Field(None, description="End time (GPS seconds)") 

25 coinc_end_time_ns: int | None = Field(None, description="End time nanoseconds") 

26 coinc_false_alarm_rate: float | None = Field(None, description="False alarm rate") 

27 coinc_combined_far: float | None = Field(None, description="Combined FAR") 

28 

29 

30class EventBurst(StrictModel): 

31 burst_duration: float | None = Field(None, description="Duration (seconds)") 

32 burst_mchirp: float | None = Field(None, description="Chirp mass (solar masses)") 

33 burst_snr: float | None = Field(None, description="Signal-to-Noise Ratio") 

34 burst_ifos: str | None = Field(None, description="Interferometers") 

35 burst_start_time: int | None = Field(None, description="Start time (GPS seconds)") 

36 burst_start_time_ns: int | None = Field(None, description="Start time nanoseconds") 

37 burst_strain: float | None = Field(None, description="Strain amplitude") 

38 burst_peak_time: int | None = Field(None, description="Peak time (GPS seconds)") 

39 burst_peak_time_ns: int | None = Field(None, description="Peak time nanoseconds") 

40 burst_central_freq: float | None = Field(None, description="Central frequency (Hz)") 

41 burst_bandwidth: float | None = Field(None, description="Bandwidth (Hz)") 

42 burst_amplitude: float | None = Field(None, description="Amplitude") 

43 burst_confidence: float | None = Field(None, description="Confidence") 

44 burst_false_alarm_rate: float | None = Field(None, description="False alarm rate") 

45 burst_ligo_axis_ra: float | None = Field(None, description="LIGO axis RA (deg)") 

46 burst_ligo_axis_dec: float | None = Field(None, description="LIGO axis Dec (deg)") 

47 burst_ligo_angle: float | None = Field(None, description="LIGO angle") 

48 burst_ligo_angle_sig: float | None = Field(None, description="LIGO angle sigma") 

49 burst_single_ifo_times: str | None = Field(None, description="Single IFO times") 

50 burst_code: str | None = Field(None, description="Pipeline code") 

51 

52 

53class EventMLyBurst(StrictModel): 

54 """MLy-specific burst fields.""" 

55 

56 mly_central_freq: float | None = Field(None, description="Central frequency (Hz)") 

57 mly_bandwidth: float | None = Field(None, description="Bandwidth (Hz)") 

58 mly_central_time: float | None = Field(None, description="Central time (GPS)") 

59 mly_detection_statistic: float | None = Field( 

60 None, description="Detection statistic" 

61 ) 

62 mly_duration: float | None = Field(None, description="Duration") 

63 mly_bbh: float | None = Field(None, description="BBH probability") 

64 mly_sglf: float | None = Field(None, description="SG low frequency") 

65 mly_sghf: float | None = Field(None, description="SG high frequency") 

66 mly_background: float | None = Field(None, description="Background") 

67 mly_glitch: float | None = Field(None, description="Glitch probability") 

68 mly_freq_correlation: float | None = Field( 

69 None, description="Frequency correlation" 

70 ) 

71 mly_mass1: float | None = Field(None, description="Mass 1 (solar masses)") 

72 mly_mass2: float | None = Field(None, description="Mass 2 (solar masses)") 

73 mly_mtotal: float | None = Field(None, description="Total mass (solar masses)") 

74 mly_mchirp: float | None = Field(None, description="Chirp mass (solar masses)") 

75 mly_spin1z: float | None = Field(None, description="Spin 1 z-component") 

76 mly_spin2z: float | None = Field(None, description="Spin 2 z-component") 

77 mly_end_time: int | None = Field(None, description="End time (GPS seconds)") 

78 mly_end_time_ns: int | None = Field(None, description="End time nanoseconds") 

79 mly_template_duration: float | None = Field( 

80 None, description="Template duration (s)" 

81 ) 

82 mly_SNR: float | None = Field(None, description="Signal-to-Noise Ratio") 

83 mly_scores_coherency: float | None = Field(None, description="Coherency score") 

84 mly_scores_coincidence: float | None = Field(None, description="Coincidence score") 

85 mly_scores_combined: float | None = Field(None, description="Combined score") 

86 

87 

88class EventSingleInspiral(StrictModel): 

89 """Single Inspiral fields for one IFO.""" 

90 

91 # Identifiers 

92 single_search: str | None = Field(None, description="Search type") 

93 

94 # Time 

95 single_end_time: int | None = Field(None, description="End time (GPS seconds)") 

96 single_end_time_ns: int | None = Field(None, description="End time nanoseconds") 

97 single_end_time_gmst: float | None = Field(None, description="End time GMST") 

98 single_impulse_time: int | None = Field(None, description="Impulse time") 

99 single_impulse_time_ns: int | None = Field(None, description="Impulse time ns") 

100 single_template_duration: float | None = Field( 

101 None, description="Template duration (s)" 

102 ) 

103 single_event_duration: float | None = Field(None, description="Event duration (s)") 

104 

105 single_amplitude: float | None = Field(None, description="Amplitude") 

106 single_eff_distance: float | None = Field( 

107 None, description="Effective distance (Mpc)" 

108 ) 

109 single_coa_phase: float | None = Field(None, description="Coalescence phase") 

110 single_mass1: float | None = Field(None, description="Mass 1 (solar masses)") 

111 single_mass2: float | None = Field(None, description="Mass 2 (solar masses)") 

112 single_mchirp: float | None = Field(None, description="Chirp mass (solar masses)") 

113 single_mtotal: float | None = Field(None, description="Total mass (solar masses)") 

114 single_eta: float | None = Field(None, description="Symmetric mass ratio") 

115 single_kappa: float | None = Field(None, description="Kappa parameter") 

116 single_chi: float | None = Field(None, description="Chi parameter") 

117 

118 # Tau parameters 

119 single_tau0: float | None = Field(None, description="Tau 0") 

120 single_tau2: float | None = Field(None, description="Tau 2") 

121 single_tau3: float | None = Field(None, description="Tau 3") 

122 single_tau4: float | None = Field(None, description="Tau 4") 

123 single_tau5: float | None = Field(None, description="Tau 5") 

124 single_ttotal: float | None = Field(None, description="Total tau") 

125 

126 # Psi parameters 

127 single_psi0: float | None = Field(None, description="Psi 0") 

128 single_psi3: float | None = Field(None, description="Psi 3") 

129 

130 # Alpha parameters 

131 single_alpha: float | None = Field(None, description="Alpha") 

132 single_alpha1: float | None = Field(None, description="Alpha 1") 

133 single_alpha2: float | None = Field(None, description="Alpha 2") 

134 single_alpha3: float | None = Field(None, description="Alpha 3") 

135 single_alpha4: float | None = Field(None, description="Alpha 4") 

136 single_alpha5: float | None = Field(None, description="Alpha 5") 

137 single_alpha6: float | None = Field(None, description="Alpha 6") 

138 

139 # Beta 

140 single_beta: float | None = Field(None, description="Beta") 

141 

142 single_f_final: float | None = Field(None, description="Final frequency (Hz)") 

143 

144 # SNR e chi-square 

145 single_snr: float | None = Field(None, description="Signal-to-Noise Ratio") 

146 single_chisq: float | None = Field(None, description="Chi-square") 

147 single_chisq_dof: int | None = Field(None, description="Chi-square DOF") 

148 single_bank_chisq: float | None = Field(None, description="Bank chi-square") 

149 single_bank_chisq_dof: int | None = Field(None, description="Bank chi-square DOF") 

150 single_cont_chisq: float | None = Field(None, description="Continuous chi-square") 

151 single_cont_chisq_dof: int | None = Field( 

152 None, description="Continuous chi-square DOF" 

153 ) 

154 

155 single_sigmasq: float | None = Field(None, description="Sigma squared") 

156 single_rsqveto_duration: float | None = Field(None, description="RSQ veto duration") 

157 

158 # Gamma parameters 

159 single_Gamma0: float | None = Field(None, description="Gamma 0") 

160 single_Gamma1: float | None = Field(None, description="Gamma 1") 

161 single_Gamma2: float | None = Field(None, description="Gamma 2") 

162 single_Gamma3: float | None = Field(None, description="Gamma 3") 

163 single_Gamma4: float | None = Field(None, description="Gamma 4") 

164 single_Gamma5: float | None = Field(None, description="Gamma 5") 

165 single_Gamma6: float | None = Field(None, description="Gamma 6") 

166 single_Gamma7: float | None = Field(None, description="Gamma 7") 

167 single_Gamma8: float | None = Field(None, description="Gamma 8") 

168 single_Gamma9: float | None = Field(None, description="Gamma 9") 

169 

170 # Spin 

171 single_spin1x: float | None = Field(None, description="Spin 1 x-component") 

172 single_spin1y: float | None = Field(None, description="Spin 1 y-component") 

173 single_spin1z: float | None = Field(None, description="Spin 1 z-component") 

174 single_spin2x: float | None = Field(None, description="Spin 2 x-component") 

175 single_spin2y: float | None = Field(None, description="Spin 2 y-component") 

176 single_spin2z: float | None = Field(None, description="Spin 2 z-component") 

177 

178 

179class EventLabels(StrictModel): 

180 label_2022_LENSING_MDC: bool | None = Field( 

181 None, description="2022_LENSING_MDC label" 

182 ) 

183 label_ADVNO: bool | None = Field(None, description="ADVNO label") 

184 label_ADVOK: bool | None = Field(None, description="ADVOK label") 

185 label_ADVREQ: bool | None = Field(None, description="ADVREQ label") 

186 label_COMBINEDSKYMAP_READY: bool | None = Field( 

187 None, description="COMBINEDSKYMAP_READY label" 

188 ) 

189 label_DQOK: bool | None = Field(None, description="DQOK label") 

190 label_DQR_REQUEST: bool | None = Field(None, description="DQR_REQUEST label") 

191 label_DQV: bool | None = Field(None, description="DQV label") 

192 label_EARLY_WARNING: bool | None = Field(None, description="EARLY_WARNING label") 

193 label_EMBRIGHT_READY: bool | None = Field(None, description="EMBRIGHT_READY label") 

194 label_EM_COINC: bool | None = Field(None, description="EM_COINC label") 

195 label_EM_READY: bool | None = Field(None, description="EM_READY label") 

196 label_EM_SENT: bool | None = Field(None, description="EM_SENT label") 

197 label_EM_Selected: bool | None = Field(None, description="EM_Selected label") 

198 label_EM_SelectedConfident: bool | None = Field( 

199 None, description="EM_SelectedConfident label" 

200 ) 

201 label_EM_Superseded: bool | None = Field(None, description="EM_Superseded label") 

202 label_EM_Throttled: bool | None = Field(None, description="EM_Throttled label") 

203 label_EXT_SKYMAP_READY: bool | None = Field( 

204 None, description="EXT_SKYMAP_READY label" 

205 ) 

206 label_GCN_PRELIM_SENT: bool | None = Field( 

207 None, description="GCN_PRELIM_SENT label" 

208 ) 

209 label_GRB_OFFLINE: bool | None = Field(None, description="GRB_OFFLINE label") 

210 label_GRB_ONLINE: bool | None = Field(None, description="GRB_ONLINE label") 

211 label_H1NO: bool | None = Field(None, description="H1NO label") 

212 label_H1OK: bool | None = Field(None, description="H1OK label") 

213 label_H1OPS: bool | None = Field(None, description="H1OPS label") 

214 label_HIGH_PROFILE: bool | None = Field(None, description="HIGH_PROFILE label") 

215 label_HWINJNO: bool | None = Field(None, description="HWINJNO label") 

216 label_HWINJOK: bool | None = Field(None, description="HWINJOK label") 

217 label_HWINJREQ: bool | None = Field(None, description="HWINJREQ label") 

218 label_INJ: bool | None = Field(None, description="INJ label") 

219 label_L1NO: bool | None = Field(None, description="L1NO label") 

220 label_L1OK: bool | None = Field(None, description="L1OK label") 

221 label_L1OPS: bool | None = Field(None, description="L1OPS label") 

222 label_LENSED_CANDIDATE: bool | None = Field( 

223 None, description="LENSED_CANDIDATE label" 

224 ) 

225 label_LLAMA_COMPLETE: bool | None = Field(None, description="LLAMA_COMPLETE label") 

226 label_LOW_SIGNIF_LOCKED: bool | None = Field( 

227 None, description="LOW_SIGNIF_LOCKED label" 

228 ) 

229 label_LOW_SIGNIF_PRELIM_SENT: bool | None = Field( 

230 None, description="LOW_SIGNIF_PRELIM_SENT label" 

231 ) 

232 label_LUMIN_GO: bool | None = Field(None, description="LUMIN_GO label") 

233 label_LUMIN_NO: bool | None = Field(None, description="LUMIN_NO label") 

234 label_MOCK: bool | None = Field(None, description="MOCK label") 

235 label_NOT_GRB: bool | None = Field(None, description="NOT_GRB label") 

236 label_PASTRO_READY: bool | None = Field(None, description="PASTRO_READY label") 

237 label_PE_READY: bool | None = Field(None, description="PE_READY label") 

238 label_RAVEN_ALERT: bool | None = Field(None, description="RAVEN_ALERT label") 

239 label_SIGNIF_LOCKED: bool | None = Field(None, description="SIGNIF_LOCKED label") 

240 label_SKYMAP_READY: bool | None = Field(None, description="SKYMAP_READY label") 

241 label_SNR_OPTIMIZED: bool | None = Field(None, description="SNR_OPTIMIZED label") 

242 label_SOG_READY: bool | None = Field(None, description="SOG_READY label") 

243 label_SUBSOLAR_MASS: bool | None = Field(None, description="SUBSOLAR_MASS label") 

244 label_SWIFT_GO: bool | None = Field(None, description="SWIFT_GO label") 

245 label_SWIFT_NO: bool | None = Field(None, description="SWIFT_NO label") 

246 label_V1NO: bool | None = Field(None, description="V1NO label") 

247 label_V1OK: bool | None = Field(None, description="V1OK label") 

248 label_V1OPS: bool | None = Field(None, description="V1OPS label") 

249 label_cWB_r: bool | None = Field(None, description="cWB_r label") 

250 label_cWB_s: bool | None = Field(None, description="cWB_s label") 

251 

252 

253class EventLinks(StrictModel): 

254 """URL links from GraceDB.""" 

255 

256 neighbors: str | None = Field(None, description="Neighbors URL") 

257 log: str | None = Field(None, description="Log URL") 

258 emobservations: str | None = Field(None, description="EM observations URL") 

259 files: str | None = Field(None, description="Files URL") 

260 labels: str | None = Field(None, description="Labels URL") 

261 self: str | None = Field(None, description="Self URL") 

262 tags: str | None = Field(None, description="Tags URL") 

263 

264 

265class EventPastro(StrictModel): 

266 """Astrophysical source-class probabilities from p_astro.json.""" 

267 

268 p_astro_terrestrial: float = Field( 

269 None, 

270 ge=0.0, 

271 le=1.0, 

272 description="Probability of terrestrial origin", 

273 ) 

274 p_astro_bns: float = Field( 

275 None, 

276 ge=0.0, 

277 le=1.0, 

278 description="Probability of binary neutron star origin", 

279 ) 

280 p_astro_bbh: float = Field( 

281 None, 

282 ge=0.0, 

283 le=1.0, 

284 description="Probability of binary black hole origin", 

285 ) 

286 p_astro_nsbh: float = Field( 

287 None, 

288 ge=0.0, 

289 le=1.0, 

290 description="Probability of neutron star-black hole origin", 

291 ) 

292 

293 @model_validator(mode="after") 

294 def validate_probability_sum(self) -> "EventPastro": 

295 """Validate that p_astro probabilities sum to approximately one.""" 

296 total = ( 

297 self.p_astro_terrestrial 

298 + self.p_astro_bns 

299 + self.p_astro_bbh 

300 + self.p_astro_nsbh 

301 ) 

302 

303 if not math.isclose( 

304 total, 

305 1.0, 

306 rel_tol=0.0, 

307 abs_tol=1e-6, 

308 ): 

309 raise ValueError( 

310 f"p_astro probabilities must sum to 1.0 (received {total:.12g})" 

311 ) 

312 

313 return self 

314 

315 

316class EventEMBright(StrictModel): 

317 """Source-class probabilities from em_bright.json.""" 

318 

319 em_bright_has_ns: float = Field( 

320 ge=0.0, le=1.0, description="Probability has neutron star" 

321 ) 

322 em_bright_has_remnant: float | None = Field( 

323 None, ge=0.0, le=1.0, description="Probability has remnant" 

324 ) 

325 em_bright_has_mass_gap: float | None = Field( 

326 None, ge=0.0, le=1.0, description="Probability in mass gap" 

327 ) 

328 em_bright_has_ssm: float | None = Field( 

329 None, ge=0.0, le=1.0, description="Probability has ssm" 

330 ) 

331 

332 

333class NormalizedEvent(StrictModel): 

334 """ 

335 Flat representation of a gravitational wave event. 

336 """ 

337 

338 # Identifiers 

339 uid: str = Field( 

340 ..., 

341 pattern=r"^[GSM]\d+(?:[a-z]+)?$", 

342 description="Unique identifier for the event (e.g G123456 or S260813fk)", 

343 ) 

344 

345 @computed_field 

346 @property 

347 def event_type(self) -> Literal["G-event", "S-event"]: 

348 return "G-event" if self.uid.startswith("G") else "S-event" 

349 

350 # COMPULSORY FIELDS 

351 group: str = Field(description="Event group (CBC, Burst)") 

352 pipeline: str = Field(description="Pipeline (gstlal, CWB, MLy, etc.)") 

353 search: str = Field(description="Search type (AllSky, etc.)") 

354 far: float = Field(description="False Alarm Rate (Hz)") 

355 far_is_upper_limit: bool | None = Field(None, description="FAR is upper limit") 

356 instruments: list[str] = Field(description="Detector list (H1,L1,V1)") 

357 reporting_latency: float = Field(description="Reporting latency (seconds)") 

358 gpstime: float = Field(description="GPS time of event (s)") 

359 

360 # OPTIONAL FIELDS - Root level 

361 H1_channel: str | None = Field("None", description="H1 channel name") 

362 L1_channel: str | None = Field("None", description="L1 channel name") 

363 V1_channel: str | None = Field("None", description="V1 channel name") 

364 K1_channel: str | None = Field("None", description="K1 channel name") 

365 alert_type: str | None = Field(None, description="Alert type (e.g., file_uploaded)") 

366 submitter: str | None = Field(None, description="User who submitted") 

367 offline: bool | None = Field(None, description="Offline analysis flag") 

368 nevents: int | None = Field(None, description="Number of events in coincidence") 

369 likelihood: float | None = Field(None, description="Log-likelihood ratio") 

370 superevent: str | None = Field(None, description="Superevent ID") 

371 created: str | None = Field(None, description="Creation time") 

372 processing_status: str | None = Field(None, description="Processing status") 

373 content_id: str | None = Field(None, description="Content ID") 

374 

375 # CBC-specific (from CoincInspiral) 

376 coinc: EventCoinc | None = Field( 

377 None, description="Fields from CoincInspiral tables" 

378 ) 

379 

380 # Burst-specific (MultiBurst) 

381 burst: EventBurst | None = Field(None, description="Fields from MultiBurst tables") 

382 

383 # MLy-specific (MLyBurst) 

384 mly: EventMLyBurst | None = Field(None, description="Fields from MLyBurst tables") 

385 

386 # Single Inspiral per IFO 

387 single_H1: EventSingleInspiral | None = Field( 

388 None, description="Single Inspiral for H1" 

389 ) 

390 single_L1: EventSingleInspiral | None = Field( 

391 None, description="Single Inspiral for L1" 

392 ) 

393 single_V1: EventSingleInspiral | None = Field( 

394 None, description="Single Inspiral for V1" 

395 ) 

396 single_K1: EventSingleInspiral | None = Field( 

397 None, description="Single Inspiral for K1" 

398 ) 

399 

400 # Labels 

401 labels: EventLabels | None = Field(None, description="Event labels") 

402 

403 # File artifacts 

404 p_astro: EventPastro | None = Field(None, description="Astrophysical probability") 

405 em_bright: EventEMBright | None = Field(None, description="EMBright probability") 

406 

407 # Links 

408 links: EventLinks | None = Field(None, description="GraceDB URL links") 

409 

410 # METADATA 

411 last_updated: datetime = Field(description="Last update time")