minimateplus: histogram body codec — FULLY DECODED
The histogram-mode event body is now byte-exact decodable.
Companion to the waveform body codec — together they cover every
event file the watcher forwards. Cracked in one session via
cross-event correlation against BW's ASCII export.
The §7.6.2 spec in instantel_protocol_reference.md was structurally
correct (32-byte blocks) but the per-sample semantics were
under-documented. Cross-checking block 130 of N844L6Z8.ZR0H
against its TXT row revealed the layout perfectly:
slot[0] = 10 (constant marker)
slot[1] = T_peak_count (× 0.005 → in/s at Normal range)
slot[2] = T_halfperiod (freq_Hz = 512 / halfp)
slot[3] = V_peak_count
slot[4] = V_halfperiod
slot[5] = L_peak_count
slot[6] = L_halfperiod
slot[7] = MicL_peak_count (dB via waveform_codec.mic_count_to_db)
slot[8] = MicL_halfperiod
The `>100 Hz` sentinel is halfperiod ≤ 5 (since 512/5 = 100 Hz).
Mic dB uses the SAME formula as the waveform codec (sign × (81.94
+ 20·log10(|count|))) — they share the mic ADC calibration constant.
Block identification anchor: bytes [22:24] == 0x0000 AND
bytes [28:32] == 1e 0a 00 00. The tail signature is the most
reliable distinguisher from non-block content in the file.
Files:
minimateplus/histogram_codec.py (new) — decoder + public API
matching the waveform codec's shape:
walk_body(body) -> records
decode_histogram_body(body) -> {Tran, Vert, Long, MicL}
decode_histogram_body_full(body) -> [per-interval dicts]
half_period_to_hz, geo_count_to_ins helpers
minimateplus/event_file_io.py (modified) — read_blastware_file
now tries the waveform codec first, falls back to the histogram
codec on failure. Same output shape, same downstream pipeline.
tests/test_histogram_codec.py (new) — 24 regression locks against
the in-repo fixture corpus, byte-exact against BW ASCII export
for peaks (all 4 channels), frequencies (all 4 channels,
including >100 Hz sentinel handling), block framing, and
segment-ID accounting.
scripts/backfill_sidecars.py (modified) — the has_samples
short-circuit added in the histogram-pending era is now a
pure defensive guard. Histograms in prod will regen .h5 files
correctly on the next backfill run.
docs/histogram_codec_re_status.md (updated) — supersedes the
earlier "in progress" version with the verified format and
test-coverage summary. Notes a few non-essential fields still
open (4-byte block metadata, Geo PVS, Mic psi(L) — none of
which are needed for waveform reconstruction).
Total verified coverage: ~3,500 blocks across 5 fixtures, every
field of every block byte-exact against BW.
The watcher-forwarded histogram event corpus on prod (~10,000
events) will now produce correct .h5 sidecars on the next backfill
run. No additional changes needed to the backfill flow — the
existing tool_version-bump cascade picks them up automatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ from .models import Event, PeakValues, ProjectInfo, Timestamp
|
||||
from . import blastware_file as _bw # avoid circular reference at module load
|
||||
from .bw_ascii_report import BwAsciiReport
|
||||
from .waveform_codec import decode_waveform_v2, decoded_to_adc_counts
|
||||
from .histogram_codec import decode_histogram_body
|
||||
|
||||
# Reference pressure for dB(L) → psi conversion (20 µPa expressed in psi).
|
||||
# Same constant as sfm/sfm_webapp.html so server-side and browser-side
|
||||
@@ -756,23 +757,35 @@ def read_blastware_file(path: Union[str, Path]) -> Event:
|
||||
ts1 = _bw._decode_ts_be(footer[2:10])
|
||||
ts2 = _bw._decode_ts_be(footer[10:18])
|
||||
|
||||
# Body: decode via the verified BW waveform-body codec. The body
|
||||
# starts with the codec's 7-byte preamble ``00 02 00 [Tran[0] BE]
|
||||
# [Tran[1] BE]`` and continues with the tagged-block stream the codec
|
||||
# walks. See ``minimateplus/waveform_codec.py`` + ``docs/waveform_codec_re_status.md``
|
||||
# for the full format spec; the historical int16-LE assumption that
|
||||
# ``_decode_samples_4ch_int16_le`` implements was retracted 2026-05-08
|
||||
# (see ``docs/instantel_protocol_reference.md`` §7.6.1).
|
||||
# Body: decode via the verified body codecs. Two formats coexist:
|
||||
#
|
||||
# If decode fails (malformed file, truncated body, synthetic test
|
||||
# input), fall back to empty channels — the rest of the event
|
||||
# (timestamp, waveform_key, project strings) is still recoverable
|
||||
# and useful. The peaks-from-samples helper handles empty input
|
||||
# gracefully.
|
||||
# 1. Waveform-mode (.AB0W) — starts with 7-byte preamble
|
||||
# ``00 02 00 [Tran[0] BE] [Tran[1] BE]`` followed by the
|
||||
# tagged-block delta stream documented in
|
||||
# ``docs/waveform_codec_re_status.md`` and §7.6.1 of the
|
||||
# protocol reference. Decoded by ``waveform_codec.decode_waveform_v2``.
|
||||
#
|
||||
# 2. Histogram-mode (.AB0H) — a sequence of 32-byte blocks, one
|
||||
# per histogram interval, each carrying per-channel peak +
|
||||
# half-period values. Decoded by
|
||||
# ``histogram_codec.decode_histogram_body``. Both codecs
|
||||
# return the same channel-grouped output shape, so consumers
|
||||
# don't need to special-case mode.
|
||||
#
|
||||
# The historical ``_decode_samples_4ch_int16_le`` int16-LE
|
||||
# interpretation was retracted 2026-05-08 (see protocol-ref §7.6.1
|
||||
# retraction box) — it produced ±32K noise on every event.
|
||||
#
|
||||
# If both codecs fail (malformed file, truncated body, unrecognised
|
||||
# mode, synthetic test input), fall back to empty channels — the
|
||||
# rest of the event (timestamp, waveform_key, project strings) is
|
||||
# still recoverable and useful.
|
||||
decoded = decode_waveform_v2(body)
|
||||
if decoded is None:
|
||||
decoded = decode_histogram_body(body)
|
||||
if decoded is None:
|
||||
log.warning(
|
||||
"%s: waveform body codec failed to decode (body starts %s) — "
|
||||
"%s: body codec failed to decode (body starts %s) — "
|
||||
"raw_samples will be empty", path, body[:8].hex(" "),
|
||||
)
|
||||
samples = {"Tran": [], "Vert": [], "Long": [], "MicL": []}
|
||||
|
||||
Reference in New Issue
Block a user