v0.20.0 -- Full s3 event parse and PDF creation. #28

Merged
serversdown merged 46 commits from dev into main 2026-05-28 17:54:34 -04:00
Showing only changes of commit a5888e1b5c - Show all commits
+55 -11
View File
@@ -285,6 +285,43 @@ def gather_report_data(
except Exception as exc: except Exception as exc:
log.warning("gather_report_data: hdf5 read failed: %s", exc) log.warning("gather_report_data: hdf5 read failed: %s", exc)
# ── Histogram aggregation ──
# Codec emits ~N per-block samples (typically 1/sec); BW reports
# one bar per configured interval (1 min / 5 min / etc.). When
# bw_report.histogram.n_intervals is populated (events ingested
# with the parser extension), group max-per-group to match. Also
# derives per-interval timestamps for the x-axis. No-op for
# waveform events or when n_intervals is missing.
if rd.is_histogram and rd.histogram_n_intervals and rd.histogram_n_intervals >= 1:
n = int(rd.histogram_n_intervals)
for ch, vals in list(rd.channels.items()):
if not vals:
continue
per_group = len(vals) // n
remainder = len(vals) % n
agg: list = []
offset = 0
for i in range(n):
grp_size = per_group + (1 if i < remainder else 0)
if grp_size > 0:
grp = vals[offset:offset + grp_size]
agg.append(max((abs(v) for v in grp if v is not None), default=0))
offset += grp_size
else:
agg.append(0)
rd.channels[ch] = agg
# Derive per-interval HH:MM:SS labels if we have the start time + size
if rd.histogram_start_str and rd.histogram_interval_size_s and not rd.histogram_interval_times:
try:
import datetime as _dt
start = _dt.datetime.fromisoformat(rd.histogram_start_str)
rd.histogram_interval_times = [
(start + _dt.timedelta(seconds=(i + 1) * rd.histogram_interval_size_s)).strftime("%H:%M:%S")
for i in range(n)
]
except Exception:
pass
return rd return rd
@@ -308,16 +345,18 @@ def render_event_report_pdf(rd: ReportData) -> bytes:
else: else:
_render_waveform_layout(fig, rd) _render_waveform_layout(fig, rd)
# Footer (common to both layouts) — Created date + Xmark-like attribution. # Page footer (common to both layouts) — Created date + event id.
# Pushed to the very page bottom so it doesn't collide with the
# waveform footer scale / trigger legend lines just above.
fig.text( fig.text(
0.07, 0.015, 0.07, 0.005,
f"Created: {rd.server_received_at or ''} • seismo-relay", f"Created: {rd.server_received_at or ''} • seismo-relay",
fontsize=7, color="#888", ha="left", fontsize=6, color="#888", ha="left",
) )
fig.text( fig.text(
0.93, 0.015, 0.93, 0.005,
f"Event {rd.event_id[:8] if rd.event_id else ''}", f"Event {rd.event_id[:8] if rd.event_id else ''}",
fontsize=7, color="#888", ha="right", fontsize=6, color="#888", ha="right",
) )
buf = io.BytesIO() buf = io.BytesIO()
@@ -331,10 +370,13 @@ def _render_waveform_layout(fig, rd: ReportData) -> None:
Stats table includes Time (Rel. to Trig), Peak Accel, Peak Disp. Stats table includes Time (Rel. to Trig), Peak Accel, Peak Disp.
Left margin sized to fit the channel labels (MicL/Long/Vert/Tran). Left margin sized to fit the channel labels (MicL/Long/Vert/Tran).
Extra bottom margin reserves space for x-axis tick labels +
"Amplitude Geo: X in/s/div Mic: Y psi(L)/div" footer + trigger
legend without overlap.
""" """
gs = fig.add_gridspec( gs = fig.add_gridspec(
nrows=4, ncols=1, nrows=4, ncols=1,
left=0.11, right=0.94, top=0.97, bottom=0.06, left=0.11, right=0.94, top=0.97, bottom=0.12,
height_ratios=[1.7, 2.0, 1.8, 5.5], height_ratios=[1.7, 2.0, 1.8, 5.5],
hspace=0.35, hspace=0.35,
) )
@@ -355,11 +397,13 @@ def _render_histogram_layout(fig, rd: ReportData) -> None:
No USBM compliance chart (it's a waveform-only concept). Stats table No USBM compliance chart (it's a waveform-only concept). Stats table
uses Date + Time-of-peak instead of relative-time + accel + disp. uses Date + Time-of-peak instead of relative-time + accel + disp.
Left margin sized to fit the channel labels. Left margin sized to fit the channel labels. Extra bottom margin
leaves room for the x-axis time labels + footer scale legend
without overlap.
""" """
gs = fig.add_gridspec( gs = fig.add_gridspec(
nrows=4, ncols=1, nrows=4, ncols=1,
left=0.11, right=0.94, top=0.97, bottom=0.06, left=0.11, right=0.94, top=0.97, bottom=0.12,
height_ratios=[1.8, 0.9, 1.7, 5.6], height_ratios=[1.8, 0.9, 1.7, 5.6],
hspace=0.35, hspace=0.35,
) )
@@ -718,12 +762,12 @@ def _draw_waveform_subplot(fig, gridspec_cell, rd: ReportData) -> None:
geo_amp_div = f"{(amax * 1.1 * 2) / 10:.3f}" geo_amp_div = f"{(amax * 1.1 * 2) / 10:.3f}"
break break
fig.text( fig.text(
0.07, 0.045, 0.11, 0.030,
f"Time(Seconds) {div_s:.2f} sec/div Amplitude Geo: {geo_amp_div} in/s/div Mic: 0.001 psi(L)/div", f"Time(Seconds) {div_s:.2f} sec/div Amplitude Geo: {geo_amp_div} in/s/div Mic: 0.001 psi(L)/div",
fontsize=7, color="#444", ha="left", fontsize=7, color="#444", ha="left",
) )
fig.text( fig.text(
0.07, 0.030, 0.11, 0.018,
"Trigger = ▶━━━━━ ━━━━━━◀", "Trigger = ▶━━━━━ ━━━━━━◀",
fontsize=7, color="#444", ha="left", fontsize=7, color="#444", ha="left",
) )
@@ -789,7 +833,7 @@ def _draw_histogram_subplot(fig, gridspec_cell, rd: ReportData) -> None:
geo_amp_div = f"{amax / 5:.3f}" geo_amp_div = f"{amax / 5:.3f}"
break break
fig.text( fig.text(
0.07, 0.045, 0.11, 0.030,
f"Time {interval_str} /div Amplitude Geo: {geo_amp_div} in/s/div Mic: 0.001 psi(L)/div", f"Time {interval_str} /div Amplitude Geo: {geo_amp_div} in/s/div Mic: 0.001 psi(L)/div",
fontsize=7, color="#444", ha="left", fontsize=7, color="#444", ha="left",
) )