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
« 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"""
5import math
6from datetime import datetime
7from typing import Literal
9from pydantic import BaseModel, ConfigDict, Field, computed_field, model_validator
12class StrictModel(BaseModel):
13 model_config = ConfigDict(extra="ignore")
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")
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")
53class EventMLyBurst(StrictModel):
54 """MLy-specific burst fields."""
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")
88class EventSingleInspiral(StrictModel):
89 """Single Inspiral fields for one IFO."""
91 # Identifiers
92 single_search: str | None = Field(None, description="Search type")
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)")
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")
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")
126 # Psi parameters
127 single_psi0: float | None = Field(None, description="Psi 0")
128 single_psi3: float | None = Field(None, description="Psi 3")
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")
139 # Beta
140 single_beta: float | None = Field(None, description="Beta")
142 single_f_final: float | None = Field(None, description="Final frequency (Hz)")
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 )
155 single_sigmasq: float | None = Field(None, description="Sigma squared")
156 single_rsqveto_duration: float | None = Field(None, description="RSQ veto duration")
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")
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")
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")
253class EventLinks(StrictModel):
254 """URL links from GraceDB."""
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")
265class EventPastro(StrictModel):
266 """Astrophysical source-class probabilities from p_astro.json."""
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 )
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 )
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 )
313 return self
316class EventEMBright(StrictModel):
317 """Source-class probabilities from em_bright.json."""
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 )
333class NormalizedEvent(StrictModel):
334 """
335 Flat representation of a gravitational wave event.
336 """
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 )
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"
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)")
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")
375 # CBC-specific (from CoincInspiral)
376 coinc: EventCoinc | None = Field(
377 None, description="Fields from CoincInspiral tables"
378 )
380 # Burst-specific (MultiBurst)
381 burst: EventBurst | None = Field(None, description="Fields from MultiBurst tables")
383 # MLy-specific (MLyBurst)
384 mly: EventMLyBurst | None = Field(None, description="Fields from MLyBurst tables")
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 )
400 # Labels
401 labels: EventLabels | None = Field(None, description="Event labels")
403 # File artifacts
404 p_astro: EventPastro | None = Field(None, description="Astrophysical probability")
405 em_bright: EventEMBright | None = Field(None, description="EMBright probability")
407 # Links
408 links: EventLinks | None = Field(None, description="GraceDB URL links")
410 # METADATA
411 last_updated: datetime = Field(description="Last update time")