From 685a17d180a077ce12c05355799eb2d0600f0f30 Mon Sep 17 00:00:00 2001 From: serversdown Date: Tue, 15 Sep 2026 20:01:39 +0000 Subject: [PATCH] feat(series4): decode sensor self-check waveforms from the IDFW binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Thor/Micromate (series-4) IDFW binary carries the sensor self-check in its fixed-header region (before the waveform body), as up to four records tagged 01 0e 3c/3d/3e/3f — the SAME channel ids as series-3 (Tran/Vert/Long/MicL). Unlike series-3's delta-coded trailing block, series-4 stores each trace as a raw int16-BE array after an 18-byte record header (2-byte sample count at offset +8). Three-channel (mic-disabled) units carry only 3c/3d/3e. New micromate/sensor_check.py: decode_idf_sensor_check(raw) locates the record chain (id-ordered marker run, so a stray body match can't chain) and reads each trace's int16 samples → {Tran,Vert,Long[,MicL]: [counts]}, or {} when absent. Reverse-engineered + validated against 4 UM oracle events (added as fixtures): clean geophone ring-downs on all, mic pulse trains on the 4-channel units, correctly no MicL on the two 3-channel units. Validated by shape + cross-event consistency (no Thor report strip to exact-match, unlike series-3's BW reports). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YDXjZCr4RqT2U3QvMDhgzf --- micromate/sensor_check.py | 89 ++++++++++++++++++ .../thor-idf-sc/UM11719_20231219162723.IDFW | Bin 0 -> 10290 bytes .../thor-idf-sc/UM12947_20250806134504.IDFW | Bin 0 -> 17530 bytes .../thor-idf-sc/UM13981_20220207084555.IDFW | Bin 0 -> 11478 bytes .../thor-idf-sc/UM20147_20250531135901.IDFW | Bin 0 -> 12462 bytes tests/test_sensor_check_idf.py | 67 +++++++++++++ 6 files changed, 156 insertions(+) create mode 100644 micromate/sensor_check.py create mode 100644 tests/fixtures/thor-idf-sc/UM11719_20231219162723.IDFW create mode 100644 tests/fixtures/thor-idf-sc/UM12947_20250806134504.IDFW create mode 100644 tests/fixtures/thor-idf-sc/UM13981_20220207084555.IDFW create mode 100644 tests/fixtures/thor-idf-sc/UM20147_20250531135901.IDFW create mode 100644 tests/test_sensor_check_idf.py diff --git a/micromate/sensor_check.py b/micromate/sensor_check.py new file mode 100644 index 0000000..dea67e6 --- /dev/null +++ b/micromate/sensor_check.py @@ -0,0 +1,89 @@ +r"""Decode the Thor / Micromate (series-4) sensor self-check waveforms from an +IDFW event binary. + +Reverse-engineered 2026-09-15 against 4 UM (Thor) oracle events. The IDFW +binary carries the sensor self-check in its fixed-header region (before the +waveform body), as up to four records tagged ``01 0e 3c/3d/3e/3f`` — the SAME +channel ids as the series-3 MiniMate Plus (Tran / Vert / Long / MicL), which is +the physical self-test: + + * 3c / 3d / 3e = Tran / Vert / Long geophone ring-downs (a damped impulse + response — resonant frequency + damping). + * 3f = MicL pulse train (the mic's known-signal gain check). Absent + on three-channel (mic-disabled) units. + +Record framing (per record):: + + 01 0e [id:1] [flags:3] [count:2 BE] [pad:10] [int16-BE samples × count] + \___ 18-byte header ___/ + +Unlike series-3's delta-coded trailing block, series-4 stores each trace as a +raw int16 big-endian array. ``count`` (the 2-byte field at header offset +8) +is the sample count; the record is padded to a fixed stride after that. +""" +from __future__ import annotations + +import struct +from typing import Dict, List + +# Record id → channel. Same ids/order as series-3 (minimateplus.sensor_check). +_ID_TO_CHANNEL = {0x3C: "Tran", 0x3D: "Vert", 0x3E: "Long", 0x3F: "MicL"} +_CHAIN_IDS = (0x3C, 0x3D, 0x3E, 0x3F) + +_MARKER = b"\x01\x0e" # precedes the 1-byte channel id +_HEADER_LEN = 18 # bytes from the marker start to the first sample +_COUNT_OFF = 8 # 2-byte BE sample count, from the marker start +_MAX_COUNT = 4000 # sanity cap (traces are ~70-200 samples) + + +def _find_chain(raw: bytes): + """Locate the sensor-check record chain. Returns a list of + ``(offset, id, count)`` for the first run of markers whose ids run + 3c, 3d, 3e[, 3f] in order, or ``[]``. + + Records are padded to a fixed stride, so the next marker is not at + ``header + count*2``; instead collect every ``01 0e [id]`` marker with a + sane count and take the first id-ordered run. Validating the id sequence + (not a lone ``01 0e 3c``) keeps a stray marker in the waveform body from + matching — the real chain sits in the fixed header, ahead of the body. + """ + n = len(raw) + markers = [] + for p in range(n - _HEADER_LEN): + if raw[p:p + 2] == _MARKER and raw[p + 2] in _ID_TO_CHANNEL: + count = int.from_bytes(raw[p + _COUNT_OFF:p + _COUNT_OFF + 2], "big") + if 0 < count <= _MAX_COUNT: + markers.append((p, raw[p + 2], count)) + + for i, (off, rid, _c) in enumerate(markers): + if rid != 0x3C: + continue + run = [markers[i]] + for m in markers[i + 1:]: + if len(run) < len(_CHAIN_IDS) and m[1] == _CHAIN_IDS[len(run)]: + run.append(m) + else: + break + if len(run) >= 3: # 3-channel (mic-disabled) units are valid + return run + return [] + + +def decode_idf_sensor_check(raw: bytes) -> Dict[str, List[int]]: + """Decode the sensor self-check traces from a Thor/Micromate IDFW binary. + + Returns ``{"Tran": [...], "Vert": [...], "Long": [...], "MicL": [...]}`` in + raw int16 ADC counts (MicL omitted on 3-channel units), or ``{}`` if the + binary carries no sensor-check chain (a non-IDF file, or an IDFH histogram). + """ + chain = _find_chain(raw) + if not chain: + return {} + out: Dict[str, List[int]] = {} + for off, rid, count in chain: + start = off + _HEADER_LEN + blob = raw[start:start + count * 2] + if len(blob) < count * 2: + continue + out[_ID_TO_CHANNEL[rid]] = list(struct.unpack(">%dh" % count, blob)) + return out diff --git a/tests/fixtures/thor-idf-sc/UM11719_20231219162723.IDFW b/tests/fixtures/thor-idf-sc/UM11719_20231219162723.IDFW new file mode 100644 index 0000000000000000000000000000000000000000..14620e8ebc0e39e151bc9cf7f40cb15292e36d70 GIT binary patch literal 10290 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/thor-idf-sc/UM12947_20250806134504.IDFW b/tests/fixtures/thor-idf-sc/UM12947_20250806134504.IDFW new file mode 100644 index 0000000000000000000000000000000000000000..a16b414dc6477d138921cdac47bb679f36de9b99 GIT binary patch literal 17530 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⁢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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/thor-idf-sc/UM20147_20250531135901.IDFW b/tests/fixtures/thor-idf-sc/UM20147_20250531135901.IDFW new file mode 100644 index 0000000000000000000000000000000000000000..476ddc905e429f5695c7e2b2185adf0e5ca76a78 GIT binary patch literal 12462 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 literal 0 HcmV?d00001 diff --git a/tests/test_sensor_check_idf.py b/tests/test_sensor_check_idf.py new file mode 100644 index 0000000..00074b2 --- /dev/null +++ b/tests/test_sensor_check_idf.py @@ -0,0 +1,67 @@ +"""Series-4 (Thor / Micromate IDFW) sensor self-check waveform decode. + +Reverse-engineered 2026-09-15 against 4 UM (Thor) oracle events. The IDFW +binary carries the sensor self-check in its fixed-header region (before the +waveform body) as up to four records tagged ``01 0e 3c/3d/3e/3f`` — the SAME +channel ids as series-3 (Tran/Vert/Long/MicL). Unlike series-3's delta-coded +trailing block, series-4 stores each trace as a raw int16-BE array after an +18-byte record header whose sample count is a 2-byte field at offset +8. + +Three-channel (mic-disabled) Thor units carry only 3c/3d/3e — no MicL record. + +Validated by shape (geophone ring-down / mic pulse train) and cross-event +consistency, since there's no Thor Event-Report strip to exact-match against. +""" +from pathlib import Path + +import numpy as np + +from micromate.sensor_check import decode_idf_sensor_check + +FIXDIR = Path(__file__).parent / "fixtures" / "thor-idf-sc" +EVENTS = sorted(p.name for p in FIXDIR.glob("*.IDFW")) + + +def _decode(name): + return decode_idf_sensor_check((FIXDIR / name).read_bytes()) + + +def test_geo_channels_present_and_ringdown_shaped(): + # Every IDFW event has the three geophone self-checks; each is a large + # one-sided deflection (~15000 raw counts) that rings back — the geophone's + # damped impulse response. + for name in EVENTS: + sc = _decode(name) + for ch in ("Tran", "Vert", "Long"): + assert ch in sc, f"{name} missing {ch}" + tr = np.asarray(sc[ch], dtype=float) + tr = tr - tr[:4].mean() # reference to the pre-trigger baseline + assert 40 <= len(tr) <= 300, f"{name}:{ch} n={len(tr)}" + assert tr.min() < -8000, f"{name}:{ch} min {tr.min()}" + # deflects one way and rings back toward / past the baseline + assert tr.max() < abs(tr.min()), f"{name}:{ch} not one-sided" + + +def test_mic_present_only_on_four_channel_units(): + # UM11719 / UM12947 record a mic; UM13981 / UM20147 are 3-channel + # (mic-disabled) units and carry no MicL self-check. + got = {name: ("MicL" in _decode(name)) for name in EVENTS} + assert any(got.values()), "expected at least one 4-channel unit" + assert not all(got.values()), "expected at least one 3-channel unit" + for name, has_mic in got.items(): + if has_mic: + tr = np.asarray(_decode(name)["MicL"], dtype=float) + tr = tr - tr[:4].mean() + # mic self-check is a bipolar pulse train — swings both ways, wide range + assert tr.max() > 5000 and tr.min() < -5000, f"{name} mic not bipolar" + + +def test_channel_ids_and_order(): + # ids decode to the canonical channel names, geo always in Tran/Vert/Long order + sc = _decode(EVENTS[0]) + assert [c for c in ("Tran", "Vert", "Long") if c in sc] == ["Tran", "Vert", "Long"] + + +def test_returns_empty_on_non_idf_input(): + assert decode_idf_sensor_check(b"not an IDF file") == {} + assert decode_idf_sensor_check(b"") == {}