From ace542cba58ecb065ace57100ba4a45d453cc7d9 Mon Sep 17 00:00:00 2001 From: serversdown Date: Wed, 27 May 2026 22:47:53 +0000 Subject: [PATCH] report_pdf: wire histogram peak date/time + PVS-when + Finish field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spotted comparing our PDF to BW's reference for T003LLUB.CE0H: - Finish blank - Per-channel Date / Time rows all dashes - MicL PSPL line missing "on May 27, 2026 at 06:19:14" - Peak Vector Sum missing "on May 27, 2026 At 06:06:14" Root cause: I'd added these fields to the projection (write side) in _bw_report_to_dict but never wired them into gather_report_data (read side). Plus the projection used keys "start"/"stop" while gather was reading "start_str"/"stop_str" — typo'd lookup. Fixes: - gather_report_data now reads bw_report.histogram.start / .stop / .channel_peak_when (correct keys, matching the projection) - Per-channel "peak_date" / "peak_time" populated from channel_peak_when[] for the histogram stats table - MicL PSPL line formats as "PSPL 125.7 dB(L) on May 27, 2026 at 06:19:14" (BW style) when channel_peak_when["MicL"] is present; falls back to the waveform-relative "at 0.012 sec" otherwise - PVS line formats as "Peak Vector Sum 0.091 in/s on May 27, 2026 At 06:06:14" (BW style) when bw_report.peaks.vector_sum.when is populated; falls back to the relative time_s for waveforms - New _split_iso_to_date_time() helper splits ISO timestamps into BW-formatted ("May 27 /26", "06:06:14") date+time pairs for the stats table's separate Date and Time rows Events ingested BEFORE the parser extension landed (most of the existing prod corpus) still show dashes — their sidecars lack the histogram block. Re-forwarding repopulates. Co-Authored-By: Claude Opus 4.7 (1M context) --- sfm/report_pdf.py | 87 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/sfm/report_pdf.py b/sfm/report_pdf.py index 9a256a0..1df8699 100644 --- a/sfm/report_pdf.py +++ b/sfm/report_pdf.py @@ -97,6 +97,7 @@ class ReportData: mic_pspl_dbl: Optional[float] = None mic_pspl_psi: 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_channel_test_result: Optional[str] = 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_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 {} 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")): ch = peaks.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({ "name": ch_label, "ppv_ips": ch.get("ppv_ips"), @@ -234,25 +242,30 @@ def gather_report_data( "peak_accel_g": ch.get("peak_accel_g"), "peak_disp_in": ch.get("peak_disp_in"), "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 vs = peaks.get("vector_sum") or {} rd.peak_vector_sum_ips = vs.get("ips") 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 - # at ingest time (when present); the parsed bw_report dict - # 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. + # Histogram-specific header fields — keys match the projection in + # _bw_report_to_dict ("start" / "stop", not "_str" suffixed). if rd.is_histogram: - hist = bw.get("histogram") or {} - rd.histogram_start_str = hist.get("start_str") or rd.event_datetime_str - rd.histogram_stop_str = hist.get("stop_str") - rd.histogram_n_intervals = hist.get("n_intervals") - rd.histogram_interval_size = hist.get("interval_size") - rd.histogram_interval_times = hist.get("interval_times") or [] + rd.histogram_start_str = hist_block.get("start") or rd.event_datetime_str + rd.histogram_stop_str = hist_block.get("stop") + rd.histogram_n_intervals = hist_block.get("n_intervals") + rd.histogram_interval_size = hist_block.get("interval_size") + rd.histogram_interval_times = hist_block.get("interval_times") or [] # ── Waveform samples — from the .h5 via the existing helper ── from sfm import event_hdf5 @@ -376,6 +389,24 @@ def _fmt_iso_to_bw(iso: Optional[str]) -> Optional[str]: 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): """Render a 'Label Value' row at axes-coordinates (x, y).""" 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]]]: - """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]]] = [] if rd.mic_pspl_dbl is not None: 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." rows.append(("PSPL", line)) 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) 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" - if when: - line += f" on {when}" + # Histograms: "0.091 in/s on May 27, 2026 At 06:06:14" + # 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", ha="left", va="top", transform=ax.transAxes) ax.text(0.0, -0.18, "NA: Not Applicable", fontsize=7, color="#888",