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 ace542cba5 - Show all commits
+70 -17
View File
@@ -97,6 +97,7 @@ class ReportData:
mic_pspl_dbl: Optional[float] = None mic_pspl_dbl: Optional[float] = None
mic_pspl_psi: Optional[float] = None mic_pspl_psi: Optional[float] = None
mic_pspl_time_s: Optional[float] = None mic_pspl_time_s: Optional[float] = None
mic_pspl_when_str: Optional[str] = None # histogram absolute date+time, BW-formatted
mic_zc_freq_hz: Optional[float] = None mic_zc_freq_hz: Optional[float] = None
mic_channel_test_result: Optional[str] = None mic_channel_test_result: Optional[str] = None
mic_channel_test_freq_hz: Optional[float] = None mic_channel_test_freq_hz: Optional[float] = None
@@ -220,12 +221,19 @@ def gather_report_data(
rd.mic_channel_test_freq_hz = sc_mic.get("freq_hz") rd.mic_channel_test_freq_hz = sc_mic.get("freq_hz")
rd.mic_channel_test_amp_mv = sc_mic.get("amplitude_mv") rd.mic_channel_test_amp_mv = sc_mic.get("amplitude_mv")
# Per-channel stats (Tran / Vert / Long) # Per-channel stats (Tran / Vert / Long). Per-channel peak
# date+time for histograms comes from bw_report.histogram.channel_peak_when
# (populated when the parser captured it; see the bw_ascii_report
# parser's histogram-fields handler).
peaks = bw.get("peaks") or {} peaks = bw.get("peaks") or {}
sc_block = bw.get("sensor_check") or {} sc_block = bw.get("sensor_check") or {}
hist_block = bw.get("histogram") or {}
peak_when = hist_block.get("channel_peak_when") or {}
for ch_lc, ch_label in (("tran", "Tran"), ("vert", "Vert"), ("long", "Long")): for ch_lc, ch_label in (("tran", "Tran"), ("vert", "Vert"), ("long", "Long")):
ch = peaks.get(ch_lc) or {} ch = peaks.get(ch_lc) or {}
sc_ch = sc_block.get(ch_lc) or {} sc_ch = sc_block.get(ch_lc) or {}
ch_when_iso = peak_when.get(ch_label)
peak_date, peak_time = _split_iso_to_date_time(ch_when_iso)
rd.channel_stats.append({ rd.channel_stats.append({
"name": ch_label, "name": ch_label,
"ppv_ips": ch.get("ppv_ips"), "ppv_ips": ch.get("ppv_ips"),
@@ -234,25 +242,30 @@ def gather_report_data(
"peak_accel_g": ch.get("peak_accel_g"), "peak_accel_g": ch.get("peak_accel_g"),
"peak_disp_in": ch.get("peak_disp_in"), "peak_disp_in": ch.get("peak_disp_in"),
"sensor_check": sc_ch.get("result"), "sensor_check": sc_ch.get("result"),
"peak_date": peak_date,
"peak_time": peak_time,
}) })
# MicL peak time (used in the mic block — "PSPL ... on DATE at TIME")
mic_when_iso = peak_when.get("MicL")
rd.mic_pspl_when_str = _fmt_iso_to_bw(mic_when_iso) if mic_when_iso else None
# Peak Vector Sum # Peak Vector Sum
vs = peaks.get("vector_sum") or {} vs = peaks.get("vector_sum") or {}
rd.peak_vector_sum_ips = vs.get("ips") rd.peak_vector_sum_ips = vs.get("ips")
rd.peak_vector_sum_time_s = vs.get("time_s") rd.peak_vector_sum_time_s = vs.get("time_s")
# PVS absolute date+time (histograms). Same formatting as Mic.
pvs_when_iso = vs.get("when")
rd.peak_vector_sum_when_str = _fmt_iso_to_bw(pvs_when_iso) if pvs_when_iso else None
# Histogram-specific header fields. These come from the BW XML # Histogram-specific header fields — keys match the projection in
# at ingest time (when present); the parsed bw_report dict # _bw_report_to_dict ("start" / "stop", not "_str" suffixed).
# carries them under the 'histogram' sub-block (added by the
# BW XML parser once that lands). For now, derive from the
# event timestamp + recording config as a best-effort.
if rd.is_histogram: if rd.is_histogram:
hist = bw.get("histogram") or {} rd.histogram_start_str = hist_block.get("start") or rd.event_datetime_str
rd.histogram_start_str = hist.get("start_str") or rd.event_datetime_str rd.histogram_stop_str = hist_block.get("stop")
rd.histogram_stop_str = hist.get("stop_str") rd.histogram_n_intervals = hist_block.get("n_intervals")
rd.histogram_n_intervals = hist.get("n_intervals") rd.histogram_interval_size = hist_block.get("interval_size")
rd.histogram_interval_size = hist.get("interval_size") rd.histogram_interval_times = hist_block.get("interval_times") or []
rd.histogram_interval_times = hist.get("interval_times") or []
# ── Waveform samples — from the .h5 via the existing helper ── # ── Waveform samples — from the .h5 via the existing helper ──
from sfm import event_hdf5 from sfm import event_hdf5
@@ -376,6 +389,24 @@ def _fmt_iso_to_bw(iso: Optional[str]) -> Optional[str]:
return iso return iso
def _split_iso_to_date_time(iso: Optional[str]) -> tuple[Optional[str], Optional[str]]:
"""Split an ISO timestamp into BW-formatted ("May 27 /26", "06:06:14")
date+time strings. Used for the histogram stats table where the
Date and Time rows are presented separately. Returns (None, None)
if the input isn't a valid ISO datetime."""
if not iso:
return (None, None)
try:
import datetime as _dt
dt = _dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))
# BW format: "May 27 /26" (3-letter month + 2-digit year)
date_str = dt.strftime("%b %d /%y").replace(" 0", " ")
time_str = dt.strftime("%H:%M:%S")
return (date_str, time_str)
except Exception:
return (None, None)
def _kv(ax, x, y, label, value, *, label_w=0.18): def _kv(ax, x, y, label, value, *, label_w=0.18):
"""Render a 'Label Value' row at axes-coordinates (x, y).""" """Render a 'Label Value' row at axes-coordinates (x, y)."""
ax.text(x, y, label, fontsize=8, color="#555", ha="left", va="top", ax.text(x, y, label, fontsize=8, color="#555", ha="left", va="top",
@@ -489,11 +520,28 @@ def _draw_mic_and_usbm(ax, rd: ReportData) -> None:
def _mic_rows(rd: ReportData) -> list[tuple[str, Optional[str]]]: def _mic_rows(rd: ReportData) -> list[tuple[str, Optional[str]]]:
"""Build the mic-section value rows (shared by both layouts).""" """Build the mic-section value rows (shared by both layouts).
For histograms, BW formats the PSPL line as
"125.7 dB(L) on May 27, 2026 at 06:19:14"
(absolute date+time of peak). Waveform events show the relative
"at 0.012 sec." instead. Both formats covered here based on which
field is populated.
"""
rows: list[tuple[str, Optional[str]]] = [] rows: list[tuple[str, Optional[str]]] = []
if rd.mic_pspl_dbl is not None: if rd.mic_pspl_dbl is not None:
line = f"{rd.mic_pspl_dbl:.1f} dB(L)" line = f"{rd.mic_pspl_dbl:.1f} dB(L)"
if rd.mic_pspl_time_s is not None: if rd.mic_pspl_when_str:
# Histogram-style: "PSPL 125.7 dB(L) on May 27, 2026 at 06:19:14"
# mic_pspl_when_str is already "HH:MM:SS Month DD, YYYY";
# reformat to "on Month DD, YYYY at HH:MM:SS" for BW match.
parts = rd.mic_pspl_when_str.split(" ", 1)
if len(parts) == 2:
line += f" on {parts[1]} at {parts[0]}"
else:
line += f" on {rd.mic_pspl_when_str}"
elif rd.mic_pspl_time_s is not None:
# Waveform-style: relative-to-trigger seconds.
line += f" at {rd.mic_pspl_time_s:.3f} sec." line += f" at {rd.mic_pspl_time_s:.3f} sec."
rows.append(("PSPL", line)) rows.append(("PSPL", line))
if rd.mic_zc_freq_hz is not None: if rd.mic_zc_freq_hz is not None:
@@ -545,10 +593,15 @@ def _draw_channel_stats_histogram(ax, rd: ReportData) -> None:
] ]
_draw_stats_table(ax, rd, rows_spec) _draw_stats_table(ax, rd, rows_spec)
if rd.peak_vector_sum_ips is not None: if rd.peak_vector_sum_ips is not None:
when = rd.peak_vector_sum_when_str or ""
line = f"Peak Vector Sum {rd.peak_vector_sum_ips:.3f} in/s" line = f"Peak Vector Sum {rd.peak_vector_sum_ips:.3f} in/s"
if when: # Histograms: "0.091 in/s on May 27, 2026 At 06:06:14"
line += f" on {when}" # The when_str is "HH:MM:SS Month DD, YYYY" — reformat for BW match.
if rd.peak_vector_sum_when_str:
parts = rd.peak_vector_sum_when_str.split(" ", 1)
if len(parts) == 2:
line += f" on {parts[1]} At {parts[0]}"
else:
line += f" on {rd.peak_vector_sum_when_str}"
ax.text(0.0, -0.08, line, fontsize=9, weight="bold", ax.text(0.0, -0.08, line, fontsize=9, weight="bold",
ha="left", va="top", transform=ax.transAxes) ha="left", va="top", transform=ax.transAxes)
ax.text(0.0, -0.18, "NA: Not Applicable", fontsize=7, color="#888", ax.text(0.0, -0.18, "NA: Not Applicable", fontsize=7, color="#888",