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 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
@@ -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
+ """