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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDXjZCr4RqT2U3QvMDhgzf
68 lines
2.9 KiB
Python
68 lines
2.9 KiB
Python
"""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"") == {}
|