v0.23.0 - ZC freq in events store #31
@@ -8,6 +8,40 @@ All notable changes to seismo-relay are documented here.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v0.23.0 — 2026-08-04
|
||||||
|
|
||||||
|
**Per-channel ZC frequency in the events store.** The `events` table and the
|
||||||
|
`/db/events` feed now carry per-channel zero-crossing frequency, so consumers
|
||||||
|
(Terra-View's Vibration Summary) get frequency in bulk instead of one sidecar
|
||||||
|
fetch per event.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`events` columns** `tran_zc_freq`, `vert_zc_freq`, `long_zc_freq`, `mic_zc_freq`
|
||||||
|
(Hz) + `*_zc_above_range` flags (the Blastware `>N Hz` device-ceiling case). Added
|
||||||
|
via the existing incremental `_migrate` ADD COLUMN pass — **auto-migrates on
|
||||||
|
`SeismoDb()` construction, no manual migration**. `query_events` / `get_event`
|
||||||
|
(and thus `/db/events`) return them automatically (`SELECT *`).
|
||||||
|
- **Populated at ingest** — `PeakValues` gains the ZC-freq fields; both
|
||||||
|
`apply_report_to_event` and `apply_bw_report_dict_to_event` copy per-channel
|
||||||
|
`zc_freq_hz` / `zc_freq_above_range` from the parsed report onto them, and
|
||||||
|
`insert_events` persists them on INSERT and UPSERT.
|
||||||
|
- **Backfill** `scripts/backfill_event_zc_freq.py` — fills the columns for existing
|
||||||
|
events from their `.sfm.json` sidecars (idempotent, UPDATE-only).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `sfm/server.py` app-version string was stale at `0.17.0`; now tracks the real version.
|
||||||
|
|
||||||
|
### Upgrade Notes
|
||||||
|
|
||||||
|
Run the backfill once after deploying, against the events DB + waveform store:
|
||||||
|
`python3 scripts/backfill_event_zc_freq.py --db-path <seismo_relay.db> --store-root <waveforms/>`
|
||||||
|
Events whose sidecar carries no frequency (or that have no sidecar) stay NULL and
|
||||||
|
render "—" downstream.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.22.0 — 2026-07-03
|
## v0.22.0 — 2026-07-03
|
||||||
|
|
||||||
Full-snapshot bundle support (SFM side). Adds the DB-snapshot, recent-waveform,
|
Full-snapshot bundle support (SFM side). Adds the DB-snapshot, recent-waveform,
|
||||||
|
|||||||
@@ -270,6 +270,19 @@ def apply_report_to_event(event: Event, report: BwAsciiReport) -> None:
|
|||||||
if report.mic.pspl_dbl is not None and report.mic.pspl_dbl > 0:
|
if report.mic.pspl_dbl is not None and report.mic.pspl_dbl > 0:
|
||||||
pv.micl = _dbl_to_psi(report.mic.pspl_dbl)
|
pv.micl = _dbl_to_psi(report.mic.pspl_dbl)
|
||||||
|
|
||||||
|
if (t := ch.get("Tran")):
|
||||||
|
pv.tran_zc_freq = t.zc_freq_hz
|
||||||
|
pv.tran_zc_above_range = bool(getattr(t, "zc_freq_above_range", False))
|
||||||
|
if (v := ch.get("Vert")):
|
||||||
|
pv.vert_zc_freq = v.zc_freq_hz
|
||||||
|
pv.vert_zc_above_range = bool(getattr(v, "zc_freq_above_range", False))
|
||||||
|
if (l := ch.get("Long")):
|
||||||
|
pv.long_zc_freq = l.zc_freq_hz
|
||||||
|
pv.long_zc_above_range = bool(getattr(l, "zc_freq_above_range", False))
|
||||||
|
if report.mic is not None:
|
||||||
|
pv.mic_zc_freq = report.mic.zc_freq_hz
|
||||||
|
pv.mic_zc_above_range = bool(getattr(report.mic, "zc_freq_above_range", False))
|
||||||
|
|
||||||
if event.project_info is None:
|
if event.project_info is None:
|
||||||
event.project_info = ProjectInfo()
|
event.project_info = ProjectInfo()
|
||||||
pi = event.project_info
|
pi = event.project_info
|
||||||
@@ -329,6 +342,17 @@ def apply_bw_report_dict_to_event(event: Event, bw_report: dict) -> None:
|
|||||||
if pspl is not None and pspl > 0:
|
if pspl is not None and pspl > 0:
|
||||||
pv.micl = _dbl_to_psi(pspl)
|
pv.micl = _dbl_to_psi(pspl)
|
||||||
|
|
||||||
|
for axis, freq_attr, above_attr in (
|
||||||
|
("tran", "tran_zc_freq", "tran_zc_above_range"),
|
||||||
|
("vert", "vert_zc_freq", "vert_zc_above_range"),
|
||||||
|
("long", "long_zc_freq", "long_zc_above_range"),
|
||||||
|
):
|
||||||
|
chd = peaks.get(axis) or {}
|
||||||
|
setattr(pv, freq_attr, chd.get("zc_freq_hz"))
|
||||||
|
setattr(pv, above_attr, bool(chd.get("zc_freq_above_range", False)))
|
||||||
|
pv.mic_zc_freq = mic.get("zc_freq_hz")
|
||||||
|
pv.mic_zc_above_range = bool(mic.get("zc_freq_above_range", False))
|
||||||
|
|
||||||
rec = bw_report.get("recording") or {}
|
rec = bw_report.get("recording") or {}
|
||||||
sr = rec.get("sample_rate_sps")
|
sr = rec.get("sample_rate_sps")
|
||||||
if sr:
|
if sr:
|
||||||
|
|||||||
@@ -352,6 +352,14 @@ class PeakValues:
|
|||||||
long: Optional[float] = None # Longitudinal PPV (in/s) ✅
|
long: Optional[float] = None # Longitudinal PPV (in/s) ✅
|
||||||
micl: Optional[float] = None # Air overpressure (psi) 🔶 (units uncertain)
|
micl: Optional[float] = None # Air overpressure (psi) 🔶 (units uncertain)
|
||||||
peak_vector_sum: Optional[float] = None # Scalar geo PVS (in/s) ✅
|
peak_vector_sum: Optional[float] = None # Scalar geo PVS (in/s) ✅
|
||||||
|
tran_zc_freq: Optional[float] = None
|
||||||
|
vert_zc_freq: Optional[float] = None
|
||||||
|
long_zc_freq: Optional[float] = None
|
||||||
|
mic_zc_freq: Optional[float] = None
|
||||||
|
tran_zc_above_range: bool = False
|
||||||
|
vert_zc_above_range: bool = False
|
||||||
|
long_zc_above_range: bool = False
|
||||||
|
mic_zc_above_range: bool = False
|
||||||
|
|
||||||
|
|
||||||
# ── Project / operator metadata ───────────────────────────────────────────────
|
# ── Project / operator metadata ───────────────────────────────────────────────
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "seismo-relay"
|
name = "seismo-relay"
|
||||||
version = "0.22.0"
|
version = "0.23.0"
|
||||||
description = "Python client and REST server for MiniMate Plus seismographs"
|
description = "Python client and REST server for MiniMate Plus seismographs"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Backfill events.*_zc_freq / *_zc_above_range from .sfm.json sidecars.
|
||||||
|
|
||||||
|
Idempotent. Reads each event's sidecar bw_report and writes the per-channel
|
||||||
|
ZC frequency into the DB columns via a direct UPDATE. Events with no sidecar
|
||||||
|
(or no bw_report freq) are left untouched. Mirrors scripts/backfill_record_type.py.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
|
from sfm.database import SeismoDb
|
||||||
|
from sfm.waveform_store import WaveformStore
|
||||||
|
|
||||||
|
log = logging.getLogger("backfill_event_zc_freq")
|
||||||
|
|
||||||
|
|
||||||
|
def _freqs_from_sidecar(sidecar: dict) -> dict | None:
|
||||||
|
bw = (sidecar or {}).get("bw_report") or {}
|
||||||
|
peaks = bw.get("peaks") or {}
|
||||||
|
mic = bw.get("mic") or {}
|
||||||
|
out = {}
|
||||||
|
for axis in ("tran", "vert", "long"):
|
||||||
|
chd = peaks.get(axis) or {}
|
||||||
|
out[f"{axis}_zc_freq"] = chd.get("zc_freq_hz")
|
||||||
|
out[f"{axis}_zc_above_range"] = 1 if chd.get("zc_freq_above_range") else 0
|
||||||
|
out["mic_zc_freq"] = mic.get("zc_freq_hz")
|
||||||
|
out["mic_zc_above_range"] = 1 if mic.get("zc_freq_above_range") else 0
|
||||||
|
# None if there's literally no frequency anywhere.
|
||||||
|
if all(out[f"{a}_zc_freq"] is None for a in ("tran", "vert", "long", "mic")):
|
||||||
|
return None
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_zc_freq(db: SeismoDb, store, *, dry_run: bool = False) -> dict:
|
||||||
|
counts = {"updated": 0, "skipped_no_sidecar": 0, "skipped_no_freq": 0}
|
||||||
|
# Pull every event (paginate generously; adjust if a store has >100k events).
|
||||||
|
rows = db.query_events(limit=1_000_000)
|
||||||
|
for row in rows:
|
||||||
|
serial = row.get("serial")
|
||||||
|
filename = row.get("blastware_filename")
|
||||||
|
if not serial or not filename:
|
||||||
|
counts["skipped_no_sidecar"] += 1
|
||||||
|
continue
|
||||||
|
sidecar = store.load_sidecar(serial, filename)
|
||||||
|
if sidecar is None:
|
||||||
|
counts["skipped_no_sidecar"] += 1
|
||||||
|
continue
|
||||||
|
freqs = _freqs_from_sidecar(sidecar)
|
||||||
|
if freqs is None:
|
||||||
|
counts["skipped_no_freq"] += 1
|
||||||
|
continue
|
||||||
|
if dry_run:
|
||||||
|
counts["updated"] += 1
|
||||||
|
continue
|
||||||
|
with db._connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""UPDATE events SET
|
||||||
|
tran_zc_freq=?, vert_zc_freq=?, long_zc_freq=?, mic_zc_freq=?,
|
||||||
|
tran_zc_above_range=?, vert_zc_above_range=?,
|
||||||
|
long_zc_above_range=?, mic_zc_above_range=?
|
||||||
|
WHERE id=?""",
|
||||||
|
(freqs["tran_zc_freq"], freqs["vert_zc_freq"], freqs["long_zc_freq"],
|
||||||
|
freqs["mic_zc_freq"], freqs["tran_zc_above_range"],
|
||||||
|
freqs["vert_zc_above_range"], freqs["long_zc_above_range"],
|
||||||
|
freqs["mic_zc_above_range"], row["id"]),
|
||||||
|
)
|
||||||
|
counts["updated"] += 1
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
p = argparse.ArgumentParser(description=__doc__)
|
||||||
|
p.add_argument("--db-path", default=str(
|
||||||
|
Path(__file__).resolve().parent.parent / "bridges" / "captures" / "seismo_relay.db"))
|
||||||
|
p.add_argument("--store-root", default=None)
|
||||||
|
p.add_argument("--dry-run", action="store_true")
|
||||||
|
p.add_argument("-v", "--verbose", action="store_true")
|
||||||
|
args = p.parse_args(argv)
|
||||||
|
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
|
||||||
|
db_path = Path(args.db_path).expanduser().resolve()
|
||||||
|
store_root = (
|
||||||
|
Path(args.store_root).expanduser().resolve()
|
||||||
|
if args.store_root else db_path.parent / "waveforms"
|
||||||
|
)
|
||||||
|
db = SeismoDb(db_path)
|
||||||
|
store = WaveformStore(store_root)
|
||||||
|
counts = backfill_zc_freq(db, store, dry_run=args.dry_run)
|
||||||
|
log.info("backfill_zc_freq: %s", counts)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+47
-3
@@ -86,6 +86,14 @@ CREATE TABLE IF NOT EXISTS events (
|
|||||||
a5_pickle_filename TEXT, -- "<filename>.a5.pkl" sidecar
|
a5_pickle_filename TEXT, -- "<filename>.a5.pkl" sidecar
|
||||||
sidecar_filename TEXT, -- "<filename>.sfm.json" review/metadata sidecar
|
sidecar_filename TEXT, -- "<filename>.sfm.json" review/metadata sidecar
|
||||||
device_family TEXT, -- "series3" (MiniMate Plus / BW) | "series4" (Micromate / Thor) — drives per-family UI rendering (units, labels)
|
device_family TEXT, -- "series3" (MiniMate Plus / BW) | "series4" (Micromate / Thor) — drives per-family UI rendering (units, labels)
|
||||||
|
tran_zc_freq REAL, -- Transverse ZC frequency (Hz)
|
||||||
|
vert_zc_freq REAL, -- Vertical ZC frequency (Hz)
|
||||||
|
long_zc_freq REAL, -- Longitudinal ZC frequency (Hz)
|
||||||
|
mic_zc_freq REAL, -- Microphone ZC frequency (Hz)
|
||||||
|
tran_zc_above_range INTEGER, -- 1 = value is device ceiling, render ">N Hz"
|
||||||
|
vert_zc_above_range INTEGER,
|
||||||
|
long_zc_above_range INTEGER,
|
||||||
|
mic_zc_above_range INTEGER,
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||||
UNIQUE(serial, timestamp)
|
UNIQUE(serial, timestamp)
|
||||||
);
|
);
|
||||||
@@ -200,6 +208,14 @@ class SeismoDb:
|
|||||||
("a5_pickle_filename", "TEXT"),
|
("a5_pickle_filename", "TEXT"),
|
||||||
("sidecar_filename", "TEXT"),
|
("sidecar_filename", "TEXT"),
|
||||||
("device_family", "TEXT"),
|
("device_family", "TEXT"),
|
||||||
|
("tran_zc_freq", "REAL"),
|
||||||
|
("vert_zc_freq", "REAL"),
|
||||||
|
("long_zc_freq", "REAL"),
|
||||||
|
("mic_zc_freq", "REAL"),
|
||||||
|
("tran_zc_above_range", "INTEGER"),
|
||||||
|
("vert_zc_above_range", "INTEGER"),
|
||||||
|
("long_zc_above_range", "INTEGER"),
|
||||||
|
("mic_zc_above_range", "INTEGER"),
|
||||||
):
|
):
|
||||||
if col not in existing_cols:
|
if col not in existing_cols:
|
||||||
log.info("_migrate: events ADD COLUMN %s %s", col, ddl)
|
log.info("_migrate: events ADD COLUMN %s %s", col, ddl)
|
||||||
@@ -399,8 +415,12 @@ class SeismoDb:
|
|||||||
sample_rate, record_type,
|
sample_rate, record_type,
|
||||||
blastware_filename, blastware_filesize,
|
blastware_filename, blastware_filesize,
|
||||||
a5_pickle_filename, sidecar_filename,
|
a5_pickle_filename, sidecar_filename,
|
||||||
device_family)
|
device_family,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
tran_zc_freq, vert_zc_freq, long_zc_freq, mic_zc_freq,
|
||||||
|
tran_zc_above_range, vert_zc_above_range,
|
||||||
|
long_zc_above_range, mic_zc_above_range)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
self._new_id(), serial, key, session_id, ts,
|
self._new_id(), serial, key, session_id, ts,
|
||||||
@@ -420,6 +440,14 @@ class SeismoDb:
|
|||||||
rec.get("a5_pickle_filename"),
|
rec.get("a5_pickle_filename"),
|
||||||
rec.get("sidecar_filename"),
|
rec.get("sidecar_filename"),
|
||||||
device_family,
|
device_family,
|
||||||
|
pv.tran_zc_freq if pv else None,
|
||||||
|
pv.vert_zc_freq if pv else None,
|
||||||
|
pv.long_zc_freq if pv else None,
|
||||||
|
pv.mic_zc_freq if pv else None,
|
||||||
|
(1 if (pv and pv.tran_zc_above_range) else 0),
|
||||||
|
(1 if (pv and pv.vert_zc_above_range) else 0),
|
||||||
|
(1 if (pv and pv.long_zc_above_range) else 0),
|
||||||
|
(1 if (pv and pv.mic_zc_above_range) else 0),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
inserted += 1
|
inserted += 1
|
||||||
@@ -461,7 +489,15 @@ class SeismoDb:
|
|||||||
blastware_filesize = ?,
|
blastware_filesize = ?,
|
||||||
a5_pickle_filename = ?,
|
a5_pickle_filename = ?,
|
||||||
sidecar_filename = ?,
|
sidecar_filename = ?,
|
||||||
device_family = COALESCE(?, device_family)
|
device_family = COALESCE(?, device_family),
|
||||||
|
tran_zc_freq = ?,
|
||||||
|
vert_zc_freq = ?,
|
||||||
|
long_zc_freq = ?,
|
||||||
|
mic_zc_freq = ?,
|
||||||
|
tran_zc_above_range = ?,
|
||||||
|
vert_zc_above_range = ?,
|
||||||
|
long_zc_above_range = ?,
|
||||||
|
mic_zc_above_range = ?
|
||||||
WHERE serial = ? AND timestamp = ?
|
WHERE serial = ? AND timestamp = ?
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
@@ -481,6 +517,14 @@ class SeismoDb:
|
|||||||
rec.get("a5_pickle_filename") if rec else None,
|
rec.get("a5_pickle_filename") if rec else None,
|
||||||
rec.get("sidecar_filename") if rec else None,
|
rec.get("sidecar_filename") if rec else None,
|
||||||
device_family,
|
device_family,
|
||||||
|
pv.tran_zc_freq if pv else None,
|
||||||
|
pv.vert_zc_freq if pv else None,
|
||||||
|
pv.long_zc_freq if pv else None,
|
||||||
|
pv.mic_zc_freq if pv else None,
|
||||||
|
(1 if (pv and pv.tran_zc_above_range) else 0),
|
||||||
|
(1 if (pv and pv.vert_zc_above_range) else 0),
|
||||||
|
(1 if (pv and pv.long_zc_above_range) else 0),
|
||||||
|
(1 if (pv and pv.mic_zc_above_range) else 0),
|
||||||
serial,
|
serial,
|
||||||
ts,
|
ts,
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-1
@@ -90,7 +90,7 @@ app = FastAPI(
|
|||||||
"Implements the minimateplus RS-232 protocol library.\n"
|
"Implements the minimateplus RS-232 protocol library.\n"
|
||||||
"Proxied by terra-view at /api/sfm/*."
|
"Proxied by terra-view at /api/sfm/*."
|
||||||
),
|
),
|
||||||
version="0.17.0",
|
version="0.23.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Allow requests from the waveform viewer opened as a local file (file://)
|
# Allow requests from the waveform viewer opened as a local file (file://)
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import os, sys, sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from sfm.database import SeismoDb
|
||||||
|
|
||||||
|
ZC_COLS = ["tran_zc_freq", "vert_zc_freq", "long_zc_freq", "mic_zc_freq",
|
||||||
|
"tran_zc_above_range", "vert_zc_above_range",
|
||||||
|
"long_zc_above_range", "mic_zc_above_range"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_fresh_db_has_zc_freq_columns(tmp_path: Path):
|
||||||
|
db = SeismoDb(tmp_path / "seismo_relay.db")
|
||||||
|
with sqlite3.connect(str(tmp_path / "seismo_relay.db")) as conn:
|
||||||
|
cols = {r[1] for r in conn.execute("PRAGMA table_info(events)").fetchall()}
|
||||||
|
for c in ZC_COLS:
|
||||||
|
assert c in cols, f"missing column {c}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_adds_zc_freq_columns_to_legacy_db(tmp_path: Path):
|
||||||
|
db_path = tmp_path / "old.db"
|
||||||
|
# A "modern-minus-zc" events table (has blastware_* but not the zc columns).
|
||||||
|
with sqlite3.connect(str(db_path)) as conn:
|
||||||
|
conn.executescript("""
|
||||||
|
CREATE TABLE events (
|
||||||
|
id TEXT PRIMARY KEY, serial TEXT NOT NULL, waveform_key TEXT NOT NULL,
|
||||||
|
session_id TEXT, timestamp TEXT,
|
||||||
|
tran_ppv REAL, vert_ppv REAL, long_ppv REAL, peak_vector_sum REAL, mic_ppv REAL,
|
||||||
|
project TEXT, client TEXT, operator TEXT, sensor_location TEXT,
|
||||||
|
sample_rate INTEGER, record_type TEXT,
|
||||||
|
false_trigger INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||||
|
UNIQUE(serial, timestamp)
|
||||||
|
);
|
||||||
|
INSERT INTO events (id, serial, waveform_key, timestamp)
|
||||||
|
VALUES ('legacy-id', 'BE11529', '01110000', '2026-04-01T12:00:00');
|
||||||
|
""")
|
||||||
|
db = SeismoDb(db_path) # triggers _migrate
|
||||||
|
rows = db.query_events(serial="BE11529")
|
||||||
|
assert len(rows) == 1
|
||||||
|
for c in ZC_COLS:
|
||||||
|
assert c in rows[0] # present
|
||||||
|
assert rows[0][c] is None # NULL for the legacy row
|
||||||
|
|
||||||
|
|
||||||
|
from minimateplus.models import PeakValues, Event
|
||||||
|
from minimateplus import event_file_io
|
||||||
|
from minimateplus.bw_ascii_report import BwAsciiReport, ChannelStats, MicStats
|
||||||
|
|
||||||
|
|
||||||
|
def _report_with_freqs():
|
||||||
|
r = BwAsciiReport()
|
||||||
|
r.channels["Tran"] = ChannelStats(ppv_ips=0.075, zc_freq_hz=100.0, zc_freq_above_range=True)
|
||||||
|
r.channels["Vert"] = ChannelStats(ppv_ips=0.220, zc_freq_hz=85.0)
|
||||||
|
r.channels["Long"] = ChannelStats(ppv_ips=0.045, zc_freq_hz=73.0)
|
||||||
|
r.mic = MicStats(zc_freq_hz=51.0)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_report_copies_zc_freq_onto_peakvalues():
|
||||||
|
ev = Event(index=0)
|
||||||
|
event_file_io.apply_report_to_event(ev, _report_with_freqs())
|
||||||
|
pv = ev.peak_values
|
||||||
|
assert pv.vert_zc_freq == 85.0
|
||||||
|
assert pv.tran_zc_freq == 100.0
|
||||||
|
assert pv.tran_zc_above_range is True
|
||||||
|
assert pv.vert_zc_above_range is False
|
||||||
|
assert pv.mic_zc_freq == 51.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_bw_report_dict_copies_zc_freq():
|
||||||
|
ev = Event(index=0)
|
||||||
|
bw = {
|
||||||
|
"peaks": {
|
||||||
|
"tran": {"ppv_ips": 0.075, "zc_freq_hz": 100.0, "zc_freq_above_range": True},
|
||||||
|
"vert": {"ppv_ips": 0.220, "zc_freq_hz": 85.0},
|
||||||
|
"long": {"ppv_ips": 0.045, "zc_freq_hz": 73.0},
|
||||||
|
},
|
||||||
|
"mic": {"zc_freq_hz": 51.0, "zc_freq_above_range": False},
|
||||||
|
}
|
||||||
|
event_file_io.apply_bw_report_dict_to_event(ev, bw)
|
||||||
|
pv = ev.peak_values
|
||||||
|
assert pv.vert_zc_freq == 85.0
|
||||||
|
assert pv.tran_zc_above_range is True
|
||||||
|
assert pv.mic_zc_freq == 51.0
|
||||||
|
|
||||||
|
|
||||||
|
def _event_with_zc(waveform_key="01110000"):
|
||||||
|
from minimateplus.models import Event, Timestamp
|
||||||
|
ev = Event(index=0)
|
||||||
|
ev._waveform_key = bytes.fromhex(waveform_key)
|
||||||
|
ev.timestamp = Timestamp(raw=b"", flag=0x10, year=2026, unknown_byte=0,
|
||||||
|
month=6, day=25, hour=8, minute=50, second=0)
|
||||||
|
ev.record_type = "Waveform"
|
||||||
|
ev.peak_values = PeakValues(
|
||||||
|
tran=0.075, vert=0.220, long=0.045, peak_vector_sum=0.231, micl=0.01,
|
||||||
|
tran_zc_freq=100.0, vert_zc_freq=85.0, long_zc_freq=73.0, mic_zc_freq=51.0,
|
||||||
|
tran_zc_above_range=True,
|
||||||
|
)
|
||||||
|
return ev
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_events_persists_zc_freq(tmp_path: Path):
|
||||||
|
db = SeismoDb(tmp_path / "seismo_relay.db")
|
||||||
|
ins, _ = db.insert_events([_event_with_zc()], serial="BE11529")
|
||||||
|
assert ins == 1
|
||||||
|
row = db.query_events(serial="BE11529")[0]
|
||||||
|
assert row["vert_zc_freq"] == 85.0
|
||||||
|
assert row["tran_zc_freq"] == 100.0
|
||||||
|
assert row["tran_zc_above_range"] == 1
|
||||||
|
assert row["vert_zc_above_range"] == 0
|
||||||
|
assert row["mic_zc_freq"] == 51.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_upsert_updates_zc_freq(tmp_path: Path):
|
||||||
|
db = SeismoDb(tmp_path / "seismo_relay.db")
|
||||||
|
db.insert_events([_event_with_zc()], serial="BE11529")
|
||||||
|
ev2 = _event_with_zc()
|
||||||
|
ev2.peak_values.vert_zc_freq = 91.0 # same (serial, timestamp) → UPSERT
|
||||||
|
db.insert_events([ev2], serial="BE11529")
|
||||||
|
row = db.query_events(serial="BE11529")[0]
|
||||||
|
assert row["vert_zc_freq"] == 91.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_backfill_zc_freq_from_sidecar(tmp_path: Path):
|
||||||
|
import importlib
|
||||||
|
mod = importlib.import_module("scripts.backfill_event_zc_freq")
|
||||||
|
|
||||||
|
db = SeismoDb(tmp_path / "seismo_relay.db")
|
||||||
|
# Insert an event WITHOUT freq (simulates a pre-feature row), with a filename.
|
||||||
|
from minimateplus.models import Event, Timestamp
|
||||||
|
ev = Event(index=0)
|
||||||
|
ev._waveform_key = bytes.fromhex("01110000")
|
||||||
|
ev.timestamp = Timestamp(raw=b"", flag=0x10, year=2026, unknown_byte=0,
|
||||||
|
month=6, day=25, hour=8, minute=50, second=0)
|
||||||
|
ev.record_type = "Waveform"
|
||||||
|
db.insert_events([ev], serial="BE11529",
|
||||||
|
waveform_records={"01110000": {"filename": "M529AAAA.AB0T"}})
|
||||||
|
|
||||||
|
class FakeStore:
|
||||||
|
def load_sidecar(self, serial, filename):
|
||||||
|
return {"bw_report": {
|
||||||
|
"peaks": {"vert": {"zc_freq_hz": 85.0},
|
||||||
|
"tran": {"zc_freq_hz": 100.0, "zc_freq_above_range": True}},
|
||||||
|
"mic": {"zc_freq_hz": 51.0}}}
|
||||||
|
|
||||||
|
counts = mod.backfill_zc_freq(db, FakeStore())
|
||||||
|
assert counts["updated"] == 1
|
||||||
|
row = db.query_events(serial="BE11529")[0]
|
||||||
|
assert row["vert_zc_freq"] == 85.0
|
||||||
|
assert row["tran_zc_above_range"] == 1
|
||||||
|
assert row["mic_zc_freq"] == 51.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_backfill_skips_event_without_sidecar(tmp_path: Path):
|
||||||
|
import importlib
|
||||||
|
mod = importlib.import_module("scripts.backfill_event_zc_freq")
|
||||||
|
db = SeismoDb(tmp_path / "seismo_relay.db")
|
||||||
|
from minimateplus.models import Event, Timestamp
|
||||||
|
ev = Event(index=0)
|
||||||
|
ev._waveform_key = bytes.fromhex("01110000")
|
||||||
|
ev.timestamp = Timestamp(raw=b"", flag=0x10, year=2026, unknown_byte=0,
|
||||||
|
month=6, day=25, hour=8, minute=50, second=0)
|
||||||
|
db.insert_events([ev], serial="BE11529",
|
||||||
|
waveform_records={"01110000": {"filename": "M529AAAA.AB0T"}})
|
||||||
|
|
||||||
|
class NoneStore:
|
||||||
|
def load_sidecar(self, serial, filename): return None
|
||||||
|
|
||||||
|
counts = mod.backfill_zc_freq(db, NoneStore())
|
||||||
|
assert counts["updated"] == 0
|
||||||
|
assert counts["skipped_no_sidecar"] == 1
|
||||||
Reference in New Issue
Block a user