fix(series4): support mic-disabled (3-channel) Thor units
Verified against a second Thor corpus (9-10-26-csv-req: UM11402, UM12947,
UM20147) with per-sample CSV exports: 139/139 waveforms exact
(1,273,380/1,273,380 samples) and 877/877 histograms within 2% of Thor's
reported PPV -- up from 66.9% and 56.6%.
Some units run with the microphone disabled, which changes two structural
things that were both hardcoded to the 4-channel shape:
- Waveform body head sat below the scan floor. A 3-channel unit has a
shorter fixed header and puts its record chain head at 0x0dba, under the
old _BODY_SCAN_FLOOR of 0x0E00. The scan could not see it and fell
through to the Vert segment-0 record, decoding a body shifted one
position around the channel rotation -- Vert came up exactly 512 samples
short. Floor lowered to 0x0C00. The body-offset scoring also had to stop
requiring four channels, or `equal` is permanently False for these events
and the pick falls back to raw sample count.
- Histogram interval record is 56 bytes, not 72. It is
16 * n_channels + 8, and is not inferable from the segment length alone.
The interval count now comes from the segment's cumulative counter
(n = counter - prev_counter) and the stride is derived from it. Assuming
72 read 7 intervals out of every 10-interval segment, then walked off
alignment into garbage that decoded as ~10 in/s peaks -- inflating some
files' PPV by up to 191,000%. Also recovers 4 files that previously
decoded no intervals at all.
Combined across both corpora: 292/292 waveform files,
2,330,916/2,330,916 samples exact. Production IDFW truncations 41 -> 22.
Series-3 unaffected (no shared-codec change in this commit; last full run
14,338/14,338).
Known open, diagnosed but NOT verified: the remaining 22 unequal + 1 failing
production IDFW files (all UM12947, 2025-07-14..09-23) stop the block walker
on tag 40 0c. data_block_len() caps the 40 NN int16 block at NN > 0x08 while
those files use NN up to 196. Both verified corpora only ever use
NN in {1,2,3,4,8}, so the cap is untested there and lifting it leaves both at
100.000% -- which is not evidence it decodes these correctly. Deliberately
not shipped; needs Thor CSV exports for UM12947 in that date range.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
This commit is contained in:
@@ -235,3 +235,59 @@ def test_body_offset_search_is_not_quadratic():
|
||||
read_idf_file(IDFW_RAW16)
|
||||
elapsed = (time.perf_counter() - start) / 3
|
||||
assert elapsed < 0.15, f"body-offset search took {elapsed*1000:.0f} ms/file"
|
||||
|
||||
|
||||
# ─── Mic-disabled (3-channel) units, found 2026-09-10 ──────────────────────
|
||||
|
||||
IDFW_3CH = FIXTURES / "UM20147_20250531135901.IDFW" # body head below old floor
|
||||
IDFH_3CH = FIXTURES / "UM20147_20250330070110.IDFH" # 56-byte interval records
|
||||
|
||||
|
||||
def test_three_channel_waveform_decodes_all_geo_channels():
|
||||
"""A mic-disabled unit's shorter header moves the record chain head.
|
||||
|
||||
Its head sits at 0x0dba, below the old ``_BODY_SCAN_FLOOR`` of 0x0E00, so
|
||||
the scan could not see it and fell through to the *Vert* segment-0 record
|
||||
— decoding a body shifted one position around the channel rotation, which
|
||||
surfaced as Vert being exactly 512 samples short.
|
||||
"""
|
||||
rows = _parse_export(IDFW_3CH.with_suffix(".IDFW.csv"))[1]
|
||||
result = read_idf_file(IDFW_3CH)
|
||||
for index, channel in enumerate(GEO_CHANNELS):
|
||||
decoded = result.samples[channel]
|
||||
assert len(decoded) == len(rows), (
|
||||
f"{channel}: {len(decoded)} samples, export has {len(rows)}"
|
||||
)
|
||||
expected = [row[index] for row in rows]
|
||||
bad = sum(
|
||||
1 for c, v in zip(decoded, expected)
|
||||
if abs(geo_count_to_ips(c) - v) >= 5e-5
|
||||
)
|
||||
assert bad == 0, f"{channel}: {bad} samples differ from Thor's export"
|
||||
# Mic is genuinely absent on these units, not merely undecoded.
|
||||
assert not result.samples.get("MicL")
|
||||
|
||||
|
||||
def test_three_channel_histogram_uses_56_byte_intervals():
|
||||
"""Interval stride is 16 bytes per channel + an 8-byte tail, not a constant.
|
||||
|
||||
A mic-disabled unit packs 56-byte records, so assuming 72 read 7 intervals
|
||||
out of every 10-interval segment and then walked off alignment into
|
||||
garbage, which decoded as ~10 in/s peaks. The true count comes from the
|
||||
segment's cumulative interval counter.
|
||||
"""
|
||||
header, _rows = _parse_export(IDFH_3CH.with_suffix(".IDFH.csv"))
|
||||
result = read_idf_file(IDFH_3CH)
|
||||
expected_intervals = float(header["NumberOfIntervals"])
|
||||
assert len(result.intervals) == pytest.approx(expected_intervals, abs=1.0)
|
||||
assert {iv.n_channels for iv in result.intervals} == {3}
|
||||
for channel, attr in (
|
||||
("Tran", "transverse_ips"),
|
||||
("Vert", "vertical_ips"),
|
||||
("Long", "longitudinal_ips"),
|
||||
):
|
||||
decoded = getattr(result.event.peaks, attr)
|
||||
assert decoded < 1.0, f"{channel} peak {decoded} looks like walked-off garbage"
|
||||
assert decoded == pytest.approx(
|
||||
_header_float(header, f"{channel}PPV"), rel=0.02
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user