bepad 0.1.1__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.
Files changed (29) hide show
  1. {bepad-0.1.1 → bepad-0.1.2}/PKG-INFO +8 -5
  2. {bepad-0.1.1 → bepad-0.1.2}/README.md +6 -4
  3. {bepad-0.1.1 → bepad-0.1.2}/bepad/__init__.py +1 -1
  4. bepad-0.1.2/bepad/blocks.py +170 -0
  5. {bepad-0.1.1 → bepad-0.1.2}/bepad/build/release.py +11 -1
  6. {bepad-0.1.1 → bepad-0.1.2}/bepad/data.py +75 -20
  7. {bepad-0.1.1 → bepad-0.1.2}/bepad/plots.py +14 -0
  8. {bepad-0.1.1 → bepad-0.1.2}/bepad/torch.py +29 -12
  9. {bepad-0.1.1 → bepad-0.1.2}/bepad.egg-info/PKG-INFO +8 -5
  10. {bepad-0.1.1 → bepad-0.1.2}/bepad.egg-info/SOURCES.txt +1 -0
  11. {bepad-0.1.1 → bepad-0.1.2}/pyproject.toml +2 -1
  12. {bepad-0.1.1 → bepad-0.1.2}/LICENSE +0 -0
  13. {bepad-0.1.1 → bepad-0.1.2}/bepad/__main__.py +0 -0
  14. {bepad-0.1.1 → bepad-0.1.2}/bepad/associator.py +0 -0
  15. {bepad-0.1.1 → bepad-0.1.2}/bepad/build/__init__.py +0 -0
  16. {bepad-0.1.1 → bepad-0.1.2}/bepad/build/config.py +0 -0
  17. {bepad-0.1.1 → bepad-0.1.2}/bepad/build/geo.py +0 -0
  18. {bepad-0.1.1 → bepad-0.1.2}/bepad/build/labels.py +0 -0
  19. {bepad-0.1.1 → bepad-0.1.2}/bepad/build/pool.py +0 -0
  20. {bepad-0.1.1 → bepad-0.1.2}/bepad/build/utdquake.py +0 -0
  21. {bepad-0.1.1 → bepad-0.1.2}/bepad/build/windows.py +0 -0
  22. {bepad-0.1.1 → bepad-0.1.2}/bepad/leaderboard.py +0 -0
  23. {bepad-0.1.1 → bepad-0.1.2}/bepad/metrics.py +0 -0
  24. {bepad-0.1.1 → bepad-0.1.2}/bepad/stats.py +0 -0
  25. {bepad-0.1.1 → bepad-0.1.2}/bepad.egg-info/dependency_links.txt +0 -0
  26. {bepad-0.1.1 → bepad-0.1.2}/bepad.egg-info/entry_points.txt +0 -0
  27. {bepad-0.1.1 → bepad-0.1.2}/bepad.egg-info/requires.txt +0 -0
  28. {bepad-0.1.1 → bepad-0.1.2}/bepad.egg-info/top_level.txt +0 -0
  29. {bepad-0.1.1 → 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.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
+ [![PyPI](https://img.shields.io/pypi/v/bepad?color=orange&label=PyPI&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/bepad/) [![Data v0.1](https://img.shields.io/badge/Data-v0.1-yellow?style=for-the-badge)](https://huggingface.co/datasets/ecastillot/BEPAD/tree/main/v0.1) [![Hugging Face Dataset](https://img.shields.io/badge/HuggingFace-Dataset-yellow?style=for-the-badge&logo=huggingface&logoColor=black)](https://huggingface.co/datasets/ecastillot/BEPAD) [![Read the Docs](https://img.shields.io/badge/Docs-Read%20the%20Docs-blue?style=for-the-badge&logo=read-the-docs)](https://bepad.readthedocs.io/en/latest/) [![GitHub](https://img.shields.io/badge/GitHub-BEPAD-black?style=for-the-badge&logo=github)](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
  ![a BEPAD window](https://raw.githubusercontent.com/ecastillot/BEPAD/main/docs/figures/window_anatomy_tx.png)
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) # download the test split from Hugging Face
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
- Code: https://github.com/ecastillot/BEPAD · Data: https://huggingface.co/datasets/ecastillot/BEPAD · Documentation: `docs/` (Read the Docs). Install: `pip install bepad` (or `pip install -e .` from
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
+ [![PyPI](https://img.shields.io/pypi/v/bepad?color=orange&label=PyPI&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/bepad/) [![Data v0.1](https://img.shields.io/badge/Data-v0.1-yellow?style=for-the-badge)](https://huggingface.co/datasets/ecastillot/BEPAD/tree/main/v0.1) [![Hugging Face Dataset](https://img.shields.io/badge/HuggingFace-Dataset-yellow?style=for-the-badge&logo=huggingface&logoColor=black)](https://huggingface.co/datasets/ecastillot/BEPAD) [![Read the Docs](https://img.shields.io/badge/Docs-Read%20the%20Docs-blue?style=for-the-badge&logo=read-the-docs)](https://bepad.readthedocs.io/en/latest/) [![GitHub](https://img.shields.io/badge/GitHub-BEPAD-black?style=for-the-badge&logo=github)](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
  ![a BEPAD window](https://raw.githubusercontent.com/ecastillot/BEPAD/main/docs/figures/window_anatomy_tx.png)
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) # download the test split from Hugging Face
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
- Code: https://github.com/ecastillot/BEPAD · Data: https://huggingface.co/datasets/ecastillot/BEPAD · Documentation: `docs/` (Read the Docs). Install: `pip install bepad` (or `pip install -e .` from
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.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
@@ -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
- df.to_parquet(d / f"network={network}.parquet", index=False)
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
 
@@ -106,14 +159,27 @@ class Release:
106
159
  raise FileNotFoundError(f"no {table} files for split={split!r}, networks={networks}")
107
160
  if window_ids is None:
108
161
  return pd.concat([pd.read_parquet(f, columns=columns) for f in files], ignore_index=True)
109
- return pd.concat([_read_windows(f, columns, window_ids) for f in files], ignore_index=True)
110
-
111
- def load(self, split, networks=None, labels=True, scenarios=None, max_windows=None, window_ids=None):
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):
112
176
  """Load one split. ``labels=False`` returns only what an associator may use.
113
177
 
114
178
  To save memory, load only some windows: ``scenarios`` (names, e.g. ``"ev-med__noise-high"``),
115
179
  ``max_windows`` (a fixed random sample of at most this many windows per network) or
116
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).
117
183
  """
118
184
  cols = None if labels else list(INPUT_COLUMNS)
119
185
  windows = self._read(split, "windows", networks)
@@ -121,6 +187,8 @@ class Release:
121
187
  if scenarios is not None or max_windows is not None or window_ids is not None:
122
188
  windows = _select(windows, scenarios, max_windows, window_ids)
123
189
  ids = set(windows["window_id"])
190
+ if lazy:
191
+ return LazySplit(self, split, windows, labels)
124
192
  s = Split(
125
193
  windows=windows,
126
194
  picks=self._read(split, "picks", networks, columns=cols, window_ids=ids),
@@ -156,20 +224,6 @@ def _select(windows, scenarios=None, max_windows=None, window_ids=None):
156
224
  return windows
157
225
 
158
226
 
159
- def _read_windows(file, columns, window_ids, batch_size=200_000):
160
- """Rows of one parquet file whose window is in ``window_ids``, read in batches (low memory)."""
161
- import pyarrow as pa
162
- import pyarrow.compute as pc
163
- import pyarrow.parquet as pq
164
-
165
- pf = pq.ParquetFile(file, pre_buffer=False, buffer_size=1 << 20) # stream, do not read the file at once
166
- schema = pa.schema([pf.schema_arrow.field(c) for c in columns]) if columns else pf.schema_arrow
167
- ids = pa.array(sorted(window_ids), type=schema.field("window_id").type)
168
- parts = [b.filter(pc.is_in(b.column("window_id"), value_set=ids))
169
- for b in pf.iter_batches(batch_size=batch_size, columns=columns)]
170
- return pa.Table.from_batches(parts, schema=schema).to_pandas()
171
-
172
-
173
227
  def _networks(networks):
174
228
  """``"core"`` in a list of networks stands for the core networks."""
175
229
  if networks is None:
@@ -203,7 +257,7 @@ def open(path): # noqa: A001 -- mirrors the usual `dataset.open` idiom
203
257
 
204
258
 
205
259
  def load(split="test", networks=None, labels=True, release=RELEASE, root=None, repo_id=REPO_ID,
206
- scenarios=None, max_windows=None, window_ids=None):
260
+ scenarios=None, max_windows=None, window_ids=None, lazy=False):
207
261
  """Load one split as a :class:`Split` (iterate it to get windows).
208
262
 
209
263
  ``root``: a local release folder (default: the ``BEPAD_ROOT`` environment
@@ -213,7 +267,8 @@ def load(split="test", networks=None, labels=True, release=RELEASE, root=None, r
213
267
  :data:`CORE_NETWORKS`.
214
268
  Less memory: ``scenarios`` (e.g. ``"ev-med__noise-high"``), ``max_windows`` (a fixed random
215
269
  sample of at most this many windows per network) or ``window_ids`` (their networks are then
216
- the only ones downloaded and read).
270
+ the only ones downloaded and read). ``lazy=True``: read each window from disk when it is asked
271
+ for (:class:`LazySplit`).
217
272
  """
218
273
  if networks is None and window_ids is not None: # a window_id starts with its network: "tx.test..."
219
274
  networks = sorted({w.split(".", 1)[0] for w in window_ids})
@@ -221,7 +276,7 @@ def load(split="test", networks=None, labels=True, release=RELEASE, root=None, r
221
276
  root = root or os.environ.get("BEPAD_ROOT") # a local copy, e.g. export BEPAD_ROOT=data/releases/v0.1
222
277
  path = Path(root) if root else download(release, splits=(split,), networks=networks, repo_id=repo_id)
223
278
  return Release(path).load(split, networks=networks, labels=labels, scenarios=scenarios,
224
- max_windows=max_windows, window_ids=window_ids)
279
+ max_windows=max_windows, window_ids=window_ids, lazy=lazy)
225
280
 
226
281
 
227
282
  def predictions_frame(window_ids, pick_idx, pred_event):
@@ -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. from ``bepad.materialize.expand_scenarios``) are
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 = {n: NetworkIndex(*load_network(data_root, n), network=n, label_model=model) for n in networks}
109
- self.networks = list(networks)
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
- net = self.networks[rng.integers(len(self.networks))]
129
- name, cfg = self.scenarios[rng.integers(len(self.scenarios))]
130
- for attempt in range(10):
131
- w = generate_window(self.index[net], self.split, cfg, seed + attempt, f"gen-{self.epoch}-{i}", str(name))
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 a window for network {net}, scenario {name}")
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.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
+ [![PyPI](https://img.shields.io/pypi/v/bepad?color=orange&label=PyPI&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/bepad/) [![Data v0.1](https://img.shields.io/badge/Data-v0.1-yellow?style=for-the-badge)](https://huggingface.co/datasets/ecastillot/BEPAD/tree/main/v0.1) [![Hugging Face Dataset](https://img.shields.io/badge/HuggingFace-Dataset-yellow?style=for-the-badge&logo=huggingface&logoColor=black)](https://huggingface.co/datasets/ecastillot/BEPAD) [![Read the Docs](https://img.shields.io/badge/Docs-Read%20the%20Docs-blue?style=for-the-badge&logo=read-the-docs)](https://bepad.readthedocs.io/en/latest/) [![GitHub](https://img.shields.io/badge/GitHub-BEPAD-black?style=for-the-badge&logo=github)](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
  ![a BEPAD window](https://raw.githubusercontent.com/ecastillot/BEPAD/main/docs/figures/window_anatomy_tx.png)
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) # download the test split from Hugging Face
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
- Code: https://github.com/ecastillot/BEPAD · Data: https://huggingface.co/datasets/ecastillot/BEPAD · Documentation: `docs/` (Read the Docs). Install: `pip install bepad` (or `pip install -e .` from
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,6 +4,7 @@ pyproject.toml
4
4
  bepad/__init__.py
5
5
  bepad/__main__.py
6
6
  bepad/associator.py
7
+ bepad/blocks.py
7
8
  bepad/data.py
8
9
  bepad/leaderboard.py
9
10
  bepad/metrics.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bepad"
7
- version = "0.1.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