Release v0.31.0 — report parity + the inverted rescue (0.29.0 → 0.31.0) #40

Merged
serversdown merged 31 commits from dev into main 2026-09-18 16:46:45 -04:00
Showing only changes of commit dc74c97ade - Show all commits
+62 -25
View File
@@ -396,18 +396,25 @@ def _render_waveform_layout(fig, rd: ReportData) -> None:
ax_stats = fig.add_subplot(gs[2]); ax_stats.axis("off")
_draw_channel_stats_waveform(ax_stats, rd)
_draw_compliance_panel(fig, gs, rd)
_draw_compliance_panel(fig, rd)
_draw_waveform_subplot(fig, gs[3], rd)
def _draw_compliance_panel(fig, gs, rd: ReportData) -> None:
"""Large square USBM RI8507 compliance chart in the upper-right, spanning the
mic (row 1) and stats (row 2) rows — matching Blastware's Event Report."""
bottoms, tops, _lefts, rights = gs.get_grid_positions(fig)
y0, y1 = bottoms[2], tops[1] # bottom of stats row → top of mic row
x0, x1 = 0.64, rights[0] # clear of the stats columns → page right margin
fig.text((x0 + x1) / 2, y1 + 0.004, "USBM RI8507", fontsize=9, weight="bold",
color="#333", ha="center", va="bottom")
# Compliance-chart placement, in figure fractions. Measured directly off a
# Blastware Event Report PDF (ref-stuff/n844lqhbzt0w_bw_pdf.pdf) so the chart
# matches BW's size and position: it spans from just under the header down
# through the stats band, hard against the right page margin. The left edge
# leaves room for the y-axis tick labels + "Velocity (in/s)" title, which the
# compacted stats table (see _draw_channel_stats_waveform) is sized to clear.
_COMPLIANCE_BOX = (0.489, 0.502, 0.951, 0.867) # x0, y0, x1, y1
def _draw_compliance_panel(fig, rd: ReportData) -> None:
"""Large USBM RI8507 compliance chart in the upper-right, sized and
positioned to match Blastware's Event Report (see _COMPLIANCE_BOX)."""
x0, y0, x1, y1 = _COMPLIANCE_BOX
fig.text((x0 + x1) / 2, y1 + 0.006, "USBM RI8507 And OSMRE", fontsize=9,
weight="bold", color="#333", ha="center", va="bottom")
if rd.channels and rd.sample_rate_sps:
from sfm.compliance import draw_compliance_chart
ax = fig.add_axes([x0, y0, x1 - x0, y1 - y0])
@@ -495,11 +502,11 @@ def _split_iso_to_date_time(iso: Optional[str]) -> tuple[Optional[str], Optional
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, fontsize=8):
"""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=fontsize, color="#555", ha="left", va="top",
transform=ax.transAxes)
ax.text(x + label_w, y, _fmt(value), fontsize=8, ha="left", va="top",
ax.text(x + label_w, y, _fmt(value), fontsize=fontsize, ha="left", va="top",
transform=ax.transAxes, family="monospace")
@@ -592,8 +599,11 @@ def _draw_mic_and_usbm(ax, rd: ReportData) -> None:
transform=ax.transAxes, va="top")
rows = _mic_rows(rd)
y = 0.80
# Tighter label indent + slightly smaller font so the long "Channel Test
# Passed (Freq = … Amp = … mv)" line clears the enlarged compliance chart's
# left edge (_COMPLIANCE_BOX) instead of running behind it.
for label, value in rows:
_kv(ax, 0.0, y, label, value, label_w=0.18)
_kv(ax, 0.0, y, label, value, label_w=0.13, fontsize=7)
y -= 0.15
# The USBM compliance chart is drawn as its own large square panel spanning
# the mic + stats rows on the right — see _draw_compliance_panel().
@@ -647,7 +657,13 @@ def _draw_channel_stats_waveform(ax, rd: ReportData) -> None:
("Peak Displacement", "peak_disp_in", "in"),
("Sensor Check", "sensor_check", ""),
]
_draw_stats_table(ax, rd, rows_spec)
# Compacted to the left half so the enlarged compliance chart (BW-sized,
# right against the page margin) has room — see _COMPLIANCE_BOX.
_draw_stats_table(
ax, rd, rows_spec,
bbox_width=0.42, fontsize=7.5,
col_widths=[0.185, 0.065, 0.065, 0.065, 0.040],
)
_draw_pvs_summary(ax, rd, n_data_rows=len(rows_spec))
@@ -708,19 +724,39 @@ def _draw_pvs_summary(
table_bottom_y = getattr(ax, "_stats_table_bottom", -0.10)
pvs_y = table_bottom_y - 0.04 # small gap below the table border
# Centered for visual balance — looks intentional rather than offset.
# The original BW-replica had a "NA: Not Applicable" caption below
# this line; dropped because we use "—" for missing values and the
# legend was always squished against the PVS line.
ax.text(0.5, pvs_y, line, fontsize=9, weight="bold",
ha="center", va="top", transform=ax.transAxes)
# Centered under the stats table for visual balance — looks intentional
# rather than offset. When the table is compacted (waveform layout), it
# occupies only the left portion of the axes, so center on the table's
# width rather than the full axes (which would push the line under the
# compliance chart). The original BW-replica had a "NA: Not Applicable"
# caption below this line; dropped because we use "—" for missing values.
table_w = getattr(ax, "_stats_table_width", 0.80)
if table_w < 0.79:
# Compacted (waveform) layout: left-align under the table, one point
# smaller, so the line clears the enlarged compliance chart's
# bottom-left tick labels on the right.
ax.text(0.0, pvs_y, line, fontsize=8, weight="bold",
ha="left", va="top", transform=ax.transAxes)
else:
ax.text(0.5, pvs_y, line, fontsize=9, weight="bold",
ha="center", va="top", transform=ax.transAxes)
def _draw_stats_table(ax, rd: ReportData, rows_spec: list[tuple[str, str, str]]) -> None:
def _draw_stats_table(
ax, rd: ReportData, rows_spec: list[tuple[str, str, str]],
*, bbox_width: float = 0.80, fontsize: float = 8,
col_widths: Optional[list[float]] = None,
) -> None:
"""Render a per-channel stats table (Tran/Vert/Long).
rows_spec: list of (label, field_name_in_channel_stats, unit_string)
``bbox_width`` / ``col_widths`` / ``fontsize`` let a caller compact the
table (the waveform layout packs it into the left half to clear the
compliance chart; the histogram layout keeps the wider defaults).
"""
if col_widths is None:
col_widths = [0.28, 0.14, 0.14, 0.14, 0.10]
headers = ["", "Tran", "Vert", "Long", ""]
ch_lookup = {c["name"]: c for c in rd.channel_stats}
@@ -760,16 +796,17 @@ def _draw_stats_table(ax, rd: ReportData, rows_spec: list[tuple[str, str, str]])
table_bottom = 1.0 - table_height
tbl = ax.table(
cellText=table_data,
colWidths=[0.28, 0.14, 0.14, 0.14, 0.10],
colWidths=col_widths,
cellLoc="left", edges="open",
bbox=[0.0, table_bottom, 0.80, table_height],
bbox=[0.0, table_bottom, bbox_width, table_height],
)
tbl.auto_set_font_size(False)
tbl.set_fontsize(8)
tbl.set_fontsize(fontsize)
for j in range(5):
tbl[(0, j)].set_text_props(weight="bold", color="#555")
# Stash the bottom Y so _draw_pvs_summary can position itself below.
# Stash the bottom Y + width so _draw_pvs_summary can position itself.
ax._stats_table_bottom = table_bottom
ax._stats_table_width = bbox_width
def _channel_axis_color(ch: str) -> str: