codec-re: crack Tran channel codec with high-amplitude May 11 bundle

User uploaded 3 high-amplitude events (PPV 6-7 in/s — shook the geophone
hard) to decode-re/5-11-26/.  These cracked the Tran codec:

- Preamble bytes [3:5] and [5:7] = Tran[0] and Tran[1] as int16 BE
  in 16-count units (LSB = 0.005 in/s).  Confirmed across all 7
  fixtures.
- First data block carries Tran deltas from sample 2 onward:
  * 10 NN block: NN/2 bytes of payload, each byte = two 4-bit signed
    nibble deltas (high nibble first)
  * 20 NN block: NN int8 signed deltas

Verified 22+42+46 = 110 Tran samples across SP0/SS0/SV0 with 0 errors
against BW's ASCII export.

Why the earlier 96-combination brute force failed: the quiet 5-8
events all had T[0] = T[1] ≈ 0 so the preamble's per-channel encoding
was undetectable.  Loud events made the encoding obvious.

What's solved:
- minimateplus.waveform_codec.decode_tran_initial: returns first
  N Tran samples in 16-count units for any body.
- Walker length formula for in-data 30 NN blocks (NN*2 instead of NN*4).
- Walker now handles bodies that start with 20 NN (in addition to 10 NN).

What's still open:
- Tran past the first data block (multi-block channel switching).
- Vert / Long / MicL channel encodings.
- Walker correctness past offset ~427 in event-b.

Tests: 36 pass.  decode_waveform_v2 still returns None — the full
multi-channel decoder is not wired up.  decode_tran_initial is the
new verified entry point.

