From c982512e170c3cdd1bf2a2ed1eece5a41dcd461e Mon Sep 17 00:00:00 2001 From: serversdown Date: Sat, 22 Aug 2026 06:06:46 +0000 Subject: [PATCH] feat(scripts): backfill events.shape_* from .h5 samples --- scripts/backfill_event_shape.py | 48 ++++++++++++++++++++++++++++++ tests/test_backfill_event_shape.py | 35 ++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 scripts/backfill_event_shape.py create mode 100644 tests/test_backfill_event_shape.py diff --git a/scripts/backfill_event_shape.py b/scripts/backfill_event_shape.py new file mode 100644 index 0000000..9de2f85 --- /dev/null +++ b/scripts/backfill_event_shape.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Backfill events.shape_* from each event's .h5 waveform samples. Idempotent.""" +from __future__ import annotations +import argparse, logging, 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 +from sfm.shape_metrics import shape_from_h5 + +log = logging.getLogger("backfill_event_shape") + +def backfill_shape(db: SeismoDb, store: WaveformStore, *, dry_run: bool = False) -> dict: + counts = {"updated": 0, "skipped_no_h5": 0, "skipped_no_samples": 0} + for row in db.query_events(limit=1_000_000): + serial, filename = row.get("serial"), row.get("blastware_filename") + if not serial or not filename: + counts["skipped_no_h5"] += 1; continue + h5_path = store.hdf5_path_for(serial, filename) + if not h5_path.exists(): + counts["skipped_no_h5"] += 1; continue + shape = shape_from_h5(h5_path) + if shape is None: + counts["skipped_no_samples"] += 1; continue + if not dry_run: + with db._connect() as conn: + conn.execute( + "UPDATE events SET shape_crest_factor=?, shape_near_peak_count=?, " + "shape_sample_count=?, shape_axis=? WHERE id=?", + (shape["crest_factor"], shape["near_peak_count"], + shape["sample_count"], shape["axis"], row["id"])) + counts["updated"] += 1 + log.info("backfill_shape: %s", counts) + return counts + +def main(argv=None) -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--db-path", required=True) + ap.add_argument("--store-root", required=True) + ap.add_argument("--dry-run", action="store_true") + a = ap.parse_args(argv) + logging.basicConfig(level=logging.INFO) + counts = backfill_shape(SeismoDb(a.db_path), WaveformStore(a.store_root), dry_run=a.dry_run) + print(counts) + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_backfill_event_shape.py b/tests/test_backfill_event_shape.py new file mode 100644 index 0000000..3c926c3 --- /dev/null +++ b/tests/test_backfill_event_shape.py @@ -0,0 +1,35 @@ +from __future__ import annotations +import numpy as np, h5py + +from sfm.database import SeismoDb +from sfm.waveform_store import WaveformStore +from scripts.backfill_event_shape import backfill_shape +from minimateplus.models import Event + + +def _h5(path, long): + with h5py.File(path, "w") as f: + g = f.create_group("samples") + for k in ("Tran", "Vert"): + g.create_dataset(k, data=np.zeros(1024, "float32")) + g.create_dataset("Long", data=np.asarray(long, "float32")) + + +def test_backfill_updates_shape_and_is_idempotent(tmp_path): + db = SeismoDb(tmp_path / "s.db") + store = WaveformStore(tmp_path / "waveforms") + ev = Event(index=0) + ev._waveform_key = bytes.fromhex("0111abcd") + db.insert_events([ev], serial="BE1", + waveform_records={ev._waveform_key.hex(): + {"filename": "F.CE0W", "filesize": 10}}) + # place the .h5 where store.paths_for expects it + long = np.zeros(1024); long[100] = 0.48 + _h5(store.hdf5_path_for("BE1", "F.CE0W"), long) + + c1 = backfill_shape(db, store) + assert c1["updated"] == 1 + row = db.query_events(serial="BE1")[0] + assert row["shape_axis"] == "Long" and row["shape_near_peak_count"] <= 3 + c2 = backfill_shape(db, store) # idempotent: re-run overwrites same values + assert db.query_events(serial="BE1")[0]["shape_crest_factor"] == row["shape_crest_factor"]