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.
Files changed (29) hide show
  1. {bepad-0.1.0 → bepad-0.1.2}/PKG-INFO +8 -5
  2. {bepad-0.1.0 → bepad-0.1.2}/README.md +6 -4
  3. {bepad-0.1.0 → bepad-0.1.2}/bepad/__init__.py +1 -1
  4. {bepad-0.1.0 → bepad-0.1.2}/bepad/associator.py +11 -14
  5. bepad-0.1.2/bepad/blocks.py +170 -0
  6. {bepad-0.1.0 → bepad-0.1.2}/bepad/build/release.py +11 -1
  7. {bepad-0.1.0 → bepad-0.1.2}/bepad/data.py +111 -11
  8. {bepad-0.1.0 → bepad-0.1.2}/bepad/metrics.py +5 -7
  9. {bepad-0.1.0 → bepad-0.1.2}/bepad/plots.py +14 -0
  10. {bepad-0.1.0 → bepad-0.1.2}/bepad/torch.py +29 -12
  11. {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/PKG-INFO +8 -5
  12. {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/SOURCES.txt +1 -0
  13. {bepad-0.1.0 → bepad-0.1.2}/pyproject.toml +2 -1
  14. {bepad-0.1.0 → bepad-0.1.2}/LICENSE +0 -0
  15. {bepad-0.1.0 → bepad-0.1.2}/bepad/__main__.py +0 -0
  16. {bepad-0.1.0 → bepad-0.1.2}/bepad/build/__init__.py +0 -0
  17. {bepad-0.1.0 → bepad-0.1.2}/bepad/build/config.py +0 -0
  18. {bepad-0.1.0 → bepad-0.1.2}/bepad/build/geo.py +0 -0
  19. {bepad-0.1.0 → bepad-0.1.2}/bepad/build/labels.py +0 -0
  20. {bepad-0.1.0 → bepad-0.1.2}/bepad/build/pool.py +0 -0
  21. {bepad-0.1.0 → bepad-0.1.2}/bepad/build/utdquake.py +0 -0
  22. {bepad-0.1.0 → bepad-0.1.2}/bepad/build/windows.py +0 -0
  23. {bepad-0.1.0 → bepad-0.1.2}/bepad/leaderboard.py +0 -0
  24. {bepad-0.1.0 → bepad-0.1.2}/bepad/stats.py +0 -0
  25. {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/dependency_links.txt +0 -0
  26. {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/entry_points.txt +0 -0
  27. {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/requires.txt +0 -0
  28. {bepad-0.1.0 → bepad-0.1.2}/bepad.egg-info/top_level.txt +0 -0
  29. {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.0
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.0"
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, workers=1, release=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); such runs are
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
- 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)
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 if win.window_id in ids]
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": n == len(w),
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
- 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
 
@@ -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
- return pd.concat([pd.read_parquet(f, columns=columns) for f in files], ignore_index=True)
108
-
109
- def load(self, split, networks=None, labels=True):
110
- """Load one split. ``labels=False`` returns only what an associator may use."""
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=self._read(split, "windows", networks),
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
- picks = rel._read(split, "picks", networks, columns=["window_id", "pick_idx", "event_idx"])
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
- picks = picks[picks.window_id.isin(keep)]
312
- events = events[events.window_id.isin(keep)]
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. 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.0
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.0"
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