bepad 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bepad/__init__.py +17 -0
- bepad/__main__.py +206 -0
- bepad/associator.py +339 -0
- bepad/build/__init__.py +11 -0
- bepad/build/config.py +116 -0
- bepad/build/geo.py +64 -0
- bepad/build/labels.py +341 -0
- bepad/build/pool.py +392 -0
- bepad/build/release.py +256 -0
- bepad/build/utdquake.py +76 -0
- bepad/build/windows.py +408 -0
- bepad/data.py +185 -0
- bepad/leaderboard.py +368 -0
- bepad/metrics.py +324 -0
- bepad/plots.py +950 -0
- bepad/stats.py +140 -0
- bepad/torch.py +159 -0
- bepad-0.1.0.dist-info/METADATA +85 -0
- bepad-0.1.0.dist-info/RECORD +23 -0
- bepad-0.1.0.dist-info/WHEEL +5 -0
- bepad-0.1.0.dist-info/entry_points.txt +2 -0
- bepad-0.1.0.dist-info/licenses/LICENSE +674 -0
- bepad-0.1.0.dist-info/top_level.txt +1 -0
bepad/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""BEPAD: a benchmark dataset for seismic phase association, built from UTDQuake.
|
|
2
|
+
|
|
3
|
+
import bepad
|
|
4
|
+
test = bepad.load("test", labels=False) # download + read the test split
|
|
5
|
+
report = bepad.evaluate(predictions, test) # score an associator
|
|
6
|
+
class MyMethod(bepad.Associator): ... # template for reusable associators
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
from .data import CORE_NAME, CORE_NETWORKS, LABEL_COLUMNS, Release, Split, download, load, open, predictions_frame # noqa: E402,A004
|
|
12
|
+
from .metrics import evaluate # noqa: E402
|
|
13
|
+
from .associator import Associator, load_associator # noqa: E402
|
|
14
|
+
|
|
15
|
+
__all__ = ["CORE_NAME", "CORE_NETWORKS", "load", "download", "open", "evaluate", "Associator", "load_associator", "predictions_frame",
|
|
16
|
+
"Release", "Split",
|
|
17
|
+
"LABEL_COLUMNS", "__version__"]
|
bepad/__main__.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Command line interface.
|
|
2
|
+
|
|
3
|
+
Users
|
|
4
|
+
bepad download --split test [--networks tx CM] [--release v0.1]
|
|
5
|
+
bepad evaluate predictions.parquet --release-dir <dir> [--split test] [--out report/]
|
|
6
|
+
|
|
7
|
+
Associator authors
|
|
8
|
+
bepad new my-method create associators/my-method/ from the template
|
|
9
|
+
bepad run associators/my-method --split val run + score (tune on val)
|
|
10
|
+
bepad run associators/my-method --split test --out results/my-method
|
|
11
|
+
bepad submit results/my-method --code associators/my-method check, then open a Hugging Face pull request
|
|
12
|
+
|
|
13
|
+
Maintainers (building the dataset)
|
|
14
|
+
python -m bepad pool add tx CM [--force] QC networks into data/pool
|
|
15
|
+
python -m bepad pool sync [--dry-run] add every UTDQuake network not in the pool
|
|
16
|
+
python -m bepad pool list
|
|
17
|
+
python -m bepad pool fit-labels [--networks NEW --keep-global]
|
|
18
|
+
python -m bepad build configs/v0.1.yaml [--networks tx] [--workers N] [--no-finalize] [--release NAME]
|
|
19
|
+
python -m bepad finalize configs/v0.1.yaml
|
|
20
|
+
python -m bepad figures configs/v0.1.yaml
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main(argv=None):
|
|
30
|
+
from .data import RELEASE
|
|
31
|
+
|
|
32
|
+
ap = argparse.ArgumentParser(prog="bepad", description="BEPAD phase association benchmark")
|
|
33
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
34
|
+
|
|
35
|
+
dl = sub.add_parser("download", help="download part of a release from Hugging Face")
|
|
36
|
+
dl.add_argument("--split", nargs="*", default=["test"])
|
|
37
|
+
dl.add_argument("--networks", nargs="*")
|
|
38
|
+
dl.add_argument("--release", default=RELEASE)
|
|
39
|
+
dl.add_argument("--local-dir")
|
|
40
|
+
|
|
41
|
+
ev = sub.add_parser("evaluate", help="score predictions on a release split")
|
|
42
|
+
ev.add_argument("predictions")
|
|
43
|
+
ev.add_argument("--release-dir", required=True, help="local release folder (see `bepad download`)")
|
|
44
|
+
ev.add_argument("--split", default="test")
|
|
45
|
+
ev.add_argument("--networks", nargs="*")
|
|
46
|
+
ev.add_argument("--min-shared", type=int, default=3)
|
|
47
|
+
ev.add_argument("--out", help="folder for the report (csv + summary.json)")
|
|
48
|
+
|
|
49
|
+
nw = sub.add_parser("new", help="create an associator folder from the template")
|
|
50
|
+
nw.add_argument("name")
|
|
51
|
+
nw.add_argument("--root", default="associators")
|
|
52
|
+
|
|
53
|
+
rn = sub.add_parser("run", help="run an associator folder on a split and score it")
|
|
54
|
+
rn.add_argument("associator", help="folder with associator.py (e.g. associators/pyocto)")
|
|
55
|
+
rn.add_argument("--split", default="test")
|
|
56
|
+
rn.add_argument("--networks", nargs="*")
|
|
57
|
+
rn.add_argument("--scenarios", nargs="*", help="only these scenarios (partial run)")
|
|
58
|
+
rn.add_argument("--max-windows", type=int, help="at most N windows per network (partial run)")
|
|
59
|
+
rn.add_argument("--workers", type=int, default=1, help="CPU processes (windows run in parallel)")
|
|
60
|
+
rn.add_argument("--release", default=RELEASE)
|
|
61
|
+
rn.add_argument("--release-dir", help="local release folder instead of downloading")
|
|
62
|
+
rn.add_argument("--out", help="save predictions, report and submission.json here")
|
|
63
|
+
|
|
64
|
+
sb = sub.add_parser("submit", help="check a result folder and open a leaderboard pull request")
|
|
65
|
+
sb.add_argument("result")
|
|
66
|
+
sb.add_argument("--code", help="the associator folder, published with the results (recommended)")
|
|
67
|
+
sb.add_argument("--release-dir", help="local release folder for the check (otherwise downloaded)")
|
|
68
|
+
sb.add_argument("--dry-run", action="store_true", help="check only; print the manual steps")
|
|
69
|
+
|
|
70
|
+
vf = sub.add_parser("verify", help="re-check a result folder (scores recomputed from predictions)")
|
|
71
|
+
vf.add_argument("result")
|
|
72
|
+
vf.add_argument("--code")
|
|
73
|
+
vf.add_argument("--release-dir")
|
|
74
|
+
|
|
75
|
+
lb = sub.add_parser("leaderboard", help="[maintainers] build tables / review pull requests")
|
|
76
|
+
lb.add_argument("action", choices=["build", "review"])
|
|
77
|
+
lb.add_argument("--dir", default="leaderboard", help="local leaderboard folder (build)")
|
|
78
|
+
lb.add_argument("--release-dir")
|
|
79
|
+
|
|
80
|
+
pool = sub.add_parser("pool", help="[build] QC'd event pool")
|
|
81
|
+
pool.add_argument("action", choices=["add", "sync", "list", "remove", "fit-labels"])
|
|
82
|
+
pool.add_argument("networks", nargs="*")
|
|
83
|
+
pool.add_argument("--config", default="configs/pool.yaml")
|
|
84
|
+
pool.add_argument("--force", action="store_true")
|
|
85
|
+
pool.add_argument("--dry-run", action="store_true")
|
|
86
|
+
pool.add_argument("--keep-global", action="store_true",
|
|
87
|
+
help="fit-labels: keep q(M,r) and sigma(r), only add curves of the given networks")
|
|
88
|
+
|
|
89
|
+
for name, hlp in (("build", "[build] generate release windows"), ("finalize", "[build] manifest + metadata"),
|
|
90
|
+
("figures", "[build] release figures")):
|
|
91
|
+
p = sub.add_parser(name, help=hlp)
|
|
92
|
+
p.add_argument("config")
|
|
93
|
+
p.add_argument("--release", help="override the release name of the config")
|
|
94
|
+
if name == "build":
|
|
95
|
+
p.add_argument("--networks", nargs="*")
|
|
96
|
+
p.add_argument("--workers", type=int)
|
|
97
|
+
p.add_argument("--no-finalize", action="store_true")
|
|
98
|
+
|
|
99
|
+
a = ap.parse_args(argv)
|
|
100
|
+
|
|
101
|
+
if a.cmd == "download":
|
|
102
|
+
from .data import download
|
|
103
|
+
print(download(a.release, splits=a.split, networks=a.networks, local_dir=a.local_dir))
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if a.cmd == "new":
|
|
107
|
+
from .associator import new_associator
|
|
108
|
+
d = new_associator(a.name, a.root)
|
|
109
|
+
print(f"created {d}/ -- fill in associate() in {d}/associator.py, then: bepad run {d} --split val")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
if a.cmd == "run":
|
|
113
|
+
from .associator import load_associator
|
|
114
|
+
assoc = load_associator(a.associator)
|
|
115
|
+
result = assoc.run(a.split, networks=a.networks, scenarios=a.scenarios, max_windows=a.max_windows,
|
|
116
|
+
workers=a.workers, release=a.release, root=a.release_dir)
|
|
117
|
+
print(result)
|
|
118
|
+
if a.out:
|
|
119
|
+
print(f"[run] saved to {result.save(a.out)}")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if a.cmd in ("submit", "verify"):
|
|
123
|
+
from . import leaderboard as L
|
|
124
|
+
if a.cmd == "submit":
|
|
125
|
+
L.submit(a.result, code_dir=a.code, root=a.release_dir, dry_run=a.dry_run)
|
|
126
|
+
else:
|
|
127
|
+
probs = L.verify(a.result, root=a.release_dir, code_dir=a.code)
|
|
128
|
+
print("OK" if not probs else "Problems:\n - " + "\n - ".join(probs))
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
if a.cmd == "leaderboard":
|
|
132
|
+
from . import leaderboard as L
|
|
133
|
+
if a.action == "build":
|
|
134
|
+
from pathlib import Path
|
|
135
|
+
print(L.build(a.dir, out_md=Path(a.dir) / "README.md", out_csv=Path(a.dir) / "leaderboard.csv",
|
|
136
|
+
out_html=Path(a.dir) / "leaderboard.html"))
|
|
137
|
+
else:
|
|
138
|
+
L.review(root=a.release_dir)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if a.cmd == "evaluate":
|
|
142
|
+
from .metrics import evaluate
|
|
143
|
+
report = evaluate(a.predictions, a.release_dir, split=a.split, networks=a.networks, min_shared=a.min_shared)
|
|
144
|
+
print(report)
|
|
145
|
+
if a.out:
|
|
146
|
+
print(f"[evaluate] report written to {report.save(a.out)}")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
from .build.config import load_pool_config, load_release_config
|
|
150
|
+
|
|
151
|
+
if a.cmd == "pool":
|
|
152
|
+
import json
|
|
153
|
+
|
|
154
|
+
from .build import pool as P
|
|
155
|
+
from .build.labels import LabelModel, fit_label_model
|
|
156
|
+
|
|
157
|
+
cfg = load_pool_config(a.config)
|
|
158
|
+
if a.action == "add":
|
|
159
|
+
for n in a.networks:
|
|
160
|
+
P.build_network(n, cfg, force=a.force)
|
|
161
|
+
elif a.action == "sync":
|
|
162
|
+
todo = P.missing_networks(cfg)
|
|
163
|
+
print(f"[pool] networks not in pool: {todo}")
|
|
164
|
+
if not a.dry_run:
|
|
165
|
+
failed = []
|
|
166
|
+
for n in todo:
|
|
167
|
+
try:
|
|
168
|
+
P.build_network(n, cfg)
|
|
169
|
+
except Exception as exc: # keep going; report at the end
|
|
170
|
+
failed.append(n)
|
|
171
|
+
print(f"[pool] {n} FAILED: {exc!r}", file=sys.stderr)
|
|
172
|
+
if failed:
|
|
173
|
+
print(f"[pool] failed networks: {failed}", file=sys.stderr)
|
|
174
|
+
elif a.action == "remove":
|
|
175
|
+
for n in a.networks:
|
|
176
|
+
P.remove_network(n, cfg)
|
|
177
|
+
elif a.action == "fit-labels":
|
|
178
|
+
path = P.pool_dir(cfg["data_root"]) / "label_model.json"
|
|
179
|
+
prev = json.loads(path.read_text()) if path.exists() else None
|
|
180
|
+
nets = a.networks or P.list_networks(cfg["data_root"])
|
|
181
|
+
fit_label_model(cfg["data_root"], nets, cfg["labels"], previous=prev,
|
|
182
|
+
refit_global=not a.keep_global).save(path)
|
|
183
|
+
print(f"[labels] wrote {path}")
|
|
184
|
+
LabelModel.load(path)
|
|
185
|
+
m = P.read_manifest(cfg["data_root"]) or {"networks": {}}
|
|
186
|
+
for n, e in sorted(m["networks"].items()):
|
|
187
|
+
print(f" {n:<16} {e['n_events']:>8} events {e['n_picks']:>9} picks {e['split_counts']}")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
cfg = load_release_config(a.config)
|
|
191
|
+
if a.release:
|
|
192
|
+
cfg["release"] = a.release
|
|
193
|
+
if a.cmd == "build":
|
|
194
|
+
from .build.release import build_release
|
|
195
|
+
build_release(cfg, networks=a.networks, workers=a.workers, finalize=not a.no_finalize)
|
|
196
|
+
elif a.cmd == "finalize":
|
|
197
|
+
from .build.release import finalize_release
|
|
198
|
+
finalize_release(cfg)
|
|
199
|
+
elif a.cmd == "figures":
|
|
200
|
+
from .build.release import release_dir
|
|
201
|
+
from .plots import release_gallery
|
|
202
|
+
print(release_gallery(release_dir(cfg), cfg, networks=cfg.get("gallery_networks")))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
main()
|
bepad/associator.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""The associator template: fill in one method, get runs, reports and leaderboard submissions.
|
|
2
|
+
|
|
3
|
+
import bepad
|
|
4
|
+
|
|
5
|
+
class MyAssociator(bepad.Associator):
|
|
6
|
+
name = "my-method"
|
|
7
|
+
version = "1.0"
|
|
8
|
+
|
|
9
|
+
def associate(self, picks, stations):
|
|
10
|
+
... # one integer per pick: event number, or -1 for noise
|
|
11
|
+
return labels
|
|
12
|
+
|
|
13
|
+
result = MyAssociator(threshold=0.3).run("test", networks=["tx"])
|
|
14
|
+
result.report # scores
|
|
15
|
+
result.save("results/my-method") # predictions + report + submission.json
|
|
16
|
+
|
|
17
|
+
A contributed associator is a folder (``bepad new <name>`` creates it)::
|
|
18
|
+
|
|
19
|
+
associators/<name>/
|
|
20
|
+
associator.py the class
|
|
21
|
+
config.yaml its parameters (passed to __init__)
|
|
22
|
+
README.md what it is, paper, license
|
|
23
|
+
requirements.txt extra packages it needs
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import datetime as dt
|
|
29
|
+
import importlib.util
|
|
30
|
+
import json
|
|
31
|
+
import platform
|
|
32
|
+
import time
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
import pandas as pd
|
|
37
|
+
|
|
38
|
+
METHOD_TYPES = ("classical", "deep learning", "hybrid")
|
|
39
|
+
TRAINING = ("none", "per network", "all networks")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Associator:
|
|
43
|
+
"""Base class. Subclasses set the metadata attributes and implement :meth:`associate`."""
|
|
44
|
+
|
|
45
|
+
#: short unique name (letters, digits, dashes), used for folders and the leaderboard
|
|
46
|
+
name: str = "unnamed"
|
|
47
|
+
version: str = "0.1"
|
|
48
|
+
authors: str = ""
|
|
49
|
+
paper: str = "" # DOI or URL
|
|
50
|
+
code: str = "" # repository URL
|
|
51
|
+
method_type: str = "classical" # "classical" | "deep learning" | "hybrid"
|
|
52
|
+
training: str = "none" # "none" | "per network" | "all networks"
|
|
53
|
+
description: str = ""
|
|
54
|
+
|
|
55
|
+
def __init__(self, **config):
|
|
56
|
+
nets = config.get("networks") or {}
|
|
57
|
+
if not isinstance(nets, dict) or not all(isinstance(v, dict) for v in nets.values()):
|
|
58
|
+
raise ValueError("config 'networks' must map a network to its settings, e.g. {tx: {p_velocity: 6.0}}")
|
|
59
|
+
self._base_config = config
|
|
60
|
+
self.network = None # the network of the current window (set before each associate call)
|
|
61
|
+
self.path = None # the associator folder (set by load_associator), for model files
|
|
62
|
+
self.config = self.network_config(None)
|
|
63
|
+
|
|
64
|
+
def network_config(self, network):
|
|
65
|
+
"""``config.yaml`` for one network: the general settings, overridden by its ``networks:`` entry."""
|
|
66
|
+
base = {k: v for k, v in self._base_config.items() if k != "networks"}
|
|
67
|
+
return {**base, **(self._base_config.get("networks") or {}).get(network, {})}
|
|
68
|
+
|
|
69
|
+
def use_network(self, network):
|
|
70
|
+
"""Called by BEPAD before each window: sets ``self.network`` and ``self.config``."""
|
|
71
|
+
if network != self.network:
|
|
72
|
+
self.network, self.config = network, self.network_config(network)
|
|
73
|
+
|
|
74
|
+
# --- to implement --------------------------------------------------------------
|
|
75
|
+
def fit(self, train, val):
|
|
76
|
+
"""Optional: learn or tune on the ``train`` / ``val`` splits (never on test)."""
|
|
77
|
+
|
|
78
|
+
def associate(self, picks: pd.DataFrame, stations: pd.DataFrame):
|
|
79
|
+
"""Associate the picks of ONE window.
|
|
80
|
+
|
|
81
|
+
picks: pick_idx, station, phase ("P"/"S"), t_rel_s (s from window start), time (UTC)
|
|
82
|
+
stations: station, latitude, longitude, elevation -- every station of the window
|
|
83
|
+
returns: one integer per pick, in the same order: event number (>= 0) or -1 (noise)
|
|
84
|
+
|
|
85
|
+
Associators that also locate can return ``labels, events`` instead, where
|
|
86
|
+
``events`` has one row per event number: event, latitude, longitude,
|
|
87
|
+
depth_km, t0_rel_s (origin time in seconds from the window start); any of
|
|
88
|
+
the location columns may be left out. Location errors are then reported too.
|
|
89
|
+
"""
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
|
|
92
|
+
# --- provided ------------------------------------------------------------------
|
|
93
|
+
def run(self, split="test", networks=None, scenarios=None, max_windows=None, workers=1, release=None,
|
|
94
|
+
root=None, progress=True):
|
|
95
|
+
"""Run on every window of a split and score it. Returns a :class:`Result`.
|
|
96
|
+
|
|
97
|
+
``workers``: number of CPU processes (windows are independent).
|
|
98
|
+
``scenarios`` / ``max_windows`` run a subset (quick tests); such runs are
|
|
99
|
+
marked partial and are not ranked on the leaderboard.
|
|
100
|
+
"""
|
|
101
|
+
from .data import RELEASE, load
|
|
102
|
+
from .metrics import evaluate
|
|
103
|
+
|
|
104
|
+
data = load(split, networks=networks, labels=False, release=release or RELEASE, root=root)
|
|
105
|
+
w = data.windows
|
|
106
|
+
keep = w
|
|
107
|
+
if scenarios is not None:
|
|
108
|
+
keep = keep[keep.scenario.isin(scenarios)]
|
|
109
|
+
if max_windows is not None:
|
|
110
|
+
keep = keep.groupby("network", group_keys=False).head(max_windows)
|
|
111
|
+
ids = set(keep.window_id)
|
|
112
|
+
todo = [(win.window_id, win.meta["network"], win.picks, win.stations) for win in data if win.window_id in ids]
|
|
113
|
+
rows, locs, secs, t0, n = [], [], [], time.time(), len(todo)
|
|
114
|
+
_RUNNING[0] = self
|
|
115
|
+
if workers > 1:
|
|
116
|
+
import multiprocessing as mp
|
|
117
|
+
pool = mp.get_context("fork").Pool(workers)
|
|
118
|
+
results = pool.imap(_associate_one, todo, chunksize=2)
|
|
119
|
+
else:
|
|
120
|
+
pool, results = None, map(_associate_one, todo)
|
|
121
|
+
try:
|
|
122
|
+
for k, (df, loc, sec) in enumerate(results):
|
|
123
|
+
rows.append(df)
|
|
124
|
+
secs.append(sec)
|
|
125
|
+
if loc is not None:
|
|
126
|
+
locs.append(loc)
|
|
127
|
+
if progress and (k + 1) % 50 == 0:
|
|
128
|
+
print(f"[{self.name}] {k + 1}/{n} windows, {time.time() - t0:.0f} s")
|
|
129
|
+
finally:
|
|
130
|
+
if pool is not None:
|
|
131
|
+
pool.close()
|
|
132
|
+
pool.join()
|
|
133
|
+
runtime = time.time() - t0
|
|
134
|
+
predictions = pd.concat(rows, ignore_index=True)
|
|
135
|
+
locations = pd.concat(locs, ignore_index=True) if locs else None
|
|
136
|
+
report = evaluate(predictions, data, window_ids=ids, locations=locations)
|
|
137
|
+
meta = {
|
|
138
|
+
"split": split, "release": data.release.path.name,
|
|
139
|
+
"networks": sorted(keep.network.unique()),
|
|
140
|
+
"n_windows": n, "complete": n == len(w),
|
|
141
|
+
"scenarios": "all" if scenarios is None else list(scenarios),
|
|
142
|
+
"workers": workers, "runtime_s": round(runtime, 2),
|
|
143
|
+
"seconds_per_window": round(float(np.mean(secs)), 4) if secs else None, # time inside associate()
|
|
144
|
+
"hardware": f"{platform.processor() or platform.machine()} ({platform.system()})",
|
|
145
|
+
}
|
|
146
|
+
return Result(self, predictions, report, meta, locations)
|
|
147
|
+
|
|
148
|
+
def metadata(self):
|
|
149
|
+
if self.method_type not in METHOD_TYPES:
|
|
150
|
+
raise ValueError(f"method_type must be one of {METHOD_TYPES}")
|
|
151
|
+
if self.training not in TRAINING:
|
|
152
|
+
raise ValueError(f"training must be one of {TRAINING}")
|
|
153
|
+
return {"name": self.name, "version": self.version, "authors": self.authors, "paper": self.paper,
|
|
154
|
+
"code": self.code, "method_type": self.method_type, "training": self.training,
|
|
155
|
+
"description": self.description, "config": self._base_config}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
_RUNNING = [None] # the associator being run (inherited by forked workers)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _associate_one(item):
|
|
162
|
+
window_id, network, picks, stations = item
|
|
163
|
+
assoc = _RUNNING[0]
|
|
164
|
+
assoc.use_network(network)
|
|
165
|
+
tic = time.perf_counter()
|
|
166
|
+
out = assoc.associate(picks, stations)
|
|
167
|
+
elapsed = time.perf_counter() - tic
|
|
168
|
+
loc = None
|
|
169
|
+
if isinstance(out, tuple):
|
|
170
|
+
out, loc = out
|
|
171
|
+
loc = pd.DataFrame(loc).rename(columns={"pred_event": "event"})
|
|
172
|
+
if "event" not in loc:
|
|
173
|
+
raise ValueError(f"{assoc.name}: the events table needs an 'event' column (the event numbers)")
|
|
174
|
+
keep = ["event"] + [c for c in ("latitude", "longitude", "depth_km", "t0_rel_s") if c in loc]
|
|
175
|
+
loc = loc[keep].assign(window_id=window_id)
|
|
176
|
+
labels = np.asarray(out)
|
|
177
|
+
if labels.shape != (len(picks),) or not np.issubdtype(labels.dtype, np.integer):
|
|
178
|
+
raise ValueError(f"{assoc.name}.associate must return {len(picks)} integers for {window_id}, "
|
|
179
|
+
f"got shape {labels.shape} dtype {labels.dtype}")
|
|
180
|
+
return pd.DataFrame({"window_id": window_id, "pick_idx": picks.pick_idx.to_numpy(),
|
|
181
|
+
"pred_event": labels.astype(np.int64)}), loc, elapsed
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Result:
|
|
185
|
+
"""Predictions + report of one run. ``save`` writes everything a submission needs."""
|
|
186
|
+
|
|
187
|
+
def __init__(self, associator, predictions, report, meta, locations=None):
|
|
188
|
+
self.associator, self.predictions, self.report, self.meta = associator, predictions, report, meta
|
|
189
|
+
self.locations = locations # one row per predicted event (only if the associator locates)
|
|
190
|
+
|
|
191
|
+
def __repr__(self):
|
|
192
|
+
return f"<bepad.Result {self.associator.name} {self.meta['split']} {self.meta['n_windows']} windows>\n" \
|
|
193
|
+
+ repr(self.report).split("\n", 1)[1]
|
|
194
|
+
|
|
195
|
+
def submission(self):
|
|
196
|
+
"""The leaderboard record (a dict, saved as submission.json)."""
|
|
197
|
+
from . import __version__
|
|
198
|
+
|
|
199
|
+
r = self.report
|
|
200
|
+
cols = ["event_precision", "event_recall", "event_f1", "pick_precision", "pick_recall", "pick_f1",
|
|
201
|
+
"noise_rejection", "split_rate", "merge_rate", "ari_mean", "n_windows"]
|
|
202
|
+
cols += [c for c in ("located_frac", "epi_median_km", "epi_p90_km", "depth_median_km", "depth_p90_km",
|
|
203
|
+
"t0_median_s", "t0_p90_s") if c in r.overall]
|
|
204
|
+
by_net = r.by_network.set_index("network")[cols]
|
|
205
|
+
return {
|
|
206
|
+
"bepad_version": __version__,
|
|
207
|
+
"created": dt.datetime.now().isoformat(timespec="seconds"),
|
|
208
|
+
"associator": self.associator.metadata(),
|
|
209
|
+
"run": self.meta,
|
|
210
|
+
"scores": {"overall": {k: _num(v) for k, v in r.overall.iloc[0][cols].items()},
|
|
211
|
+
"by_network": {n: {k: _num(v) for k, v in row.items()} for n, row in by_net.iterrows()}},
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def save(self, out_dir):
|
|
215
|
+
"""Write ``submission.json``, ``predictions/network=<name>.parquet``, ``locations/`` (if any)
|
|
216
|
+
and the report tables."""
|
|
217
|
+
out = Path(out_dir)
|
|
218
|
+
(out / "predictions").mkdir(parents=True, exist_ok=True)
|
|
219
|
+
net = self.predictions.window_id.str.split(".", n=1).str[0]
|
|
220
|
+
for n, df in self.predictions.groupby(net):
|
|
221
|
+
df.to_parquet(out / "predictions" / f"network={n}.parquet", index=False, compression="zstd")
|
|
222
|
+
if self.locations is not None:
|
|
223
|
+
(out / "locations").mkdir(exist_ok=True)
|
|
224
|
+
lnet = self.locations.window_id.str.split(".", n=1).str[0]
|
|
225
|
+
for n, df in self.locations.groupby(lnet):
|
|
226
|
+
df.to_parquet(out / "locations" / f"network={n}.parquet", index=False)
|
|
227
|
+
self.report.save(out / "report")
|
|
228
|
+
(out / "submission.json").write_text(json.dumps(self.submission(), indent=2))
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _num(v):
|
|
233
|
+
v = float(v)
|
|
234
|
+
return None if v != v else round(v, 6)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ----------------------------------------------------------------------
|
|
238
|
+
def load_associator(path, **overrides):
|
|
239
|
+
"""Load an associator folder (or .py file): its Associator subclass + config.yaml."""
|
|
240
|
+
import yaml
|
|
241
|
+
|
|
242
|
+
path = Path(path)
|
|
243
|
+
file = path / "associator.py" if path.is_dir() else path
|
|
244
|
+
spec = importlib.util.spec_from_file_location(f"bepad_assoc_{file.parent.name}", file)
|
|
245
|
+
mod = importlib.util.module_from_spec(spec)
|
|
246
|
+
spec.loader.exec_module(mod)
|
|
247
|
+
classes = [c for c in vars(mod).values()
|
|
248
|
+
if isinstance(c, type) and issubclass(c, Associator) and c is not Associator]
|
|
249
|
+
if len(classes) != 1:
|
|
250
|
+
raise ValueError(f"{file} must define exactly one bepad.Associator subclass, found {len(classes)}")
|
|
251
|
+
cfg_file = file.parent / "config.yaml"
|
|
252
|
+
config = (yaml.safe_load(cfg_file.read_text()) or {}) if cfg_file.exists() else {}
|
|
253
|
+
obj = classes[0](**{**config, **overrides})
|
|
254
|
+
obj.path = file.parent
|
|
255
|
+
return obj
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
TEMPLATE = {
|
|
259
|
+
"associator.py": '''"""{name}: a BEPAD associator.
|
|
260
|
+
|
|
261
|
+
Fill in ``associate`` (required) and ``fit`` (optional), set the metadata, then:
|
|
262
|
+
bepad run associators/{name} --split val # tune
|
|
263
|
+
bepad run associators/{name} --split test --out results/{name}
|
|
264
|
+
bepad submit results/{name}
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
import numpy as np
|
|
268
|
+
|
|
269
|
+
import bepad
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class Associator(bepad.Associator):
|
|
273
|
+
name = "{name}"
|
|
274
|
+
version = "0.1"
|
|
275
|
+
authors = "Your Name"
|
|
276
|
+
paper = "" # DOI or URL of the paper describing the method
|
|
277
|
+
code = "" # URL of the original code, if any
|
|
278
|
+
method_type = "classical" # "classical" | "deep learning" | "hybrid"
|
|
279
|
+
training = "none" # "none" | "per network" | "all networks"
|
|
280
|
+
description = "One sentence: what the method does."
|
|
281
|
+
|
|
282
|
+
def fit(self, train, val):
|
|
283
|
+
"""Optional: learn or tune on train / val. Parameters are in self.config."""
|
|
284
|
+
|
|
285
|
+
def associate(self, picks, stations):
|
|
286
|
+
"""One window.
|
|
287
|
+
|
|
288
|
+
picks: pick_idx, station, phase ("P"/"S"), t_rel_s (s from window start), time (UTC)
|
|
289
|
+
stations: station, latitude, longitude, elevation
|
|
290
|
+
return: one integer per pick (same order): event number, or -1 for noise
|
|
291
|
+
|
|
292
|
+
self.network is the window's network (e.g. "tx") and self.config its settings
|
|
293
|
+
(config.yaml, with that network's overrides). Files of the folder: self.path / "models/...".
|
|
294
|
+
"""
|
|
295
|
+
labels = np.full(len(picks), -1) # <- replace with your method
|
|
296
|
+
return labels
|
|
297
|
+
''',
|
|
298
|
+
"config.yaml": '''# Parameters of {name}; passed to the class as self.config.
|
|
299
|
+
# Every value here is saved with the results, so others can reproduce them.
|
|
300
|
+
example_threshold: 0.5
|
|
301
|
+
|
|
302
|
+
# Optional: settings for some networks, overriding the ones above (e.g. a 1D velocity model).
|
|
303
|
+
# Model files go in models/velocity/ (velocity models) and models/weights/ (trained models).
|
|
304
|
+
# networks:
|
|
305
|
+
# tx: {{velocity_model: models/velocity/tx_1d.csv}}
|
|
306
|
+
# JMA: {{velocity_model: models/velocity/jma2001.csv}}
|
|
307
|
+
''',
|
|
308
|
+
"README.md": '''# {name}
|
|
309
|
+
|
|
310
|
+
What the method does, in two or three sentences.
|
|
311
|
+
|
|
312
|
+
- **Paper:** <DOI or URL>
|
|
313
|
+
- **Original code:** <URL>
|
|
314
|
+
- **License:** <license of this code>
|
|
315
|
+
- **Requirements:** see `requirements.txt`
|
|
316
|
+
|
|
317
|
+
## Parameters
|
|
318
|
+
|
|
319
|
+
| Parameter | Meaning |
|
|
320
|
+
|---|---|
|
|
321
|
+
| `example_threshold` | ... |
|
|
322
|
+
''',
|
|
323
|
+
"requirements.txt": "# extra packages this associator needs, one per line\n",
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def new_associator(name, root="associators"):
|
|
328
|
+
"""Create ``<root>/<name>/`` from the template."""
|
|
329
|
+
import re
|
|
330
|
+
|
|
331
|
+
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]*", name):
|
|
332
|
+
raise ValueError("name: lowercase letters, digits, dashes and underscores (e.g. 'my-method')")
|
|
333
|
+
d = Path(root) / name
|
|
334
|
+
if d.exists():
|
|
335
|
+
raise FileExistsError(f"{d} already exists")
|
|
336
|
+
d.mkdir(parents=True)
|
|
337
|
+
for fname, text in TEMPLATE.items():
|
|
338
|
+
(d / fname).write_text(text.format(name=name))
|
|
339
|
+
return d
|
bepad/build/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Building BEPAD (maintainers only).
|
|
2
|
+
|
|
3
|
+
Users never need this subpackage: they download the finished dataset with
|
|
4
|
+
``bepad.load``. The build runs in four steps:
|
|
5
|
+
|
|
6
|
+
pool.py UTDQuake network -> quality-controlled events, picks, stations
|
|
7
|
+
labels.py fit the label model: detectability q(M, r), timing sigma(r),
|
|
8
|
+
per-network travel-time curves and S/P ratios
|
|
9
|
+
windows.py pool + label model + seed -> one window
|
|
10
|
+
release.py thousands of windows per network/split/scenario -> Parquet
|
|
11
|
+
"""
|