bepad 0.1.0__tar.gz → 0.1.2__tar.gz
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-0.1.0 → bepad-0.1.2}/PKG-INFO +8 -5
- {bepad-0.1.0 → bepad-0.1.2}/README.md +6 -4
- {bepad-0.1.0 → bepad-0.1.2}/bepad/__init__.py +1 -1
- {bepad-0.1.0 → bepad-0.1.2}/bepad/associator.py +11 -14
- bepad-0.1.2/bepad/blocks.py +170 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/build/release.py +11 -1
- {bepad-0.1.0 → bepad-0.1.2}/bepad/data.py +111 -11
- {bepad-0.1.0 → bepad-0.1.2}/bepad/metrics.py +5 -7
- {bepad-0.1.0 → bepad-0.1.2}/bepad/plots.py +14 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/torch.py +29 -12
- {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/PKG-INFO +8 -5
- {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/SOURCES.txt +1 -0
- {bepad-0.1.0 → bepad-0.1.2}/pyproject.toml +2 -1
- {bepad-0.1.0 → bepad-0.1.2}/LICENSE +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/__main__.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/build/__init__.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/build/config.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/build/geo.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/build/labels.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/build/pool.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/build/utdquake.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/build/windows.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/leaderboard.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad/stats.py +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/dependency_links.txt +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/entry_points.txt +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/requires.txt +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/top_level.txt +0 -0
- {bepad-0.1.0 → bepad-0.1.2}/setup.cfg +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bepad
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: BEPAD: a benchmark dataset for seismic phase association built from UTDQuake
|
|
5
5
|
License-Expression: GPL-3.0-or-later
|
|
6
|
+
Project-URL: Documentation, https://bepad.readthedocs.io
|
|
6
7
|
Project-URL: Repository, https://github.com/ecastillot/BEPAD
|
|
7
8
|
Project-URL: Dataset, https://huggingface.co/datasets/ecastillot/BEPAD
|
|
8
9
|
Requires-Python: >=3.10
|
|
@@ -30,6 +31,8 @@ Dynamic: license-file
|
|
|
30
31
|
|
|
31
32
|
# BEPAD — a benchmark dataset for seismic phase association
|
|
32
33
|
|
|
34
|
+
[](https://pypi.org/project/bepad/) [](https://huggingface.co/datasets/ecastillot/BEPAD/tree/main/v0.1) [](https://huggingface.co/datasets/ecastillot/BEPAD) [](https://bepad.readthedocs.io/en/latest/) [](https://github.com/ecastillot/BEPAD)
|
|
35
|
+
|
|
33
36
|
A phase associator groups the P and S picks of a seismic network into earthquakes. BEPAD gives
|
|
34
37
|
thousands of association problems built from **real earthquakes and real picks**
|
|
35
38
|
([UTDQuake](https://huggingface.co/datasets/ecastillot/UTDQuake)), with the answer for every pick.
|
|
@@ -37,12 +40,12 @@ thousands of association problems built from **real earthquakes and real picks**
|
|
|
37
40
|

|
|
38
41
|
|
|
39
42
|
*One window (Texas network): (a) earthquakes and stations, (b) every pick at its distance from
|
|
40
|
-
its earthquake, (c) picks by type.*
|
|
43
|
+
its earthquake, (c) picks by type. The associator's job is to find which picks belong together.*
|
|
41
44
|
|
|
42
45
|
```python
|
|
43
46
|
import bepad
|
|
44
47
|
|
|
45
|
-
test = bepad.load("test", labels=False)
|
|
48
|
+
test = bepad.load("test", networks=["tx"], max_windows=20, labels=False) # 20 Texas test windows
|
|
46
49
|
for w in test: # one window = one association problem
|
|
47
50
|
w.picks, w.stations # picks (station, P/S, time) and station coordinates
|
|
48
51
|
report = bepad.evaluate(predictions, test) # score your associator
|
|
@@ -64,8 +67,8 @@ bepad submit results/my-method --code associators/my-method # opens a Hugging
|
|
|
64
67
|
| **Associator authors** | fill in `bepad.Associator`, run on `test`, submit to the leaderboard — see *Contribute your associator* |
|
|
65
68
|
| **Deep learning** | `bepad.torch.ReleaseWindowDataset` — see *Train a model* |
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
this folder; `.[dl]` for PyTorch, `.[build]` to rebuild the dataset).
|
|
70
|
+
Documentation: https://bepad.readthedocs.io · Data: https://huggingface.co/datasets/ecastillot/BEPAD · Code: https://github.com/ecastillot/BEPAD.
|
|
71
|
+
Install: `pip install bepad` (or `pip install -e .` from this folder; `.[dl]` for PyTorch, `.[build]` to rebuild the dataset).
|
|
69
72
|
|
|
70
73
|
## Repository
|
|
71
74
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# BEPAD — a benchmark dataset for seismic phase association
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/bepad/) [](https://huggingface.co/datasets/ecastillot/BEPAD/tree/main/v0.1) [](https://huggingface.co/datasets/ecastillot/BEPAD) [](https://bepad.readthedocs.io/en/latest/) [](https://github.com/ecastillot/BEPAD)
|
|
4
|
+
|
|
3
5
|
A phase associator groups the P and S picks of a seismic network into earthquakes. BEPAD gives
|
|
4
6
|
thousands of association problems built from **real earthquakes and real picks**
|
|
5
7
|
([UTDQuake](https://huggingface.co/datasets/ecastillot/UTDQuake)), with the answer for every pick.
|
|
@@ -7,12 +9,12 @@ thousands of association problems built from **real earthquakes and real picks**
|
|
|
7
9
|

|
|
8
10
|
|
|
9
11
|
*One window (Texas network): (a) earthquakes and stations, (b) every pick at its distance from
|
|
10
|
-
its earthquake, (c) picks by type.*
|
|
12
|
+
its earthquake, (c) picks by type. The associator's job is to find which picks belong together.*
|
|
11
13
|
|
|
12
14
|
```python
|
|
13
15
|
import bepad
|
|
14
16
|
|
|
15
|
-
test = bepad.load("test", labels=False)
|
|
17
|
+
test = bepad.load("test", networks=["tx"], max_windows=20, labels=False) # 20 Texas test windows
|
|
16
18
|
for w in test: # one window = one association problem
|
|
17
19
|
w.picks, w.stations # picks (station, P/S, time) and station coordinates
|
|
18
20
|
report = bepad.evaluate(predictions, test) # score your associator
|
|
@@ -34,8 +36,8 @@ bepad submit results/my-method --code associators/my-method # opens a Hugging
|
|
|
34
36
|
| **Associator authors** | fill in `bepad.Associator`, run on `test`, submit to the leaderboard — see *Contribute your associator* |
|
|
35
37
|
| **Deep learning** | `bepad.torch.ReleaseWindowDataset` — see *Train a model* |
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
this folder; `.[dl]` for PyTorch, `.[build]` to rebuild the dataset).
|
|
39
|
+
Documentation: https://bepad.readthedocs.io · Data: https://huggingface.co/datasets/ecastillot/BEPAD · Code: https://github.com/ecastillot/BEPAD.
|
|
40
|
+
Install: `pip install bepad` (or `pip install -e .` from this folder; `.[dl]` for PyTorch, `.[build]` to rebuild the dataset).
|
|
39
41
|
|
|
40
42
|
## Repository
|
|
41
43
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
class MyMethod(bepad.Associator): ... # template for reusable associators
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "0.1.
|
|
9
|
+
__version__ = "0.1.2"
|
|
10
10
|
|
|
11
11
|
from .data import CORE_NAME, CORE_NETWORKS, LABEL_COLUMNS, Release, Split, download, load, open, predictions_frame # noqa: E402,A004
|
|
12
12
|
from .metrics import evaluate # noqa: E402
|
|
@@ -90,26 +90,23 @@ class Associator:
|
|
|
90
90
|
raise NotImplementedError
|
|
91
91
|
|
|
92
92
|
# --- provided ------------------------------------------------------------------
|
|
93
|
-
def run(self, split="test", networks=None, scenarios=None, max_windows=None,
|
|
94
|
-
root=None, progress=True):
|
|
93
|
+
def run(self, split="test", networks=None, scenarios=None, max_windows=None, window_ids=None, workers=1,
|
|
94
|
+
release=None, root=None, progress=True):
|
|
95
95
|
"""Run on every window of a split and score it. Returns a :class:`Result`.
|
|
96
96
|
|
|
97
97
|
``workers``: number of CPU processes (windows are independent).
|
|
98
|
-
``scenarios`` / ``max_windows`` run a subset (quick tests
|
|
99
|
-
marked partial and are not ranked on the leaderboard.
|
|
98
|
+
``scenarios`` / ``max_windows`` / ``window_ids`` run a subset (quick tests, as in
|
|
99
|
+
:func:`bepad.load`); such runs are marked partial and are not ranked on the leaderboard.
|
|
100
100
|
"""
|
|
101
101
|
from .data import RELEASE, load
|
|
102
102
|
from .metrics import evaluate
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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)
|
|
104
|
+
subset = scenarios is not None or max_windows is not None or window_ids is not None
|
|
105
|
+
data = load(split, networks=networks, labels=False, release=release or RELEASE, root=root,
|
|
106
|
+
scenarios=scenarios, max_windows=max_windows, window_ids=window_ids)
|
|
107
|
+
keep = data.windows
|
|
111
108
|
ids = set(keep.window_id)
|
|
112
|
-
todo = [(win.window_id, win.meta["network"], win.picks, win.stations) for win in data
|
|
109
|
+
todo = [(win.window_id, win.meta["network"], win.picks, win.stations) for win in data]
|
|
113
110
|
rows, locs, secs, t0, n = [], [], [], time.time(), len(todo)
|
|
114
111
|
_RUNNING[0] = self
|
|
115
112
|
if workers > 1:
|
|
@@ -137,8 +134,8 @@ class Associator:
|
|
|
137
134
|
meta = {
|
|
138
135
|
"split": split, "release": data.release.path.name,
|
|
139
136
|
"networks": sorted(keep.network.unique()),
|
|
140
|
-
"n_windows": n, "complete":
|
|
141
|
-
"scenarios": "all" if scenarios is None else list(scenarios),
|
|
137
|
+
"n_windows": n, "complete": not subset,
|
|
138
|
+
"scenarios": "all" if scenarios is None else ([scenarios] if isinstance(scenarios, str) else list(scenarios)),
|
|
142
139
|
"workers": workers, "runtime_s": round(runtime, 2),
|
|
143
140
|
"seconds_per_window": round(float(np.mean(secs)), 4) if secs else None, # time inside associate()
|
|
144
141
|
"hardware": f"{platform.processor() or platform.machine()} ({platform.system()})",
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Release files stored in blocks, so one window can be read without reading the whole file.
|
|
2
|
+
|
|
3
|
+
The picks, events and window_stations tables of a release are sorted by ``window_id`` (keeping the
|
|
4
|
+
order of the rows inside each window) and written in row groups ("blocks") of about
|
|
5
|
+
:data:`BLOCK_ROWS` rows (per table), compressed with zstd. A block holds whole windows only. Parquet keeps the first and last
|
|
6
|
+
``window_id`` of every block, so finding a window needs no separate index. The windows table keeps
|
|
7
|
+
its own order (it is small and read whole).
|
|
8
|
+
|
|
9
|
+
Files in the older layout (not sorted) still work: they are read whole.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import bisect
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
# Rows per block. Smaller blocks read one window faster but compress less; window_stations has two
|
|
22
|
+
# short columns, so its blocks are larger (measured on tx and R07: about the old file sizes).
|
|
23
|
+
BLOCK_ROWS = {"picks": 5_000, "events": 5_000, "window_stations": 20_000}
|
|
24
|
+
COMPRESSION = "zstd"
|
|
25
|
+
BLOCKED_TABLES = ("picks", "events", "window_stations")
|
|
26
|
+
LAYOUT = "blocks-v1" # recorded in manifest.json
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def write_blocked(df, path, block_rows=5_000):
|
|
30
|
+
"""Write ``df`` sorted by window_id, in blocks of about ``block_rows`` rows of whole windows."""
|
|
31
|
+
import pyarrow as pa
|
|
32
|
+
import pyarrow.parquet as pq
|
|
33
|
+
|
|
34
|
+
path = Path(path)
|
|
35
|
+
df = df.sort_values("window_id", kind="stable", ignore_index=True)
|
|
36
|
+
ids = df["window_id"].to_numpy()
|
|
37
|
+
starts = np.flatnonzero(np.r_[True, ids[1:] != ids[:-1]]) if len(ids) else np.array([], int)
|
|
38
|
+
block = starts // block_rows # block of each window, by its first row
|
|
39
|
+
bounds = list(starts[np.r_[True, block[1:] != block[:-1]]]) + [len(df)] if len(ids) else [0, 0]
|
|
40
|
+
schema = pa.Schema.from_pandas(df, preserve_index=False)
|
|
41
|
+
tmp = path.with_suffix(".tmp")
|
|
42
|
+
with pq.ParquetWriter(tmp, schema, compression=COMPRESSION) as w:
|
|
43
|
+
for a, b in zip(bounds[:-1], bounds[1:]):
|
|
44
|
+
w.write_table(pa.Table.from_pandas(df.iloc[a:b], schema=schema, preserve_index=False),
|
|
45
|
+
row_group_size=max(b - a, 1))
|
|
46
|
+
os.replace(tmp, path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _bounds(md):
|
|
50
|
+
"""(first, last) window_id of every row group, or None without statistics."""
|
|
51
|
+
col = md.schema.to_arrow_schema().get_field_index("window_id")
|
|
52
|
+
out = []
|
|
53
|
+
for i in range(md.num_row_groups):
|
|
54
|
+
st = md.row_group(i).column(col).statistics
|
|
55
|
+
if st is None or not st.has_min_max:
|
|
56
|
+
return None
|
|
57
|
+
out.append((st.min, st.max))
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def block_bounds(pf):
|
|
62
|
+
"""(first, last) window_id per block if the file is in blocks (sorted, no window across two
|
|
63
|
+
blocks), else None."""
|
|
64
|
+
b = _bounds(pf.metadata)
|
|
65
|
+
if b is None or any(b[i][1] >= b[i + 1][0] for i in range(len(b) - 1)):
|
|
66
|
+
return None
|
|
67
|
+
return b
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_blocked(path):
|
|
71
|
+
"""Problems of one file (empty list = in blocks): sorted rows, whole windows per block."""
|
|
72
|
+
import pyarrow.parquet as pq
|
|
73
|
+
|
|
74
|
+
pf = pq.ParquetFile(path)
|
|
75
|
+
if pf.metadata.num_rows == 0:
|
|
76
|
+
return []
|
|
77
|
+
problems = []
|
|
78
|
+
ids = pf.read(columns=["window_id"]).column("window_id").to_numpy(zero_copy_only=False)
|
|
79
|
+
if not (ids[1:] >= ids[:-1]).all():
|
|
80
|
+
problems.append(f"{path}: rows are not sorted by window_id")
|
|
81
|
+
if block_bounds(pf) is None:
|
|
82
|
+
problems.append(f"{path}: a window is split across two blocks (or no statistics)")
|
|
83
|
+
return problems
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def read_rows(path, window_ids, columns=None, batch_size=200_000):
|
|
87
|
+
"""Rows of one file whose window is in ``window_ids``: only the blocks that hold them are read
|
|
88
|
+
(older files: read in batches, keeping the matching rows)."""
|
|
89
|
+
import pyarrow as pa
|
|
90
|
+
import pyarrow.compute as pc
|
|
91
|
+
import pyarrow.parquet as pq
|
|
92
|
+
|
|
93
|
+
pf = pq.ParquetFile(path, pre_buffer=False, buffer_size=1 << 20)
|
|
94
|
+
schema = pa.schema([pf.schema_arrow.field(c) for c in columns]) if columns else pf.schema_arrow
|
|
95
|
+
ids = pa.array(sorted(window_ids), type=schema.field("window_id").type)
|
|
96
|
+
b = block_bounds(pf)
|
|
97
|
+
if b is not None:
|
|
98
|
+
lo = [x[0] for x in b]
|
|
99
|
+
groups = sorted({g for w in ids.to_pylist() if (g := bisect.bisect_right(lo, w) - 1) >= 0 and w <= b[g][1]})
|
|
100
|
+
batches = pf.iter_batches(batch_size=batch_size, row_groups=groups, columns=columns) if groups else []
|
|
101
|
+
else:
|
|
102
|
+
batches = pf.iter_batches(batch_size=batch_size, columns=columns)
|
|
103
|
+
parts = [x.filter(pc.is_in(x.column("window_id"), value_set=ids)) for x in batches]
|
|
104
|
+
return pa.Table.from_batches(parts, schema=schema).to_pandas()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class BlockReader:
|
|
108
|
+
"""One file, read one window at a time. Keeps the last block in memory, so windows read in
|
|
109
|
+
order cost one block read per block. Safe in forked DataLoader workers (reopens per process).
|
|
110
|
+
An older file (not in blocks) is read whole once."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, path, columns=None):
|
|
113
|
+
self.path, self.columns = Path(path), columns
|
|
114
|
+
self._pid = None
|
|
115
|
+
|
|
116
|
+
def __getstate__(self): # pickled DataLoader workers reopen the file
|
|
117
|
+
return {"path": self.path, "columns": self.columns, "_pid": None}
|
|
118
|
+
|
|
119
|
+
def _open(self):
|
|
120
|
+
import pyarrow.parquet as pq
|
|
121
|
+
|
|
122
|
+
self._pf = pq.ParquetFile(self.path)
|
|
123
|
+
b = block_bounds(self._pf)
|
|
124
|
+
self._lo = None if b is None else [x[0] for x in b]
|
|
125
|
+
self._hi = None if b is None else [x[1] for x in b]
|
|
126
|
+
self._key, self._df, self._rows = None, None, {}
|
|
127
|
+
self._pid = os.getpid()
|
|
128
|
+
|
|
129
|
+
def _load(self, key):
|
|
130
|
+
if key != self._key:
|
|
131
|
+
t = self._pf.read(columns=self.columns) if key == "all" else \
|
|
132
|
+
self._pf.read_row_group(key, columns=self.columns)
|
|
133
|
+
self._df = t.to_pandas()
|
|
134
|
+
self._rows = self._df.groupby("window_id", sort=False).indices
|
|
135
|
+
self._key = key
|
|
136
|
+
|
|
137
|
+
def window(self, wid):
|
|
138
|
+
"""The rows of window ``wid`` (an empty frame if it has none)."""
|
|
139
|
+
if self._pid != os.getpid():
|
|
140
|
+
self._open()
|
|
141
|
+
if self._lo is None:
|
|
142
|
+
self._load("all")
|
|
143
|
+
else:
|
|
144
|
+
g = bisect.bisect_right(self._lo, wid) - 1
|
|
145
|
+
if g < 0 or wid > self._hi[g]:
|
|
146
|
+
return self._empty()
|
|
147
|
+
self._load(g)
|
|
148
|
+
rows = self._rows.get(wid)
|
|
149
|
+
return self._df.iloc[rows].reset_index(drop=True) if rows is not None else self._empty()
|
|
150
|
+
|
|
151
|
+
def _empty(self):
|
|
152
|
+
if self._df is None:
|
|
153
|
+
self._load(0 if self._lo else "all")
|
|
154
|
+
return self._df.iloc[:0].reset_index(drop=True)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def reblock(release_dir, networks=None, splits=None, block_rows=None):
|
|
158
|
+
"""Rewrite the blocked tables of an existing release in blocks (same rows, same content)."""
|
|
159
|
+
release_dir = Path(release_dir)
|
|
160
|
+
done = []
|
|
161
|
+
for split_dir in sorted(p for p in release_dir.iterdir() if (p / "windows").is_dir()):
|
|
162
|
+
if splits is not None and split_dir.name not in splits:
|
|
163
|
+
continue
|
|
164
|
+
for table in BLOCKED_TABLES:
|
|
165
|
+
for f in sorted((split_dir / table).glob("network=*.parquet")):
|
|
166
|
+
if networks is not None and f.stem.split("=", 1)[1] not in networks:
|
|
167
|
+
continue
|
|
168
|
+
write_blocked(pd.read_parquet(f), f, block_rows or BLOCK_ROWS[table])
|
|
169
|
+
done.append(f)
|
|
170
|
+
return done
|
|
@@ -8,6 +8,7 @@ Hugging Face dataset configs/splits:
|
|
|
8
8
|
<split>/picks/network=<name>.parquet one row per pick (label: event_idx, -1 noise)
|
|
9
9
|
<split>/events/network=<name>.parquet one row per event in a window
|
|
10
10
|
<split>/window_stations/network=<name>.parquet stations available in each window
|
|
11
|
+
(picks, events and window_stations are sorted by window_id and stored in blocks: bepad.blocks)
|
|
11
12
|
_parts/<name>.json per-network build record
|
|
12
13
|
--- written by finalize_release ---
|
|
13
14
|
stations.parquet station coordinates
|
|
@@ -38,6 +39,7 @@ from pathlib import Path
|
|
|
38
39
|
import pandas as pd
|
|
39
40
|
|
|
40
41
|
from .. import __version__
|
|
42
|
+
from ..blocks import BLOCK_ROWS, BLOCKED_TABLES, COMPRESSION, LAYOUT, check_blocked, write_blocked
|
|
41
43
|
from .config import REPO_ROOT, WINDOW_DEFAULTS
|
|
42
44
|
from .geo import stable_seed
|
|
43
45
|
from .labels import LabelModel
|
|
@@ -138,7 +140,10 @@ def _build_network(network, cfg, scenarios, workers):
|
|
|
138
140
|
for t, df in frames.items():
|
|
139
141
|
d = out / split / t
|
|
140
142
|
d.mkdir(parents=True, exist_ok=True)
|
|
141
|
-
|
|
143
|
+
if t in BLOCKED_TABLES: # one window can be read without the whole file
|
|
144
|
+
write_blocked(df, d / f"network={network}.parquet", BLOCK_ROWS[t])
|
|
145
|
+
else:
|
|
146
|
+
df.to_parquet(d / f"network={network}.parquet", index=False)
|
|
142
147
|
|
|
143
148
|
w, e = frames["windows"], frames["events"]
|
|
144
149
|
uses = e["src_event_id"].value_counts()
|
|
@@ -193,6 +198,10 @@ def finalize_release(cfg):
|
|
|
193
198
|
networks = sorted(parts)
|
|
194
199
|
if not networks:
|
|
195
200
|
raise RuntimeError(f"nothing to finalize: no built networks in {out / '_parts'}")
|
|
201
|
+
problems = [q for t in BLOCKED_TABLES for f in sorted(out.glob(f"*/{t}/network=*.parquet"))
|
|
202
|
+
for q in check_blocked(f)]
|
|
203
|
+
if problems:
|
|
204
|
+
raise RuntimeError("files not in blocks (rebuild them, or bepad.blocks.reblock):\n " + "\n ".join(problems))
|
|
196
205
|
|
|
197
206
|
pd.concat([load_network(cfg["data_root"], n)[2] for n in networks], ignore_index=True) \
|
|
198
207
|
.to_parquet(out / "stations.parquet", index=False)
|
|
@@ -229,6 +238,7 @@ def finalize_release(cfg):
|
|
|
229
238
|
"source_sha256", "n_events", "built")}
|
|
230
239
|
for n in networks},
|
|
231
240
|
"counts": counts,
|
|
241
|
+
"layout": {"name": LAYOUT, "block_rows": BLOCK_ROWS, "compression": COMPRESSION},
|
|
232
242
|
"skipped_splits": {n: p["skipped_splits"] for n, p in parts.items() if p["skipped_splits"]},
|
|
233
243
|
}
|
|
234
244
|
with open(out / "manifest.json", "w") as f:
|
|
@@ -16,6 +16,8 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
import pandas as pd
|
|
18
18
|
|
|
19
|
+
from .blocks import BlockReader, read_rows
|
|
20
|
+
|
|
19
21
|
#: Columns of picks.parquet that carry ground truth. Never give them to an associator.
|
|
20
22
|
LABEL_COLUMNS = ("event_idx", "pick_type", "real", "prob", "sigma_s", "in_scorable_event",
|
|
21
23
|
"src_event_id", "src_pick_id")
|
|
@@ -70,6 +72,57 @@ class Split:
|
|
|
70
72
|
f"labels={self.labels}>")
|
|
71
73
|
|
|
72
74
|
|
|
75
|
+
class LazySplit:
|
|
76
|
+
"""The windows of one split, each read from disk when it is asked for.
|
|
77
|
+
|
|
78
|
+
Same use as :class:`Split` (``len``, ``split[i]``, ``split["<window_id>"]``, iteration, the
|
|
79
|
+
``windows`` table), but only the window table is in memory, so it fits any split. Reading is
|
|
80
|
+
fast when the release files are in blocks (:mod:`bepad.blocks`); windows read in order share
|
|
81
|
+
their block reads.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, release, split, windows, labels=True):
|
|
85
|
+
self.release, self.name, self.labels = release, split, labels
|
|
86
|
+
self.windows = windows.reset_index(drop=True)
|
|
87
|
+
self._pos = dict(zip(self.windows["window_id"], range(len(self.windows))))
|
|
88
|
+
st = pd.read_parquet(release.path / "stations.parquet")
|
|
89
|
+
self._stations = {n: g.drop(columns="network") for n, g in st.groupby("network")}
|
|
90
|
+
self._readers = {}
|
|
91
|
+
|
|
92
|
+
def _reader(self, network, table):
|
|
93
|
+
key = (network, table)
|
|
94
|
+
if key not in self._readers:
|
|
95
|
+
cols = list(INPUT_COLUMNS) if table == "picks" and not self.labels else None
|
|
96
|
+
self._readers[key] = BlockReader(self.release.path / self.name / table / f"network={network}.parquet", cols)
|
|
97
|
+
return self._readers[key]
|
|
98
|
+
|
|
99
|
+
def __len__(self):
|
|
100
|
+
return len(self.windows)
|
|
101
|
+
|
|
102
|
+
def __iter__(self):
|
|
103
|
+
for i in range(len(self.windows)):
|
|
104
|
+
yield self[i]
|
|
105
|
+
|
|
106
|
+
def __getitem__(self, i):
|
|
107
|
+
if isinstance(i, str):
|
|
108
|
+
i = self._pos[i]
|
|
109
|
+
meta = self.windows.iloc[i]
|
|
110
|
+
wid, net = meta["window_id"], meta["network"]
|
|
111
|
+
picks = self._reader(net, "picks").window(wid)
|
|
112
|
+
ws = self._reader(net, "window_stations").window(wid)
|
|
113
|
+
st = ws.merge(self._stations[net], on="station", how="left")
|
|
114
|
+
st = st[["station", "latitude", "longitude", "elevation"]].reset_index(drop=True)
|
|
115
|
+
ev = None
|
|
116
|
+
if self.labels:
|
|
117
|
+
ev = self._reader(net, "events").window(wid)
|
|
118
|
+
ev = ev if len(ev) else None
|
|
119
|
+
return WindowData(wid, meta, picks, st, ev)
|
|
120
|
+
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
nets = self.windows["network"].nunique() if len(self.windows) else 0
|
|
123
|
+
return f"<bepad.LazySplit {len(self)} windows, {nets} networks, labels={self.labels}>"
|
|
124
|
+
|
|
125
|
+
|
|
73
126
|
class Release:
|
|
74
127
|
"""A BEPAD release folder (local copy of the Hugging Face dataset or a local build)."""
|
|
75
128
|
|
|
@@ -97,24 +150,51 @@ class Release:
|
|
|
97
150
|
def scenarios(self):
|
|
98
151
|
return json.loads((self.path / "scenarios.json").read_text())
|
|
99
152
|
|
|
100
|
-
def _read(self, split, table, networks, columns=None):
|
|
153
|
+
def _read(self, split, table, networks, columns=None, window_ids=None):
|
|
101
154
|
files = sorted((self.path / split / table).glob("network=*.parquet"))
|
|
102
155
|
if networks is not None:
|
|
103
156
|
keep = {f"network={n}.parquet" for n in networks}
|
|
104
157
|
files = [f for f in files if f.name in keep]
|
|
105
158
|
if not files:
|
|
106
159
|
raise FileNotFoundError(f"no {table} files for split={split!r}, networks={networks}")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
160
|
+
if window_ids is None:
|
|
161
|
+
return pd.concat([pd.read_parquet(f, columns=columns) for f in files], ignore_index=True)
|
|
162
|
+
return pd.concat([read_rows(f, window_ids, columns) for f in files], ignore_index=True)
|
|
163
|
+
|
|
164
|
+
def table(self, split, name, networks=None, window_ids=None):
|
|
165
|
+
"""One table as a DataFrame: ``"windows"`` (metadata), ``"window_stations"``, ``"events"``,
|
|
166
|
+
``"picks"`` (with labels) of ``split``, or ``"stations"`` (coordinates, every split).
|
|
167
|
+
``networks`` (``"core"`` = BEPAD-10) and ``window_ids`` limit the rows that are read."""
|
|
168
|
+
if name == "stations":
|
|
169
|
+
st = pd.read_parquet(self.path / "stations.parquet")
|
|
170
|
+
nets = _networks(networks)
|
|
171
|
+
return st if nets is None else st[st["network"].isin(nets)].reset_index(drop=True)
|
|
172
|
+
return self._read(split, name, _networks(networks), window_ids=None if window_ids is None else set(window_ids))
|
|
173
|
+
|
|
174
|
+
def load(self, split, networks=None, labels=True, scenarios=None, max_windows=None, window_ids=None,
|
|
175
|
+
lazy=False):
|
|
176
|
+
"""Load one split. ``labels=False`` returns only what an associator may use.
|
|
177
|
+
|
|
178
|
+
To save memory, load only some windows: ``scenarios`` (names, e.g. ``"ev-med__noise-high"``),
|
|
179
|
+
``max_windows`` (a fixed random sample of at most this many windows per network) or
|
|
180
|
+
``window_ids``. Only the rows of those windows are read.
|
|
181
|
+
``lazy=True``: keep only the window table in memory and read each window from disk when it
|
|
182
|
+
is asked for (:class:`LazySplit`; for training on many windows).
|
|
183
|
+
"""
|
|
111
184
|
cols = None if labels else list(INPUT_COLUMNS)
|
|
185
|
+
windows = self._read(split, "windows", networks)
|
|
186
|
+
ids = None
|
|
187
|
+
if scenarios is not None or max_windows is not None or window_ids is not None:
|
|
188
|
+
windows = _select(windows, scenarios, max_windows, window_ids)
|
|
189
|
+
ids = set(windows["window_id"])
|
|
190
|
+
if lazy:
|
|
191
|
+
return LazySplit(self, split, windows, labels)
|
|
112
192
|
s = Split(
|
|
113
|
-
windows=
|
|
114
|
-
picks=self._read(split, "picks", networks, columns=cols),
|
|
115
|
-
window_stations=self._read(split, "window_stations", networks),
|
|
193
|
+
windows=windows,
|
|
194
|
+
picks=self._read(split, "picks", networks, columns=cols, window_ids=ids),
|
|
195
|
+
window_stations=self._read(split, "window_stations", networks, window_ids=ids),
|
|
116
196
|
stations=pd.read_parquet(self.path / "stations.parquet"),
|
|
117
|
-
events=self._read(split, "events", networks) if labels else None,
|
|
197
|
+
events=self._read(split, "events", networks, window_ids=ids) if labels else None,
|
|
118
198
|
labels=labels,
|
|
119
199
|
)
|
|
120
200
|
s.release, s.name = self, split
|
|
@@ -132,6 +212,18 @@ CORE_NAME = "BEPAD-10"
|
|
|
132
212
|
CORE_NETWORKS = ["R05", "R07", "R01", "nc", "tx", "JMA", "CM", "R09", "ak", "NOA"]
|
|
133
213
|
|
|
134
214
|
|
|
215
|
+
def _select(windows, scenarios=None, max_windows=None, window_ids=None):
|
|
216
|
+
"""The windows kept by :meth:`Release.load` (scenario names, a sample per network, or ids)."""
|
|
217
|
+
if window_ids is not None:
|
|
218
|
+
windows = windows[windows["window_id"].isin(set(window_ids))]
|
|
219
|
+
if scenarios is not None:
|
|
220
|
+
windows = windows[windows["scenario"].isin([scenarios] if isinstance(scenarios, str) else scenarios)]
|
|
221
|
+
if max_windows is not None:
|
|
222
|
+
windows = (windows.groupby("network", group_keys=False)
|
|
223
|
+
.apply(lambda g: g.sample(min(len(g), max_windows), random_state=0).sort_index()))
|
|
224
|
+
return windows
|
|
225
|
+
|
|
226
|
+
|
|
135
227
|
def _networks(networks):
|
|
136
228
|
"""``"core"`` in a list of networks stands for the core networks."""
|
|
137
229
|
if networks is None:
|
|
@@ -164,7 +256,8 @@ def open(path): # noqa: A001 -- mirrors the usual `dataset.open` idiom
|
|
|
164
256
|
return Release(path)
|
|
165
257
|
|
|
166
258
|
|
|
167
|
-
def load(split="test", networks=None, labels=True, release=RELEASE, root=None, repo_id=REPO_ID
|
|
259
|
+
def load(split="test", networks=None, labels=True, release=RELEASE, root=None, repo_id=REPO_ID,
|
|
260
|
+
scenarios=None, max_windows=None, window_ids=None, lazy=False):
|
|
168
261
|
"""Load one split as a :class:`Split` (iterate it to get windows).
|
|
169
262
|
|
|
170
263
|
``root``: a local release folder (default: the ``BEPAD_ROOT`` environment
|
|
@@ -172,11 +265,18 @@ def load(split="test", networks=None, labels=True, release=RELEASE, root=None, r
|
|
|
172
265
|
(``repo_id``, ``release``) and cached.
|
|
173
266
|
``labels=False`` returns only what an associator may use. ``networks=["core"]``: the
|
|
174
267
|
:data:`CORE_NETWORKS`.
|
|
268
|
+
Less memory: ``scenarios`` (e.g. ``"ev-med__noise-high"``), ``max_windows`` (a fixed random
|
|
269
|
+
sample of at most this many windows per network) or ``window_ids`` (their networks are then
|
|
270
|
+
the only ones downloaded and read). ``lazy=True``: read each window from disk when it is asked
|
|
271
|
+
for (:class:`LazySplit`).
|
|
175
272
|
"""
|
|
273
|
+
if networks is None and window_ids is not None: # a window_id starts with its network: "tx.test..."
|
|
274
|
+
networks = sorted({w.split(".", 1)[0] for w in window_ids})
|
|
176
275
|
networks = _networks(networks)
|
|
177
276
|
root = root or os.environ.get("BEPAD_ROOT") # a local copy, e.g. export BEPAD_ROOT=data/releases/v0.1
|
|
178
277
|
path = Path(root) if root else download(release, splits=(split,), networks=networks, repo_id=repo_id)
|
|
179
|
-
return Release(path).load(split, networks=networks, labels=labels
|
|
278
|
+
return Release(path).load(split, networks=networks, labels=labels, scenarios=scenarios,
|
|
279
|
+
max_windows=max_windows, window_ids=window_ids, lazy=lazy)
|
|
180
280
|
|
|
181
281
|
|
|
182
282
|
def predictions_frame(window_ids, pick_idx, pred_event):
|
|
@@ -301,15 +301,13 @@ def evaluate(predictions, release, split="test", networks=None, min_shared=3, wi
|
|
|
301
301
|
if not isinstance(predictions, pd.DataFrame):
|
|
302
302
|
p = Path(predictions)
|
|
303
303
|
predictions = pd.read_parquet(p) if p.suffix == ".parquet" else pd.read_csv(p)
|
|
304
|
+
keep = set(window_ids) if window_ids is not None else None # a partial run: read only its rows
|
|
304
305
|
windows = rel._read(split, "windows", networks)
|
|
305
|
-
|
|
306
|
-
events = rel._read(split, "events", networks, columns=["window_id", "event_idx", "scorable", "latitude",
|
|
307
|
-
"longitude", "depth", "t0_rel_s"])
|
|
308
|
-
if window_ids is not None:
|
|
309
|
-
keep = set(window_ids)
|
|
306
|
+
if keep is not None:
|
|
310
307
|
windows = windows[windows.window_id.isin(keep)]
|
|
311
|
-
|
|
312
|
-
|
|
308
|
+
picks = rel._read(split, "picks", networks, columns=["window_id", "pick_idx", "event_idx"], window_ids=keep)
|
|
309
|
+
events = rel._read(split, "events", networks, columns=["window_id", "event_idx", "scorable", "latitude",
|
|
310
|
+
"longitude", "depth", "t0_rel_s"], window_ids=keep)
|
|
313
311
|
checks = check_predictions(predictions, picks)
|
|
314
312
|
if checks["n_missing_picks"]:
|
|
315
313
|
print(f"[evaluate] warning: {checks['n_missing_picks']} picks have no prediction -> counted as noise")
|
|
@@ -70,6 +70,12 @@ def _finish(fig, save_path, dpi=DPI):
|
|
|
70
70
|
return fig
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def _window_id(ax, wid):
|
|
74
|
+
"""The window_id in a corner of a panel, so the window can be loaded again: bepad.load(window_ids=[...])."""
|
|
75
|
+
ax.text(0.99, 0.02, wid, transform=ax.transAxes, ha="right", va="bottom", fontsize=4.5, color=INK2, zorder=10,
|
|
76
|
+
bbox=dict(boxstyle="square,pad=0.15", fc="white", ec="none", alpha=0.85))
|
|
77
|
+
|
|
78
|
+
|
|
73
79
|
def _panel(ax, letter):
|
|
74
80
|
ax.text(-0.02, 1.02, letter, transform=ax.transAxes, fontweight="bold", fontsize=8.5, ha="right", va="bottom",
|
|
75
81
|
color=INK)
|
|
@@ -157,6 +163,8 @@ def plot_window(w, model=None, ax=None, show_bands=True, legend=True, title=None
|
|
|
157
163
|
ax.set_ylabel("Hypocentral distance [km]")
|
|
158
164
|
if title:
|
|
159
165
|
ax.set_title(title, loc="left")
|
|
166
|
+
elif own:
|
|
167
|
+
ax.set_title(w.window_id, loc="left", fontsize=7, color=INK2)
|
|
160
168
|
if legend:
|
|
161
169
|
h = [Line2D([], [], marker="o", ls="", ms=3.5, color=INK2, label="real P"),
|
|
162
170
|
Line2D([], [], marker="o", ls="", ms=3.5, mfc="white", color=INK2, label="real S"),
|
|
@@ -208,6 +216,8 @@ def plot_window_map(w, ax=None, all_stations=None, legend=True, save_path=None,
|
|
|
208
216
|
ax.yaxis.set_major_locator(matplotlib.ticker.MaxNLocator(4))
|
|
209
217
|
ax.set_xlabel("Longitude [°]")
|
|
210
218
|
ax.set_ylabel("Latitude [°]")
|
|
219
|
+
if own:
|
|
220
|
+
ax.set_title(w.window_id, loc="left", fontsize=6, color=INK2)
|
|
211
221
|
if legend:
|
|
212
222
|
h = [Line2D([], [], marker="^", ls="", ms=4, color=INK2, label="station with picks"),
|
|
213
223
|
Line2D([], [], marker="^", ls="", ms=4, color="#c9c8c1", label="station, no picks"),
|
|
@@ -321,6 +331,7 @@ def plot_event_labels(w, model, events=None, ncols=3, save_path=None, dpi=DPI):
|
|
|
321
331
|
plt.Rectangle((0, 0), 1, 1, color="#b9d3f1", label="band: generation allowed"),
|
|
322
332
|
Line2D([], [], color=SERIES[1], ls="--", lw=0.8, label="last station")]
|
|
323
333
|
fig.legend(handles=h, loc="upper center", ncol=7, bbox_to_anchor=(0.45, 1.0), fontsize=5.5)
|
|
334
|
+
fig.suptitle(w.window_id, x=0.01, y=1.04, ha="left", fontsize=7, color=INK2)
|
|
324
335
|
return _finish(fig, save_path, dpi)
|
|
325
336
|
|
|
326
337
|
|
|
@@ -466,6 +477,7 @@ def plot_scenario_grid(split, network, ev_levels=("sparse", "low", "med", "high"
|
|
|
466
477
|
w = split[wid]
|
|
467
478
|
plot_window(w, model=model, ax=ax, legend=False, show_bands=False)
|
|
468
479
|
ax.set_title(f"{el} ({int(w.meta['n_events'])} ev.) · noise {nl}", loc="left", fontsize=6)
|
|
480
|
+
_window_id(ax, wid)
|
|
469
481
|
if i < len(noise_levels) - 1:
|
|
470
482
|
ax.set_xlabel("")
|
|
471
483
|
if j > 0:
|
|
@@ -497,6 +509,7 @@ def plot_gap_sweep(split, network, scenarios=("gap-000-090", "gap-090-180", "gap
|
|
|
497
509
|
lo, hi = sc.split("-")[1:]
|
|
498
510
|
axes[0][j].set_title(f"azimuthal gap {int(lo)}–{int(hi)}°", loc="left")
|
|
499
511
|
plot_window(w, ax=axes[1][j], legend=False, show_bands=False)
|
|
512
|
+
_window_id(axes[1][j], wid)
|
|
500
513
|
if j:
|
|
501
514
|
axes[0][j].set_ylabel("")
|
|
502
515
|
axes[1][j].set_ylabel("")
|
|
@@ -841,6 +854,7 @@ def plot_representative_windows(split, by="max_concurrent_events", quantiles=(0.
|
|
|
841
854
|
plot_window(win, model=model, ax=ax, legend=False, show_bands=False)
|
|
842
855
|
ax.set_title(f"{n} ({scales[n]}) · {names.get(q, q)} · {by.replace('_', ' ')} = {int(win.meta[by])}",
|
|
843
856
|
loc="left", fontsize=5.8)
|
|
857
|
+
_window_id(ax, wid)
|
|
844
858
|
if i < len(quantiles) - 1:
|
|
845
859
|
ax.set_xlabel("")
|
|
846
860
|
if j:
|
|
@@ -70,16 +70,22 @@ class ReleaseWindowDataset(Dataset):
|
|
|
70
70
|
|
|
71
71
|
``release``: a :class:`bepad.data.Release`, a local release folder, or None
|
|
72
72
|
to download ``split`` from Hugging Face.
|
|
73
|
+
Each window is read from disk when the ``DataLoader`` asks for it (``lazy=True``, the default),
|
|
74
|
+
so any split fits in memory. ``lazy=False`` holds all chosen windows in memory (faster per
|
|
75
|
+
window, but 20 GB for R07 train alone). ``networks``, ``scenarios``, ``max_windows`` (per
|
|
76
|
+
network) and ``window_ids`` choose windows, as in :func:`bepad.load`.
|
|
73
77
|
"""
|
|
74
78
|
|
|
75
|
-
def __init__(self, release=None, split="train", networks=None
|
|
79
|
+
def __init__(self, release=None, split="train", networks=None, scenarios=None, max_windows=None,
|
|
80
|
+
window_ids=None, lazy=True):
|
|
76
81
|
from .data import Release, load
|
|
77
82
|
|
|
83
|
+
sub = dict(scenarios=scenarios, max_windows=max_windows, window_ids=window_ids, lazy=lazy)
|
|
78
84
|
if release is None:
|
|
79
|
-
self.split = load(split, networks=networks, labels=True)
|
|
85
|
+
self.split = load(split, networks=networks, labels=True, **sub)
|
|
80
86
|
else:
|
|
81
87
|
rel = release if isinstance(release, Release) else Release(release)
|
|
82
|
-
self.split = rel.load(split, networks=networks, labels=True)
|
|
88
|
+
self.split = rel.load(split, networks=networks, labels=True, **sub)
|
|
83
89
|
|
|
84
90
|
def __len__(self):
|
|
85
91
|
return len(self.split)
|
|
@@ -94,8 +100,10 @@ class GeneratedWindowDataset(Dataset):
|
|
|
94
100
|
|
|
95
101
|
Item ``i`` of epoch ``e`` uses seed sha256(base_seed, e, i), so a run is
|
|
96
102
|
reproducible and every epoch sees new windows. Scenario configs (dicts of
|
|
97
|
-
window parameters, e.g.
|
|
98
|
-
drawn uniformly per item, as is the network.
|
|
103
|
+
window parameters, e.g. ``Release.scenarios()``, BEPAD's own) are
|
|
104
|
+
drawn uniformly per item, as is the network. Networks without earthquakes in ``split`` (test-only
|
|
105
|
+
networks, e.g. R06 for train) are left out; if a network cannot make the drawn scenario, the item
|
|
106
|
+
draws another network and scenario (still reproducible).
|
|
99
107
|
"""
|
|
100
108
|
|
|
101
109
|
def __init__(self, data_root, networks, scenarios, split="train", length=10_000, base_seed=0):
|
|
@@ -105,8 +113,17 @@ class GeneratedWindowDataset(Dataset):
|
|
|
105
113
|
from .build.windows import NetworkIndex
|
|
106
114
|
|
|
107
115
|
model = LabelModel.load(pool_dir(data_root) / "label_model.json")
|
|
108
|
-
self.index = {
|
|
109
|
-
|
|
116
|
+
self.index = {}
|
|
117
|
+
for n in networks:
|
|
118
|
+
ev, pk, st = load_network(data_root, n)
|
|
119
|
+
if (ev["split"] == split).any():
|
|
120
|
+
self.index[n] = NetworkIndex(ev, pk, st, network=n, label_model=model)
|
|
121
|
+
skipped = sorted(set(networks) - set(self.index))
|
|
122
|
+
if skipped:
|
|
123
|
+
print(f"[GeneratedWindowDataset] no {split} earthquakes, left out: {', '.join(skipped)}")
|
|
124
|
+
if not self.index:
|
|
125
|
+
raise ValueError(f"no network has {split} earthquakes")
|
|
126
|
+
self.networks = list(self.index)
|
|
110
127
|
self.scenarios = [(k, {**WINDOW_DEFAULTS, **v}) for k, v in
|
|
111
128
|
(scenarios.items() if isinstance(scenarios, dict) else enumerate(scenarios))]
|
|
112
129
|
self.split = split
|
|
@@ -125,14 +142,14 @@ class GeneratedWindowDataset(Dataset):
|
|
|
125
142
|
|
|
126
143
|
seed = stable_seed(self.base_seed, self.epoch, i)
|
|
127
144
|
rng = np.random.default_rng(seed)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
w = generate_window(self.index[net], self.split, cfg, seed +
|
|
145
|
+
for draw in range(20): # a network may not be able to make a scenario
|
|
146
|
+
net = self.networks[rng.integers(len(self.networks))]
|
|
147
|
+
name, cfg = self.scenarios[rng.integers(len(self.scenarios))]
|
|
148
|
+
w = generate_window(self.index[net], self.split, cfg, seed + draw, f"gen-{self.epoch}-{i}", str(name))
|
|
132
149
|
if w is not None:
|
|
133
150
|
break
|
|
134
151
|
else:
|
|
135
|
-
raise RuntimeError(f"could not generate
|
|
152
|
+
raise RuntimeError(f"could not generate window {i}: 20 draws of network and scenario failed")
|
|
136
153
|
st = self.index[net].stations.loc[w.stations["station"]].reset_index()
|
|
137
154
|
return window_to_tensors(w.window["window_id"], w.picks, st, w.events)
|
|
138
155
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bepad
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: BEPAD: a benchmark dataset for seismic phase association built from UTDQuake
|
|
5
5
|
License-Expression: GPL-3.0-or-later
|
|
6
|
+
Project-URL: Documentation, https://bepad.readthedocs.io
|
|
6
7
|
Project-URL: Repository, https://github.com/ecastillot/BEPAD
|
|
7
8
|
Project-URL: Dataset, https://huggingface.co/datasets/ecastillot/BEPAD
|
|
8
9
|
Requires-Python: >=3.10
|
|
@@ -30,6 +31,8 @@ Dynamic: license-file
|
|
|
30
31
|
|
|
31
32
|
# BEPAD — a benchmark dataset for seismic phase association
|
|
32
33
|
|
|
34
|
+
[](https://pypi.org/project/bepad/) [](https://huggingface.co/datasets/ecastillot/BEPAD/tree/main/v0.1) [](https://huggingface.co/datasets/ecastillot/BEPAD) [](https://bepad.readthedocs.io/en/latest/) [](https://github.com/ecastillot/BEPAD)
|
|
35
|
+
|
|
33
36
|
A phase associator groups the P and S picks of a seismic network into earthquakes. BEPAD gives
|
|
34
37
|
thousands of association problems built from **real earthquakes and real picks**
|
|
35
38
|
([UTDQuake](https://huggingface.co/datasets/ecastillot/UTDQuake)), with the answer for every pick.
|
|
@@ -37,12 +40,12 @@ thousands of association problems built from **real earthquakes and real picks**
|
|
|
37
40
|

|
|
38
41
|
|
|
39
42
|
*One window (Texas network): (a) earthquakes and stations, (b) every pick at its distance from
|
|
40
|
-
its earthquake, (c) picks by type.*
|
|
43
|
+
its earthquake, (c) picks by type. The associator's job is to find which picks belong together.*
|
|
41
44
|
|
|
42
45
|
```python
|
|
43
46
|
import bepad
|
|
44
47
|
|
|
45
|
-
test = bepad.load("test", labels=False)
|
|
48
|
+
test = bepad.load("test", networks=["tx"], max_windows=20, labels=False) # 20 Texas test windows
|
|
46
49
|
for w in test: # one window = one association problem
|
|
47
50
|
w.picks, w.stations # picks (station, P/S, time) and station coordinates
|
|
48
51
|
report = bepad.evaluate(predictions, test) # score your associator
|
|
@@ -64,8 +67,8 @@ bepad submit results/my-method --code associators/my-method # opens a Hugging
|
|
|
64
67
|
| **Associator authors** | fill in `bepad.Associator`, run on `test`, submit to the leaderboard — see *Contribute your associator* |
|
|
65
68
|
| **Deep learning** | `bepad.torch.ReleaseWindowDataset` — see *Train a model* |
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
this folder; `.[dl]` for PyTorch, `.[build]` to rebuild the dataset).
|
|
70
|
+
Documentation: https://bepad.readthedocs.io · Data: https://huggingface.co/datasets/ecastillot/BEPAD · Code: https://github.com/ecastillot/BEPAD.
|
|
71
|
+
Install: `pip install bepad` (or `pip install -e .` from this folder; `.[dl]` for PyTorch, `.[build]` to rebuild the dataset).
|
|
69
72
|
|
|
70
73
|
## Repository
|
|
71
74
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bepad"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "BEPAD: a benchmark dataset for seismic phase association built from UTDQuake"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
@@ -20,6 +20,7 @@ dependencies = [
|
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
[project.urls]
|
|
23
|
+
Documentation = "https://bepad.readthedocs.io"
|
|
23
24
|
Repository = "https://github.com/ecastillot/BEPAD"
|
|
24
25
|
Dataset = "https://huggingface.co/datasets/ecastillot/BEPAD"
|
|
25
26
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|