feat(offset): the non-motion test, and BE12599 diagnosed as a connector
Brian noticed BE12599's 2026-08-09 event reports no ZC frequency because the trace never crosses zero. That is the best detector in this investigation. A geophone has no DC response, so its output must integrate to ~zero over a record. |mean|/peak is therefore ~0 for real motion and ~1 for anything electrical. Across 12,068 channel-events with peak >= 0.05 in/s the statistic is bimodal with a 1.09% dead zone, and at mp >= 0.8 it returns exactly the five confirmed units -- from physics rather than a tuned threshold. Two detectors on different principles agreeing is the strongest corroboration the list has had. It also settles BE11007 as NOT an offset: mp 0.75-0.89 but frac_neg 0.99 at peaks of 7.4-9.4 in/s, i.e. a one-sided near-full-scale blast. Journal 8e diagnoses BE12599 specifically. Its August waveforms are unipolar impulses with an RC tail (26 ms -> 118 ms -> never recovers over 14 days), and the fault MOVES between Long and Tran while the sensor self-check passes on every event. A failing element cannot hop channels; a connector can -- which also explains why the swing test never fails and why an autozero rarely helps. Corrects 8c's claim that the spread gate is blind to onsets: of 87 BE18438|Vert events it rejected one, the transitional record. Narrower than stated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
This commit is contained in:
@@ -847,6 +847,93 @@ geophone *replacement*" is component-level-negative in one observation.
|
||||
|
||||
---
|
||||
|
||||
### 8d. The non-motion test — Brian's "it doesn't cross zero" (2026-09-07)
|
||||
|
||||
Looking at BE12599's 2026-08-09 event, Brian noted it reports no ZC frequency
|
||||
**because the trace never crosses zero**. That observation is the best detector
|
||||
in this investigation, and it comes from physics rather than a threshold.
|
||||
|
||||
A geophone is a velocity sensor with no DC response, so its output over a record
|
||||
must integrate to ~zero — the ground does not relocate. Real motion therefore
|
||||
sits roughly half below zero. Anything electrical is one-sided.
|
||||
|
||||
mp = |mean| / peak ~0 for motion, ~1 for a fault
|
||||
frac_neg = share of samples < 0
|
||||
|
||||
`scratch/nonmotion_scan.py`, all 6,577 waveforms, 19,731 channel-rows.
|
||||
Restricted to peak >= 0.05 in/s (n = 12,068), the distribution is **bimodal
|
||||
with an empty middle**:
|
||||
|
||||
| mp band | channel-events |
|
||||
|---|---|
|
||||
| 0.0–0.1 | 11,384 |
|
||||
| 0.1–0.2 | 293 |
|
||||
| **0.15–0.85 (dead zone)** | **131 = 1.09%** |
|
||||
| 0.9–1.0 | 278 |
|
||||
|
||||
At `mp >= 0.8` with >=3 events it returns **exactly the five confirmed units** —
|
||||
BE9558, BE11529, BE12599, BE13117, BE18438 — stable from 0.5 to 0.9. Two
|
||||
detectors on entirely different principles agreeing on the unit list is the
|
||||
strongest corroboration that list has.
|
||||
|
||||
**BE11007 is settled: NOT an offset.** It reaches mp 0.75–0.89, but with
|
||||
`frac_neg = 0.99` at peaks of **7.4–9.4 in/s** — parked *negative* during a
|
||||
near-full-scale blast. §4's guess was right. `mp` alone cannot separate a
|
||||
pedestal from a large one-sided blast; pair it with a peak ceiling or with
|
||||
sign-consistency across events.
|
||||
|
||||
⚠ **Not a rediscovery of the retracted v1 detector.** v1 scored only the
|
||||
largest-peak axis and used the mean as a *baseline estimator* where the median
|
||||
was required. Here the mean is the signal itself, per channel — that is what the
|
||||
physics licenses.
|
||||
|
||||
**Correction to §8c.** That section says the spread gate is "blind to onsets by
|
||||
construction." Too strong: of 87 BE18438|Vert events at mp >= 0.5 the gate
|
||||
rejected **one** — the transitional record. It does not lose onsets
|
||||
systematically; it loses the transition specifically.
|
||||
|
||||
### 8e. BE12599 — a connector, not a geophone (2026-09-07)
|
||||
|
||||
Waveform shapes across its August episode, measured rather than eyeballed:
|
||||
|
||||
| date | channel | shape |
|
||||
|---|---|---|
|
||||
| Aug 09 05:29 | Long | **unipolar +**, 0/2304 samples below zero, decay tau **26 ms** |
|
||||
| Aug 09 05:35 | Long | unipolar +, 3 spikes at irregular gaps (744, 1032 ms), tau **38 ms** |
|
||||
| Aug 14 05:00 | Long | single lobe, bipolar, tau **118 ms** |
|
||||
| Aug 17–23 | Tran | **flat DC pedestal**, sd/level 0.015–0.020, 0 zero crossings |
|
||||
|
||||
**Unipolar impulses with an RC tail are not mechanical.** Fast rise, exponential
|
||||
decay, one polarity, irregular timing — that is charge dumped into a
|
||||
capacitively-coupled input and draining through the input resistance. The
|
||||
progression 26 ms -> 118 ms -> never recovers, over 14 days, is a leakage path
|
||||
worsening.
|
||||
|
||||
**And the fault moved channels** — Long on Aug 9/14, Tran on Aug 17–23, Long
|
||||
again on Aug 21 (1.065 in/s) while Tran held its pedestal. Vert stayed clean
|
||||
throughout. **A failing geophone element cannot hop channels. A connector can.**
|
||||
|
||||
That single fact explains what had been puzzling:
|
||||
- **The sensor self-check keeps passing** (7.4/7.5/7.6 Hz, ratios 3.6–4.2, all
|
||||
four channels Passed, on the very events where Long throws 0.5 in/s spikes).
|
||||
The swing test drives the element; the element is fine. The fault is in the
|
||||
wiring to it.
|
||||
- **Why Instantel's autozero fixes only ~10%** — it cannot fix a connector.
|
||||
- **Why onset "ramps" over minutes to hours** — contact resistance drifting.
|
||||
|
||||
All seven Aug 17–23 events are stamped **05:00:14**, the same second, and their
|
||||
filename extensions run `8E → WE → KE → 8E → WE → KE → 8E` — the documented
|
||||
3-day cycle for a fixed daily time. Clock-scheduled, not physically triggered:
|
||||
the modem powers up, draws a surge, and a marginal connection responds.
|
||||
|
||||
**Field action: inspect and photograph the geophone connector BEFORE reseating
|
||||
anything** — an intermittent contact clears the moment it is disturbed.
|
||||
|
||||
⚠ Scoped to BE12599. BE18438's onset was a smooth 7-minute ramp with no spikes,
|
||||
which looks like a different failure mode wearing the same signature.
|
||||
|
||||
---
|
||||
|
||||
### ⚠ Serial prefixes — four of these units are BlastMates, not MiniMates
|
||||
|
||||
Corrected 2026-09-06, after Brian queried "BA10895?" against a report that
|
||||
@@ -896,3 +983,5 @@ decoder lost the geo threshold along with the serial. See commit `9ceff65`.
|
||||
| 2026-09-06 | **Onset is a RAMP of minutes-to-hours, not a step** — BE18438 Vert resolved to one-minute cadence, 50% of the excursion in 7 min, >=25 intermediates, ASCII-validated 75/75. Kills both a latched digital step AND slow component degradation. Surviving shape: a reversible two-time-constant settling process — a bias/leakage path charging a high-impedance node. |
|
||||
| 2026-09-06 | **Polarity consistency RETRACTED** (a tautology of the spread gate; amplitude-only gives 12 of 53 unit-channels mixed) and the fleet **re-cut to 8–9 units, not 5**. "Persistent until serviced" weakened: 23 recoveries, median gap 6 days — though a field autozero cannot be excluded. |
|
||||
| 2026-09-06 | The spread gate is **blind to onsets by construction** — it rejects a moving floor, which is what an onset is. It rejected the very record in which the ramp is visible. |
|
||||
| 2026-09-07 | **The non-motion test** (Brian: "it doesn't cross zero"). `\|mean\|/peak` is bimodal with a 1.09% dead zone and returns exactly the 5 confirmed units from physics, not a threshold. Independent corroboration of the unit list. **BE11007 settled as NOT an offset** — a one-sided 9 in/s blast. |
|
||||
| 2026-09-07 | **BE12599 is a connector fault, not a geophone fault.** Unipolar spikes with a 26→118 ms RC tail progressing to a flat pedestal, and the fault MOVES between Long and Tran while the sensor self-check passes on every event. An element cannot hop channels; a connector can. Inspect before reseating. |
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Detect NON-MOTION on a geophone channel: |mean| / peak.
|
||||
|
||||
A geophone is a velocity sensor with no DC response, so its output over a
|
||||
record must integrate to ~zero — the ground does not relocate. Real motion
|
||||
therefore sits roughly half above and half below zero. Anything electrical —
|
||||
a charge-injection spike, a step, a parked pedestal — is one-sided.
|
||||
|
||||
mp = |mean| / peak ~0 for motion, ~1 for a pedestal
|
||||
frac_neg = share of samples < 0 ~0.3-0.5 for motion, ~0 for a fault
|
||||
|
||||
Why this beats the pre-trigger floor (`offset_scan3.py`): that detector's
|
||||
`spread <= 0.02` gate rejects any record whose floor is MOVING, which is
|
||||
exactly what an onset is — it discarded the one BE18438 record in which the
|
||||
ramp was visible. This test is indifferent to whether the fault is a spike,
|
||||
a ramp or a flat pedestal; none of them cross zero.
|
||||
|
||||
⚠ Not a rediscovery of the retracted v1 detector. v1 scored only the
|
||||
largest-peak axis and used the mean as a BASELINE estimator, where the median
|
||||
was required. Here the mean is the signal itself, per channel, and that is
|
||||
what the physics licenses.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse, csv, re, statistics, sys
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from minimateplus.event_file_io import read_blastware_file
|
||||
|
||||
GEO=("Tran","Vert","Long"); K=10.0/32000.0
|
||||
_WAVE=re.compile(r"\.[A-Za-z0-9]{2}0[Ww]$"); _STEM=re.compile(r"^([B-Z])(\d{3})")
|
||||
_SER=re.compile(rb"[A-Z]{2}\d{3,6}")
|
||||
|
||||
def serial_of(name, path=None):
|
||||
m=_STEM.match(name)
|
||||
if not m: return "?"
|
||||
num=(ord(m.group(1))-ord("B"))*1000+int(m.group(2))
|
||||
if path is not None:
|
||||
try:
|
||||
for s in _SER.findall(Path(path).read_bytes()):
|
||||
s=s.decode()
|
||||
if s[2:].lstrip("0")==str(num): return s
|
||||
except Exception: pass
|
||||
return f"BE{num}"
|
||||
|
||||
def scan(ps):
|
||||
import logging; logging.disable(logging.WARNING)
|
||||
p=Path(ps)
|
||||
try: ev=read_blastware_file(p)
|
||||
except Exception: return None
|
||||
s=ev.raw_samples or {}
|
||||
if not all(s.get(c) for c in GEO): return None
|
||||
ts=ev.timestamp
|
||||
stamp=(f"{ts.year:04d}-{ts.month:02d}-{ts.day:02d}T"
|
||||
f"{ts.hour:02d}:{ts.minute:02d}:{ts.second:02d}") if ts else ""
|
||||
ser=serial_of(p.name,p); out=[]
|
||||
for ch in GEO:
|
||||
a=[x*K for x in s[ch]]
|
||||
pk=max(abs(x) for x in a)
|
||||
if pk<=0: continue
|
||||
out.append({"serial":ser,"timestamp":stamp,"filename":p.name,"channel":ch,
|
||||
"peak":round(pk,4),
|
||||
"mean":round(statistics.fmean(a),4),
|
||||
"mp":round(abs(statistics.fmean(a))/pk,4),
|
||||
"frac_neg":round(sum(1 for x in a if x<0)/len(a),4),
|
||||
"n":len(a)})
|
||||
return out
|
||||
|
||||
COLS=["serial","timestamp","filename","channel","peak","mean","mp","frac_neg","n"]
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser()
|
||||
ap.add_argument("--dir",required=True); ap.add_argument("--out",required=True)
|
||||
ap.add_argument("--jobs",type=int,default=4)
|
||||
a=ap.parse_args()
|
||||
seen=set(); files=[]
|
||||
for q in sorted(Path(a.dir).rglob("*")):
|
||||
if q.is_file() and _WAVE.search(q.name) and q.name not in seen:
|
||||
seen.add(q.name); files.append(str(q))
|
||||
print(f"unique waveform binaries: {len(files)}",flush=True)
|
||||
rows=[]
|
||||
with ProcessPoolExecutor(max_workers=a.jobs) as ex:
|
||||
for i,f in enumerate(as_completed([ex.submit(scan,p) for p in files]),1):
|
||||
r=f.result()
|
||||
if r: rows.extend(r)
|
||||
if i%1000==0: print(f" {i}/{len(files)}",flush=True)
|
||||
with open(a.out,"w",newline="") as fh:
|
||||
w=csv.DictWriter(fh,fieldnames=COLS); w.writeheader(); w.writerows(rows)
|
||||
print(f"\nwrote {a.out} ({len(rows)} channel-rows)")
|
||||
|
||||
if __name__=="__main__": main()
|
||||
Reference in New Issue
Block a user