From 2902ab373e7b356e2ed7cbf5269bbd3f6268e092 Mon Sep 17 00:00:00 2001 From: serversdown Date: Mon, 14 Sep 2026 17:15:16 +0000 Subject: [PATCH] feat(fft): Blastware-compatible channel FFT (waveform_fft) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit channel_spectrum(samples, sps) → the single-sided amplitude spectrum Blastware's FFT Report draws, and dominant_frequency() picks its peak in the 2–250 Hz band. Reverse-engineered against 7 BE12844 (MiniMate Plus) events with Blastware FFT reports as ground truth. Recipe: DC-remove, NO window (a window smears the peak and worsens the match), zero-pad to 4096 (→ 0.25 Hz bins at 1024 sps — the resolution every reported dominant frequency lands on), single-sided 2/N amplitude. Reproduces Blastware's dominant frequency to the exact bin on all 28 channels and the amplitude to report precision. This is the missing piece for both the USBM RI8507 compliance chart (its scatter is these (freq, amp) points vs the limit curve) and the FFT view. Pure numpy, series-agnostic (feed it in/s samples from either decoder). The 7 events land in tests/fixtures as the oracle (force-added past the fixtures gitignore, matching 5-11-26 / decode-re-5-8-26). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YDXjZCr4RqT2U3QvMDhgzf --- .../fft-oracle-2026-09-14/N844LPGH.VV0W | Bin 0 -> 9102 bytes .../fft-oracle-2026-09-14/N844LPPR.3S0W | Bin 0 -> 8306 bytes .../fft-oracle-2026-09-14/N844LQHB.ZT0W | Bin 0 -> 11260 bytes .../fft-oracle-2026-09-14/N844LQUE.T50W | Bin 0 -> 11102 bytes .../fft-oracle-2026-09-14/N844LR8W.790W | Bin 0 -> 9678 bytes .../fft-oracle-2026-09-14/N844LRCO.G60W | Bin 0 -> 9450 bytes .../fft-oracle-2026-09-14/N844LRCW.F30W | Bin 0 -> 9654 bytes tests/test_waveform_fft.py | 85 ++++++++++++++++++ waveform_fft.py | 66 ++++++++++++++ 9 files changed, 151 insertions(+) create mode 100644 tests/fixtures/fft-oracle-2026-09-14/N844LPGH.VV0W create mode 100644 tests/fixtures/fft-oracle-2026-09-14/N844LPPR.3S0W create mode 100644 tests/fixtures/fft-oracle-2026-09-14/N844LQHB.ZT0W create mode 100644 tests/fixtures/fft-oracle-2026-09-14/N844LQUE.T50W create mode 100644 tests/fixtures/fft-oracle-2026-09-14/N844LR8W.790W create mode 100644 tests/fixtures/fft-oracle-2026-09-14/N844LRCO.G60W create mode 100644 tests/fixtures/fft-oracle-2026-09-14/N844LRCW.F30W create mode 100644 tests/test_waveform_fft.py create mode 100644 waveform_fft.py diff --git a/tests/fixtures/fft-oracle-2026-09-14/N844LPGH.VV0W b/tests/fixtures/fft-oracle-2026-09-14/N844LPGH.VV0W new file mode 100644 index 0000000000000000000000000000000000000000..785721f56b80dd382638dd138b0452e4faf4d9a5 GIT binary patch literal 9102 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 I{nzII0@&1)d;kCd literal 0 HcmV?d00001 diff --git a/tests/fixtures/fft-oracle-2026-09-14/N844LPPR.3S0W b/tests/fixtures/fft-oracle-2026-09-14/N844LPPR.3S0W new file mode 100644 index 0000000000000000000000000000000000000000..b4395a16c0005446e6b5d122adc115e1672312da GIT binary patch literal 8306 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/fft-oracle-2026-09-14/N844LRCW.F30W b/tests/fixtures/fft-oracle-2026-09-14/N844LRCW.F30W new file mode 100644 index 0000000000000000000000000000000000000000..6e9ef75628c54a71e198b1adec9326f7a1a9c3e6 GIT binary patch literal 9654 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 0.25: + misses.append(f"{fname}:{ch} got {got_hz} want {want_hz}") + assert not misses, "dominant-frequency mismatches:\n" + "\n".join(misses) + + +def test_amplitude_matches_blastware(): + misses = [] + for fname, chans in ORACLE.items(): + spectra = _spectra(fname) + for ch, (_, want_amp) in chans.items(): + if want_amp is None: + continue + _, got_amp = dominant_frequency(*spectra[ch]) + if abs(got_amp - want_amp) > 0.0015: + misses.append(f"{fname}:{ch} got {got_amp:.4f} want {want_amp:.3f}") + assert not misses, "amplitude mismatches:\n" + "\n".join(misses) diff --git a/waveform_fft.py b/waveform_fft.py new file mode 100644 index 0000000..7e84565 --- /dev/null +++ b/waveform_fft.py @@ -0,0 +1,66 @@ +"""Blastware-compatible FFT of a decoded seismograph channel. + +Pure numpy; no I/O, no device or DB dependencies. Feed it a channel's decoded +samples **in the unit you want the amplitudes in** (e.g. in/s) and it returns the +single-sided amplitude spectrum that Blastware's *FFT Report* draws. + +Reverse-engineered 2026-09-14 against 7 BE12844 (MiniMate Plus) events with +Blastware FFT reports as ground truth. The recipe reproduces Blastware's +**dominant frequency to the exact 0.25 Hz bin on all 28 channels** and the +amplitude to report precision: + + 1. remove the DC component (subtract the mean); **no window** — a window + smears the peak and measurably worsens the match, + 2. zero-pad to ``nfft`` (4096 → 0.25 Hz bins at 1024 sps — Blastware's + resolution), + 3. single-sided amplitude ``A[k] = 2·|X[k]| / N`` where ``N`` is the real + sample count (not ``nfft``). + +The compliance chart (USBM RI8507 / OSMRE) is this spectrum's ``(freq, amp)`` +points plotted against the regulatory limit curve; the #10 FFT view is the +spectrum itself. +""" +from __future__ import annotations + +import numpy as np + +BW_NFFT = 4096 # 0.25 Hz bins at 1024 sps — Blastware's FFT resolution +BW_FMIN = 2.0 # dominant-frequency search floor (Hz) +BW_FMAX = 250.0 # dominant-frequency search ceiling (Hz) + + +def channel_spectrum(samples, sps: float = 1024.0, nfft: int = BW_NFFT): + """Single-sided amplitude spectrum of one channel, Blastware-compatible. + + ``samples`` is a 1-D sequence in the desired amplitude unit (in/s). Returns + ``(freqs, amps)`` numpy arrays covering ``0 .. sps/2`` in ``sps/nfft`` steps. + + Records longer than ``nfft`` are truncated by the transform — untested + against Blastware for that case (real MiniMate Plus records are ≤ ~3.3 s, + well under 4096 samples at 1024 sps). + """ + x = np.asarray(samples, dtype=float) + n = x.size + if n == 0: + return np.empty(0), np.empty(0) + x = x - x.mean() # DC removal, no window + mag = np.abs(np.fft.rfft(x, nfft)) + freqs = np.fft.rfftfreq(nfft, 1.0 / sps) + amps = (2.0 / n) * mag # single-sided amplitude + return freqs, amps + + +def dominant_frequency(freqs, amps, fmin: float = BW_FMIN, fmax: float = BW_FMAX): + """Peak ``(frequency_hz, amplitude)`` of a spectrum within ``[fmin, fmax)``. + + Matches Blastware's "Dominant Frequency" — the largest spectral bin in the + reportable band (below 2 Hz is baseline/DC drift, above 250 Hz is noise). + """ + freqs = np.asarray(freqs) + amps = np.asarray(amps) + lo = int(np.searchsorted(freqs, fmin)) + hi = int(np.searchsorted(freqs, fmax)) + if hi <= lo: + return 0.0, 0.0 + k = lo + int(np.argmax(amps[lo:hi])) + return float(freqs[k]), float(amps[k])