merge full s3 codec decoded #23

Merged
serversdown merged 18 commits from codec-re into main 2026-05-20 13:45:33 -04:00
14 changed files with 10113 additions and 50 deletions
Showing only changes of commit 6ac126e05c - Show all commits
+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 > **Status (2026-05-11):** Block-level framing is solved. The Tran-channel
> against the 4-event May 8 2026 bundle (3 sec / 2 sec / 1 sec / 1 sec > encoding (preamble + first data block) is **fully verified** against the
> events captured live from BE11529). The per-byte mapping from block > 3-event May 11 2026 high-amplitude bundle (PPV 6-7 in/s) and the 4-event
> data to ADC samples is **still open** — the previous int16 LE claim > May 8 bundle. Verts / Long / MicL channel encodings and multi-block
> is REFUTED (see history below). > 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" > 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 > 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] [trailer: per-channel summary blocks]
``` ```
**Preamble:** starts with the 4-byte magic ``00 02 00 00``. Single-shot **Preamble (CONFIRMED 2026-05-11 across 3+4 events):**
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 body[0:3] = 00 02 00 magic
per-channel state but the layout has not been pinned down — for some body[3:5] = Tran[0] int16 BE first Tran sample (LSB = 0.005 in/s)
events byte [4] equals truth Tran[0] in 16-count units (0.005 in/s body[5:7] = Tran[1] int16 BE second Tran sample
LSB), but other channel-byte assignments don't fit consistently. ```
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) ##### 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 int8 channel-bias values around -12). Detailed decoding of the
trailer is outside the path needed for sample reconstruction. 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 ##### What's still open
- **The byte → sample mapping inside ``10 NN`` and ``20 NN`` blocks.** - **Tran past the first data block.** After the first block, the
Tested hypotheses that did not match BW's ASCII export to within ±1 body has more ``10 NN`` / ``20 NN`` blocks separated by ``00 NN``
ADC count: 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, - **Vert / Long / MicL channel encodings.** No verified decoder
all 24 channel permutations × 2 nibble orders × 2 sign conventions exists for these yet. Hypotheses tested without success:
× 2 init-from-header settings (= 96 combinations). All produce V_init stored as int16 BE in ``30 NN`` block payload; V/L/M
values that diverge from truth after the first ~7 sample-sets. blocks encoded in order after Tran with ``30 NN`` separators;
2. ``20 NN`` data = int8 absolute or delta samples for one channel. V encoded as ``V - T`` differential. None match truth.
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.
The codec is more elaborate than uniform 4-bit deltas. A hybrid - **``30 NN`` block length.** In the trailer, ``30 NN`` blocks
variable-bit-width scheme (4-bit deltas in ``10 NN``, 8-bit deltas are NN×4 bytes long. In the data section, ``30 NN`` blocks are
or absolutes in ``20 NN``, segment-header anchors after each NN×2 bytes long (= 8 bytes for NN=4 in SS0). The walker tries
``40 02``) is the most plausible remaining hypothesis. NN×2 first and falls back to NN×4 if needed.
- **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.
- **Walker correctness past offset ~427 in event-b.** The walker - **Walker correctness past offset ~427 in event-b.** The walker
bails out partway through event-b — there is at least one block bails out partway through event-b — there is at least one block
whose length doesn't fit the lengths confirmed for the other whose length doesn't fit the lengths confirmed for the other
three events. Likely a ``20 NN`` with NN > 0xFC (currently events. This is a separate (now lower-priority) issue.
rejected by the walker), or a different length formula in some
context.
##### Recommended next step ##### Recommended next step
@@ -1011,6 +1045,7 @@ output shape — keep the ``.h5`` sidecars marked as
| Date | Note | | 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 | 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-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). | | 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: 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)): 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 i
return -1 return -1
@@ -167,7 +175,18 @@ def walk_body(body: bytes, start: Optional[int] = None) -> List[WaveformBlock]:
elif t0 == 0x00 and t1 % 4 == 0: elif t0 == 0x00 and t1 % 4 == 0:
length = 2 length = 2
elif t0 == 0x30 and t1 % 4 == 0 and 0 < t1 <= 0x10: 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: elif t0 == 0x40 and t1 == 0x02:
length = 20 length = 20
else: 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]: def decode_waveform_v2(body: bytes) -> Optional[dict]:
""" """
Decode the body into per-channel sample arrays. Decode the body into per-channel sample arrays.
Returns a dict ``{"Tran": [...], "Vert": [...], "Long": [...], "MicL": [...]}`` Returns ``None`` because the full multi-channel decoder is not yet
when a verified decoder is wired up; returns ``None`` otherwise. 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. Status (2026-05-11):
The block framing in :func:`walk_body` is verified — callers can use - Tran[0:N] correctly decoded by ``decode_tran_initial`` for the
that to inspect block-level structure without claiming the per-byte first N samples of every fixture (where N = 22 / 42 / 46
interpretation. 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 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 ( from minimateplus.waveform_codec import (
WaveformBlock, WaveformBlock,
decode_tran_initial,
decode_waveform_v2,
find_data_start, find_data_start,
parse_segment_header, parse_segment_header,
split_segments, split_segments,
walk_body, walk_body,
decode_waveform_v2,
) )
@@ -238,7 +239,7 @@ def test_segment_counter_increments():
@pytest.mark.parametrize("event_name", list(FIXTURES_INFO.keys())) @pytest.mark.parametrize("event_name", list(FIXTURES_INFO.keys()))
def test_decode_waveform_v2_returns_none_until_verified(event_name): 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 This test ensures decode_waveform_v2 returns ``None`` so callers know
to keep using the legacy decoder. When a verified decoder lands, 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}") pytest.skip(f"fixture missing: {path}")
body = _bw_body(path) body = _bw_body(path)
assert decode_waveform_v2(body) is None 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]