feat(offset): DC-offset detector productionized into the shape pipeline
Productionizes the validated scratch/offset_scan3.py: a DC offset (baseline shifted off zero — sensor bumped/settled/drifted) is |median(pre-trigger)| >= 5 counts (0.025 in/s) AND flat across pre/mid/end thirds (spread <= 0.02); a transient moves one third and is rejected by the spread test. - shape_metrics: offset_from_samples / offset_from_h5 (reads .h5 samples + pretrig_samples attr; range-aware via the .h5's in/s float samples) - events schema: shape_offset / _axis / _pre / _spread (via _SCHEMA + the _migrate ADD COLUMN loop only; NOT the Migration-1 rebuild), threaded through insert + upsert mirroring shape_* - ingest: computed at all three waveform_store save paths alongside shape - backfill_event_shape: also computes + stores (and stale-clears) offset - exposed via /db/events automatically (SELECT *) Gating to waveforms is done downstream in terra-view ft_suspicion (mirrors how shape is ignored for histograms), not at the SFM call sites. 13 new tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDXjZCr4RqT2U3QvMDhgzf
This commit is contained in:
@@ -47,6 +47,60 @@ def shape_from_samples(chans: dict) -> dict | None:
|
||||
return s
|
||||
|
||||
|
||||
# ── Offset (DC-baseline) detection ────────────────────────────────────────────
|
||||
# A DC offset is a false trigger where the geophone baseline sits at a constant
|
||||
# non-zero floor (sensor bumped / settled / drifted) instead of oscillating
|
||||
# around zero. Brian's method (validated in scratch/offset_scan3.py): the
|
||||
# pre-trigger window is definitionally quiet, so a true offset shows |pre| off
|
||||
# zero AND stays flat across the record (pre ≈ mid ≈ end). A transient moves one
|
||||
# third relative to the others and is rejected by the spread test.
|
||||
# Thresholds are in in/s (the .h5 samples are already range-scaled); validated at
|
||||
# Normal range (10 in/s) — the only range in the fleet.
|
||||
OFFSET_FLOOR = 0.025 # |pre| at/above this reads as an off-zero baseline (5 A/D counts)
|
||||
OFFSET_MAX_SPREAD = 0.02 # max(pre,mid,end) - min(...) at/below this reads as flat/constant
|
||||
|
||||
|
||||
def _channel_offset(x, pretrig_n):
|
||||
"""Return (pre, spread, is_offset) for one channel, or None if unusable."""
|
||||
x = np.asarray(x, dtype=float)
|
||||
n = x.size
|
||||
if n < 3:
|
||||
return None
|
||||
t = n // 3
|
||||
pre = x[:pretrig_n] if (pretrig_n and 0 < pretrig_n < n) else x[:t]
|
||||
mid, end = x[t:2 * t], x[2 * t:]
|
||||
if pre.size == 0 or mid.size == 0 or end.size == 0:
|
||||
return None
|
||||
vals = [float(np.median(seg)) for seg in (pre, mid, end)]
|
||||
spread = max(vals) - min(vals)
|
||||
is_offset = abs(vals[0]) >= OFFSET_FLOOR and spread <= OFFSET_MAX_SPREAD
|
||||
return vals[0], spread, is_offset
|
||||
|
||||
|
||||
def offset_from_samples(chans: dict, pretrig_n) -> dict | None:
|
||||
"""Detect a DC-offset false trigger across the geophone channels.
|
||||
|
||||
An event is offset if ANY geo channel's pre-trigger baseline is off zero and
|
||||
flat across the record. Reports the tripping axis (or, if none trips, the
|
||||
most-offset-like axis) with its ``pre``/``spread`` for transparency + tuning.
|
||||
Returns None when no geo channel is usable.
|
||||
"""
|
||||
results = []
|
||||
for ax in _GEO_CHANNELS:
|
||||
x = chans.get(ax)
|
||||
if x is None:
|
||||
continue
|
||||
r = _channel_offset(x, pretrig_n)
|
||||
if r is not None:
|
||||
results.append((ax, r[0], r[1], r[2]))
|
||||
if not results:
|
||||
return None
|
||||
offenders = [r for r in results if r[3]]
|
||||
ax, pre, spread, _ = max(offenders or results, key=lambda r: abs(r[1]))
|
||||
return {"offset": bool(offenders), "axis": ax,
|
||||
"pre": round(pre, 6), "spread": round(spread, 6)}
|
||||
|
||||
|
||||
def shape_from_h5(path) -> dict | None:
|
||||
import h5py
|
||||
try:
|
||||
@@ -56,3 +110,18 @@ def shape_from_h5(path) -> dict | None:
|
||||
except Exception:
|
||||
return None
|
||||
return shape_from_samples(chans)
|
||||
|
||||
|
||||
def offset_from_h5(path) -> dict | None:
|
||||
"""offset_from_samples fed from an event's .h5 (float32 in/s geo samples +
|
||||
the pretrig_samples attribute)."""
|
||||
import h5py
|
||||
try:
|
||||
with h5py.File(path, "r") as f:
|
||||
chans = {ax: f[f"samples/{ax}"][:] for ax in _GEO_CHANNELS
|
||||
if f"samples/{ax}" in f}
|
||||
pretrig_n = f.attrs.get("pretrig_samples")
|
||||
except Exception:
|
||||
return None
|
||||
pretrig_n = int(pretrig_n) if pretrig_n is not None else 0
|
||||
return offset_from_samples(chans, pretrig_n)
|
||||
|
||||
Reference in New Issue
Block a user