Files: minimateplus/waveform_codec.py, tests/test_waveform_codec.py
(adds 5-11-26 fixtures + decode_tran_initial tests), and
docs/instantel_protocol_reference.md §7.6.1 (Tran codec spec).
This commit is contained in:
Claude
2026-05-11 18:30:56 +00:00
committed by serversdown
parent d3f77d1d96
commit 6ac126e05c
14 changed files with 10113 additions and 50 deletions
+50
View File
@@ -0,0 +1,50 @@
"""Quick inspection of the new high-amplitude events."""
import os, re, sys
sys.path.insert(0, ".")
from analysis.load_bundle import _parse_txt
from minimateplus.waveform_codec import walk_body, find_data_start
ROOT = "decode-re/5-11-26"
def main():
for stem in ("M529LL1A.SP0", "M529LL1A.SS0", "M529LL1A.SV0"):
bin_path = os.path.join(ROOT, stem)
txt_path = bin_path + ".TXT"
with open(bin_path, "rb") as f:
raw = f.read()
body = raw[43:-26]
meta, samples = _parse_txt(txt_path)
n = len(samples["Tran"])
print(f"\n=== {stem} ===")
print(f" file={len(raw)}, body={len(body)}, N_samples={n}")
print(f" rectime={meta.get('Record Time')} pretrig={meta.get('Pre-trigger Length')}")
print(f" PPV(T,V,L)={meta.get('Tran PPV')} / {meta.get('Vert PPV')} / {meta.get('Long PPV')}")
# Show first few non-trivial samples
print(f" First 5 truth samples (in/s):")
for i in range(5):
print(f" T={samples['Tran'][i]:8.3f} V={samples['Vert'][i]:8.3f} "
f"L={samples['Long'][i]:8.3f} M={samples['MicL'][i]:8.3f}")
# Peak sample positions
for ch in ("Tran", "Vert", "Long"):
vals = samples[ch]
peak_i = max(range(n), key=lambda i: abs(vals[i]))
print(f" {ch}: peak {vals[peak_i]:.3f} at sample {peak_i} (t={peak_i/1024:.3f}s)")
# Body structure
start = find_data_start(body)
blocks = walk_body(body, start)
types = {}
for b in blocks:
types[b.tag_hi] = types.get(b.tag_hi, 0) + 1
print(f" body start={start}, total blocks walked: {len(blocks)}")
print(f" block tag counts: {types}")
# How far the walker got
if blocks:
last = blocks[-1]
walked = last.offset + last.length
print(f" walker stopped at offset {walked}/{len(body)} ({100*walked/len(body):.0f}%)")
if __name__ == "__main__":
main()
+71
View File
@@ -0,0 +1,71 @@
"""Test: does the second '20 NN' block in SS0 continue Tran samples?"""
import sys
sys.path.insert(0, ".")
from analysis.load_bundle import _parse_txt
from minimateplus.waveform_codec import walk_body, find_data_start
def s4(n):
return n if n < 8 else n - 16
def i8(b):
return b if b < 128 else b - 256
def main():
stem = "M529LL1A.SS0"
path = f"decode-re/5-11-26/{stem}"
with open(path, "rb") as f:
body = f.read()[43:-26]
_, samples = _parse_txt(path + ".TXT")
truth_T_16 = [round(v * 200) for v in samples["Tran"]]
# Preamble
T0 = int.from_bytes(body[3:5], "big", signed=True)
T1 = int.from_bytes(body[5:7], "big", signed=True)
# Walk blocks
start = find_data_start(body)
blocks = walk_body(body, start)
print(f"=== {stem} === T[0]={T0} T[1]={T1}")
# Hypothesis: Tran continues through ALL 10 NN and 20 NN blocks
# in order, until the next 40 02 segment header (which resets).
T = [T0, T1]
cur = T1
decoded_count = 2 # T[0], T[1] from preamble
for bi, blk in enumerate(blocks):
if blk.tag_hi == 0x10:
for byte in blk.data:
for nib in ((byte >> 4) & 0xF, byte & 0xF):
cur += s4(nib)
T.append(cur)
decoded_count += 1
elif blk.tag_hi == 0x20:
for byte in blk.data:
cur += i8(byte)
T.append(cur)
decoded_count += 1
elif blk.tag_hi == 0x40:
# Segment header — stop here for this test
break
# 00 and 30 NN don't contribute to Tran (in this hypothesis)
# Compare to truth
print(f" Decoded {len(T)} T samples up to first 40 02")
matches = sum(1 for i in range(min(len(T), len(truth_T_16))) if T[i] == truth_T_16[i])
print(f" Matches in first {min(len(T), len(truth_T_16))}: {matches}")
# Print first divergence
for i in range(min(len(T), len(truth_T_16))):
if T[i] != truth_T_16[i]:
print(f" First divergence: sample {i}: pred={T[i]}, truth={truth_T_16[i]}")
# Show context
print(f" pred [{i-3}:{i+5}]: {T[max(0,i-3):i+5]}")
print(f" truth [{i-3}:{i+5}]: {truth_T_16[max(0,i-3):i+5]}")
break
if __name__ == "__main__":
main()
+71
View File
@@ -0,0 +1,71 @@
"""Verify: preamble[3:7] = Tran[0], Tran[1] as int16 BE in 16-count units.
And first 20/10 NN block = Tran deltas starting at sample 2.
"""
import os, sys
sys.path.insert(0, ".")
from analysis.load_bundle import _parse_txt
from minimateplus.waveform_codec import walk_body, find_data_start
def s4(n):
return n if n < 8 else n - 16
def i8(b):
return b if b < 128 else b - 256
def main():
for stem in ("M529LL1A.SP0", "M529LL1A.SS0", "M529LL1A.SV0"):
path = f"decode-re/5-11-26/{stem}"
with open(path, "rb") as f:
raw = f.read()
body = raw[43:-26]
_, samples = _parse_txt(path + ".TXT")
truth_T_16 = [round(v * 200) for v in samples["Tran"]]
# Preamble parse
T0_pre = int.from_bytes(body[3:5], "big", signed=True)
T1_pre = int.from_bytes(body[5:7], "big", signed=True)
print(f"\n=== {stem} ===")
print(f" Preamble T[0]={T0_pre} (truth {truth_T_16[0]}) T[1]={T1_pre} (truth {truth_T_16[1]}) match={T0_pre==truth_T_16[0] and T1_pre==truth_T_16[1]}")
# First block
start = find_data_start(body)
blocks = walk_body(body, start)
if not blocks:
print(f" no blocks found")
continue
# Assume first block = Tran deltas from sample 2
first = blocks[0]
T = [T0_pre, T1_pre]
cur_T = T1_pre
if first.tag_hi == 0x10:
# Nibble pairs
for byte in first.data:
for nib in ((byte >> 4) & 0xF, byte & 0xF):
cur_T += s4(nib)
T.append(cur_T)
elif first.tag_hi == 0x20:
# int8 per byte
for byte in first.data:
cur_T += i8(byte)
T.append(cur_T)
# Compare against truth
n_check = min(len(T), len(truth_T_16))
match_count = sum(1 for i in range(n_check) if T[i] == truth_T_16[i])
print(f" First block type=0x{first.tag_hi:02x} NN=0x{first.tag_lo:02x} len={len(first.data)}{len(T)} T samples decoded")
print(f" Tran predicted[0:10]: {T[:10]}")
print(f" Tran truth [0:10]: {truth_T_16[:10]}")
print(f" Matches in first {n_check}: {match_count} / {n_check}")
# Show where it diverges
for i in range(n_check):
if T[i] != truth_T_16[i]:
print(f" First divergence: sample {i}: pred={T[i]}, truth={truth_T_16[i]}")
break
if __name__ == "__main__":
main()
+20
View File
@@ -0,0 +1,20 @@
"""Walk blocks of the new 5-11-26 events and look at what comes after Tran block."""
import sys
sys.path.insert(0, ".")
from minimateplus.waveform_codec import walk_body, find_data_start
def main():
for stem in ("M529LL1A.SP0", "M529LL1A.SS0", "M529LL1A.SV0"):
with open(f"decode-re/5-11-26/{stem}", "rb") as f:
raw = f.read()
body = raw[43:-26]
start = find_data_start(body)
blocks = walk_body(body, start)
print(f"\n=== {stem} === body={len(body)} start={start} blocks walked={len(blocks)}")
for i, b in enumerate(blocks[:20]):
print(f" block[{i:>2}] @ {b.offset:>5} tag={b.tag_hi:02x} NN=0x{b.tag_lo:02x}({b.tag_lo}) len={b.length} data[:24]={b.data[:24].hex(' ')}")
if __name__ == "__main__":
main()
+74 -39
View File
@@ -860,13 +860,14 @@ MicL: 39 64 1D AA = 0.0000875 psi
---
#### 7.6.1 Blast / Waveform mode — 🟡 STRUCTURAL FRAMING DECODED (2026-05-08)
#### 7.6.1 Blast / Waveform mode — 🟡 STRUCTURAL FRAMING + TRAN CODEC DECODED (2026-05-11)
> **Status (2026-05-08):** Block-level framing is solved and verified
> against the 4-event May 8 2026 bundle (3 sec / 2 sec / 1 sec / 1 sec
> events captured live from BE11529). The per-byte mapping from block
> data to ADC samples is **still open** — the previous int16 LE claim
> is REFUTED (see history below).
> **Status (2026-05-11):** Block-level framing is solved. The Tran-channel
> encoding (preamble + first data block) is **fully verified** against the
> 3-event May 11 2026 high-amplitude bundle (PPV 6-7 in/s) and the 4-event
> May 8 bundle. Verts / Long / MicL channel encodings and multi-block
> Tran continuation are **still open**. The previous int16 LE claim
> remains REFUTED (see history below).
>
> The earlier "4-channel interleaved s16 LE, 8 bytes per sample-set"
> claim was never validated and was wrong. No event in the project's
@@ -886,13 +887,32 @@ the 21-byte STRT record and the 26-byte file footer) is composed of
[trailer: per-channel summary blocks]
```
**Preamble:** starts with the 4-byte magic ``00 02 00 00``. Single-shot
events have a 7-byte preamble; continuous events have a 9-byte preamble
(the 4 events in the May 8 2026 bundle split 2/2 between the two
lengths). Bytes [4:9] of the preamble appear to encode initial
per-channel state but the layout has not been pinned down — for some
events byte [4] equals truth Tran[0] in 16-count units (0.005 in/s
LSB), but other channel-byte assignments don't fit consistently.
**Preamble (CONFIRMED 2026-05-11 across 3+4 events):**
```
body[0:3] = 00 02 00 magic
body[3:5] = Tran[0] int16 BE first Tran sample (LSB = 0.005 in/s)
body[5:7] = Tran[1] int16 BE second Tran sample
```
The preamble is therefore 7 bytes long. Earlier observations of a
"9-byte preamble" on continuous-mode events were a misread — those
events still have a 7-byte preamble; the next 2 bytes are part of the
first ``10 NN`` or ``20 NN`` data block (its tag).
Verified preamble decode for all 7 fixture events — Tran[0] and Tran[1]
from the preamble bytes exactly match the BW ASCII export (rounded to
0.005 in/s):
| Event | Preamble [3:7] (hex) | T[0] decoded | T[0] truth | T[1] decoded | T[1] truth |
|---|---|---|---|---|---|
| event-a (May 8) | ``01 00 00 00`` | +1 | +1 (0.005) | 0 | 0 |
| event-b (May 8) | ``ff ff ff 00`` | -1 | -1 | -1 | -1 |
| event-c (May 8) | ``00 00 00 00`` | 0 | 0 | 0 | 0 |
| event-d (May 8) | ``00 00 00 00`` | 0 | 0 | 0 | 0 |
| SP0 (May 11) | ``00 04 00 04`` | +4 | +4 (0.020) | +4 | +4 |
| SS0 (May 11) | ``ff a7 ff a7`` | -89 | -89 (-0.445) | -89 | -89 |
| SV0 (May 11) | ``fd 17 fd 06`` | -745 | -745 (-3.725) | -762 | -762 |
##### Block tags (CONFIRMED 2026-05-08)
@@ -951,40 +971,54 @@ in the form ``f3/f4/f5`` near ``20 10`` markers strongly resemble
int8 channel-bias values around -12). Detailed decoding of the
trailer is outside the path needed for sample reconstruction.
##### Tran channel codec — CONFIRMED 2026-05-11
The first data block (immediately after the 7-byte preamble) carries
Tran-channel deltas starting at sample 2. Two block types in alternation:
- ``10 NN``: ``NN/2`` bytes of payload. Each byte = two 4-bit signed
nibbles (high nibble first; 0..7 → 0..+7, 8..F → -8..-1). Each
nibble is one Tran delta in 16-count units.
- ``20 NN``: ``NN`` bytes of payload. Each byte = one int8 signed delta
in 16-count units.
Verified against all 3 May-11 fixture events:
| Event | First block | # T samples decoded | Matches truth |
|---|---|---|---|
| SP0 | ``10 14`` (10 bytes / 20 nibbles) | 22 (= 2 preamble + 20 deltas) | 22/22 ✓ |
| SS0 | ``10 28`` (20 bytes / 40 nibbles) | 42 | 42/42 ✓ |
| SV0 | ``20 2c`` (44 int8 bytes) | 46 | 46/46 ✓ |
Implementation: :func:`minimateplus.waveform_codec.decode_tran_initial`.
##### What's still open
- **The byte → sample mapping inside ``10 NN`` and ``20 NN`` blocks.**
Tested hypotheses that did not match BW's ASCII export to within ±1
ADC count:
- **Tran past the first data block.** After the first block, the
body has more ``10 NN`` / ``20 NN`` blocks separated by ``00 NN``
markers and occasionally ``30 NN`` blocks. Naive continuation
(treat all subsequent ``10/20 NN`` blocks as Tran) does NOT match
truth past the first block — the codec interleaves channels somehow.
``30 04`` markers appearing in SS0 between blocks 1 and 5 look
like channel-switch tags, but the switching rule has not been
fully decoded.
1. ``10 NN`` data = 4-bit signed nibble deltas, channel-interleaved,
all 24 channel permutations × 2 nibble orders × 2 sign conventions
× 2 init-from-header settings (= 96 combinations). All produce
values that diverge from truth after the first ~7 sample-sets.
2. ``20 NN`` data = int8 absolute or delta samples for one channel.
Magnitudes in observed blocks (peak ±34 in event-c at offset 351)
do not match any channel's PPV at any plausible ADC quantization
(1-count, 4-count, 8-count, 16-count).
3. ``00 NN`` marker = "skip N sample-sets with zero deltas". Sums
of NN/4 across markers do not consistently match the 80
sample-sets-per-segment count.
- **Vert / Long / MicL channel encodings.** No verified decoder
exists for these yet. Hypotheses tested without success:
V_init stored as int16 BE in ``30 NN`` block payload; V/L/M
blocks encoded in order after Tran with ``30 NN`` separators;
V encoded as ``V - T`` differential. None match truth.
The codec is more elaborate than uniform 4-bit deltas. A hybrid
variable-bit-width scheme (4-bit deltas in ``10 NN``, 8-bit deltas
or absolutes in ``20 NN``, segment-header anchors after each
``40 02``) is the most plausible remaining hypothesis.
- **The role of byte [4:9] of the preamble.** Byte 4 == Tran[0]
truth value (in 16-count units) for events a/b/d, but doesn't
fit consistently for event-c. Bytes [5:9] don't match a simple
per-channel encoding.
- **``30 NN`` block length.** In the trailer, ``30 NN`` blocks
are NN×4 bytes long. In the data section, ``30 NN`` blocks are
NN×2 bytes long (= 8 bytes for NN=4 in SS0). The walker tries
NN×2 first and falls back to NN×4 if needed.
- **Walker correctness past offset ~427 in event-b.** The walker
bails out partway through event-b — there is at least one block
whose length doesn't fit the lengths confirmed for the other
three events. Likely a ``20 NN`` with NN > 0xFC (currently
rejected by the walker), or a different length formula in some
context.
events. This is a separate (now lower-priority) issue.
##### Recommended next step
@@ -1011,6 +1045,7 @@ output shape — keep the ``.h5`` sidecars marked as
| Date | Note |
|---|---|
| 2026-05-11 | Tran channel codec cracked using a high-amplitude (PPV 6-7 in/s) event bundle. Preamble[3:7] = Tran[0]/Tran[1] as int16 BE in 16-count units (LSB = 0.005 in/s). First data block (``10 NN`` nibble-deltas or ``20 NN`` int8-deltas) carries Tran deltas from sample 2. Verified 22+42+46 = 110 samples across SP0/SS0/SV0 with 0 errors. Earlier 96-combination brute-force search on the quiet 5-8 bundle failed because Tran[0] = Tran[1] = 0 in those events made initial-value-from-preamble undetectable. |
| 2026-05-08 | Block tagging confirmed against the 4-event May 2026 bundle. All bodies parse cleanly through `walk_body` for events a/c/d. Event-b walks partway and stops at offset 427 (open issue). |
| 2026-05-08 | Earlier "4-channel interleaved s16 LE" claim formally retracted — never validated, produced full-scale ±32K noise on every event because the bytes are encoded, not raw samples. |
| 2026-04-02 | "Frame 7 metadata", "Frame 9 terminator", and `0x0400`-step chunk-counter claims documented as-was; later proved to be artifacts of an over-reading 5A walk (now superseded by §7.8.57.8.7). |
+103 -9
View File
@@ -137,9 +137,17 @@ class WaveformBlock:
def find_data_start(body: bytes) -> int:
"""Auto-detect the offset of the first ``10 NN`` block."""
"""Auto-detect the offset of the first data block (``10 NN`` or ``20 NN``).
The preamble is always either 7 bytes (when sample 0 and 1 have small
values) or 9 bytes (when they don't, but only on continuous-mode events
in the small May-8 bundle). Returning the offset of the first ``10/20 NN``
tag is the most robust heuristic.
"""
for i in range(min(20, len(body) - 1)):
if body[i] == 0x10 and body[i + 1] % 4 == 0 and 0 < body[i + 1] <= 0xFC:
b = body[i]
nn = body[i + 1]
if b in (0x10, 0x20) and nn % 4 == 0 and 0 < nn <= 0xFC:
return i
return -1
@@ -167,7 +175,18 @@ def walk_body(body: bytes, start: Optional[int] = None) -> List[WaveformBlock]:
elif t0 == 0x00 and t1 % 4 == 0:
length = 2
elif t0 == 0x30 and t1 % 4 == 0 and 0 < t1 <= 0x10:
length = t1 * 4
# Data-section ``30 NN`` blocks have length NN*2 (= 8 for NN=4,
# confirmed in M529LL1A.SS0 at body offset 29). Trailer-section
# ``30 NN`` blocks have length NN*4 (= 32 for NN=8, confirmed in
# event-d trailer at body offset 3941). We pick NN*2 if it lands
# on a recognized tag, otherwise fall through to NN*4.
cand2 = t1 * 2
cand4 = t1 * 4
if (i + cand2 < len(body) - 1
and body[i + cand2] in (0x10, 0x20, 0x00, 0x30, 0x40)):
length = cand2
else:
length = cand4
elif t0 == 0x40 and t1 == 0x02:
length = 20
else:
@@ -227,16 +246,91 @@ def parse_segment_header(block: WaveformBlock) -> Optional[dict]:
}
def _s4(n: int) -> int:
"""Sign-extend a 4-bit value to signed int (0..7 → 0..7; 8..F → -8..-1)."""
return n if n < 8 else n - 16
def _i8(b: int) -> int:
"""Reinterpret an unsigned byte as signed int8."""
return b if b < 128 else b - 256
def decode_tran_initial(body: bytes) -> Optional[List[int]]:
"""
Decode the initial Tran-channel samples from the body VERIFIED 2026-05-11
against M529LL1A.SP0 / .SS0 / .SV0 (22 + 42 + 46 samples, 0 errors).
Returns a list of Tran sample values in **16-count units** (LSB = 0.005 in/s
at Normal range, the same quantization BW uses for its ASCII export).
Returns ``None`` if the body cannot be parsed.
The decoded list extends from sample 0 (= ``Tran[0]`` from preamble bytes
[3:5]) through the end of the FIRST data block. Subsequent samples
require decoding additional blocks that walk is not yet wired up here
because the multi-block channel-switching rule is still under
investigation (see waveform_codec module docstring).
Codec details (CONFIRMED 2026-05-11):
- Body bytes [0:3] are the magic ``00 02 00``.
- Body bytes [3:5] = ``Tran[0]`` as int16 BE in 16-count units.
- Body bytes [5:7] = ``Tran[1]`` as int16 BE in 16-count units.
- The first data block (``10 NN`` or ``20 NN``) carries Tran deltas
starting at sample 2:
* ``10 NN``: NN nibbles = NN/2 bytes; each nibble is a 4-bit signed
delta (0..7 0..+7; 8..F -8..-1). High nibble of each byte
comes first.
* ``20 NN``: NN int8 signed deltas (one delta per byte).
"""
if len(body) < 9:
return None
if body[0:3] != b"\x00\x02\x00":
return None
t0 = int.from_bytes(body[3:5], "big", signed=True)
t1 = int.from_bytes(body[5:7], "big", signed=True)
start = find_data_start(body)
if start < 0:
return None
blocks = walk_body(body, start)
if not blocks:
return [t0, t1]
first = blocks[0]
out = [t0, t1]
cur = t1
if first.tag_hi == 0x10:
for byte in first.data:
for nib in ((byte >> 4) & 0xF, byte & 0xF):
cur += _s4(nib)
out.append(cur)
elif first.tag_hi == 0x20:
for byte in first.data:
cur += _i8(byte)
out.append(cur)
else:
# First block is something else — fall back to just the preamble.
return out
return out
def decode_waveform_v2(body: bytes) -> Optional[dict]:
"""
Decode the body into per-channel sample arrays.
Returns a dict ``{"Tran": [...], "Vert": [...], "Long": [...], "MicL": [...]}``
when a verified decoder is wired up; returns ``None`` otherwise.
Returns ``None`` because the full multi-channel decoder is not yet
wired up. Tran is partially solved see :func:`decode_tran_initial`
for the initial portion (verified against ground-truth BW exports).
Currently returns ``None`` because the byte-to-sample mapping is OPEN.
The block framing in :func:`walk_body` is verified callers can use
that to inspect block-level structure without claiming the per-byte
interpretation.
Status (2026-05-11):
- Tran[0:N] correctly decoded by ``decode_tran_initial`` for the
first N samples of every fixture (where N = 22 / 42 / 46
depending on event).
- Subsequent Tran samples + all Vert / Long / MicL samples: open.
The block stream after the first data block likely interleaves
channels with ``30 NN`` channel-switch markers, but the exact
switching rule is still under investigation.
"""
return None
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
+64 -2
View File
@@ -14,11 +14,12 @@ import pytest
from minimateplus.waveform_codec import (
WaveformBlock,
decode_tran_initial,
decode_waveform_v2,
find_data_start,
parse_segment_header,
split_segments,
walk_body,
decode_waveform_v2,
)
@@ -238,7 +239,7 @@ def test_segment_counter_increments():
@pytest.mark.parametrize("event_name", list(FIXTURES_INFO.keys()))
def test_decode_waveform_v2_returns_none_until_verified(event_name):
"""
The verified per-byte sample decoder is not yet wired up.
The full per-channel decoder is not yet wired up.
This test ensures decode_waveform_v2 returns ``None`` so callers know
to keep using the legacy decoder. When a verified decoder lands,
@@ -250,3 +251,64 @@ def test_decode_waveform_v2_returns_none_until_verified(event_name):
pytest.skip(f"fixture missing: {path}")
body = _bw_body(path)
assert decode_waveform_v2(body) is None
# ── decode_tran_initial: confirmed correct against ground truth ──────────────
# Bundled fixtures for the high-amplitude 5-11-26 events (PPV ~6-7 in/s).
# These cracked the Tran codec — see waveform_codec module docstring.
TRAN_INITIAL_FIXTURES = [
# (path, expected first N Tran samples in 16-count units, # of samples to verify)
(
os.path.join(os.path.dirname(__file__), "fixtures", "5-11-26", "M529LL1A.SP0"),
[4, 4, 3, 3, 3, 2, 2, 3, 2, 2, 2, 2, 1, 1, 1, 2, 1, 1, 1, 0, 1, 0],
22,
),
(
os.path.join(os.path.dirname(__file__), "fixtures", "5-11-26", "M529LL1A.SS0"),
[-89, -89, -91, -91, -92, -93, -94, -94, -94, -94],
42,
),
(
os.path.join(os.path.dirname(__file__), "fixtures", "5-11-26", "M529LL1A.SV0"),
[-745, -762, -771, -774, -779, -794, -808, -811, -811, -819],
46,
),
]
@pytest.mark.parametrize("path,expected,n_required", TRAN_INITIAL_FIXTURES)
def test_decode_tran_initial_matches_ground_truth(path, expected, n_required):
"""The Tran initial decoder produces values matching the BW ASCII export exactly."""
if not os.path.exists(path):
pytest.skip(f"fixture missing: {path}")
with open(path, "rb") as f:
raw = f.read()
body = raw[43:-26]
decoded = decode_tran_initial(body)
assert decoded is not None
# Check first len(expected) samples match exactly.
for i in range(len(expected)):
assert decoded[i] == expected[i], (
f"sample {i}: decoded={decoded[i]} expected={expected[i]}"
)
# And we got at least n_required samples decoded.
assert len(decoded) >= n_required, (
f"decoded only {len(decoded)} samples, expected at least {n_required}"
)
def test_decode_tran_initial_handles_empty():
assert decode_tran_initial(b"") is None
assert decode_tran_initial(b"not a body") is None
def test_decode_tran_initial_synthetic_body():
"""A synthetic body with preamble + one 10 04 block decodes correctly."""
# Magic + T[0]=10 + T[1]=20 in 16-count units.
# Then 10 04 block with 4 nibbles: (+1, -1, +2, -2)
# Encoded high-nibble first: 0x1F = (1, -1), 0x2E = (2, -2)
body = b"\x00\x02\x00\x00\x0a\x00\x14" + b"\x10\x04" + b"\x1f\x2e"
decoded = decode_tran_initial(body)
# T[0]=10, T[1]=20, then deltas (+1, -1, +2, -2) from T[1]=20
assert decoded == [10, 20, 21, 20, 22, 20]