bepad 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bepad/stats.py ADDED
@@ -0,0 +1,140 @@
1
+ """Dataset statistics: tables that describe what is in a BEPAD split.
2
+
3
+ Every function takes a loaded split (``bepad.load(split)``, with labels) and
4
+ returns a pandas DataFrame; ``bepad.plots`` draws them.
5
+
6
+ network_scales(split) network -> "local" | "regional" | "global"
7
+ source_events(split) one row per source earthquake (catalog values + how often it is used)
8
+ event_pairs(split) one row per pair of earthquakes in the same window: distance, time apart
9
+ window_geometry(split) one row per window: stations, spacing, aperture, distances
10
+ pick_distances(split) one row per pick: type, phase, distance to its earthquake, travel time
11
+
12
+ Large splits are sampled with ``max_windows`` (per network) where the table is per pick or
13
+ per station, so the functions stay fast on the full release.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+
21
+ from .build.geo import haversine_km
22
+
23
+ #: typical pick distance [km] separating the three scales
24
+ SCALE_EDGES = (150.0, 1500.0)
25
+ SCALES = ("local", "regional", "global")
26
+
27
+
28
+ def _sample_ids(split, max_windows, seed=0):
29
+ w = split.windows
30
+ if max_windows is None:
31
+ return set(w.window_id)
32
+ return set(w.groupby("network", group_keys=False)
33
+ .apply(lambda g: g.sample(min(len(g), max_windows), random_state=seed)).window_id)
34
+
35
+
36
+ def _coords(split):
37
+ st = split.window_stations.drop_duplicates(["network", "station"]).set_index(["network", "station"])
38
+ return st[["latitude", "longitude"]]
39
+
40
+
41
+ def pick_distances(split, max_windows=200):
42
+ """Picks of (a sample of) windows with the hypocentral distance to their own earthquake
43
+ (event picks) or to the nearest earthquake (noise), and the travel time of event picks."""
44
+ p = _pick_distances(split, max_windows)
45
+ p["scale"] = p.network.map(network_scales(split))
46
+ return p[["window_id", "network", "scale", "station", "phase", "pick_type", "prob", "event_idx",
47
+ "distance_km", "travel_time_s"]]
48
+
49
+
50
+ def _pick_distances(split, max_windows):
51
+ ids = _sample_ids(split, max_windows)
52
+ net = split.windows.set_index("window_id").network
53
+ p = split.picks[split.picks.window_id.isin(ids)].copy()
54
+ p["network"] = p.window_id.map(net)
55
+ xy = _coords(split).reindex(pd.MultiIndex.from_arrays([p.network, p.station])).to_numpy()
56
+ ev = split.events[split.events.window_id.isin(ids)]
57
+ out = np.full(len(p), np.nan)
58
+ tt = np.full(len(p), np.nan)
59
+ for wid, rows in p.groupby("window_id").indices.items():
60
+ e = ev[ev.window_id == wid]
61
+ d = np.hypot(haversine_km(e.latitude.to_numpy()[:, None], e.longitude.to_numpy()[:, None],
62
+ xy[rows, 0][None, :], xy[rows, 1][None, :]),
63
+ (e.depth.to_numpy() / 1e3)[:, None]) # (E, n)
64
+ own = p.event_idx.to_numpy()[rows]
65
+ pos = {k: i for i, k in enumerate(e.event_idx)}
66
+ idx = np.array([pos.get(k, -1) for k in own])
67
+ out[rows] = np.where(idx >= 0, d[np.maximum(idx, 0), np.arange(len(rows))], d.min(axis=0))
68
+ t0 = e.set_index("event_idx").t0_rel_s.reindex(own).to_numpy() # NaN for noise (-1)
69
+ tt[rows] = p.t_rel_s.to_numpy()[rows] - t0
70
+ p["distance_km"], p["travel_time_s"] = out, tt
71
+ return p
72
+
73
+
74
+ def network_scales(split, max_windows=50):
75
+ """Scale of each network from the 90th percentile of the hypocentral distance of its real picks:
76
+ local (< 150 km), regional (150-1500 km) or global (> 1500 km)."""
77
+ p = _pick_distances(split, max_windows)
78
+ p90 = p[p.pick_type == "real"].groupby("network").distance_km.quantile(0.9)
79
+ return p90.map(lambda v: SCALES[int(np.searchsorted(SCALE_EDGES, v))])
80
+
81
+
82
+ def source_events(split):
83
+ """One row per source earthquake: catalog location, depth, magnitude, farthest station,
84
+ stations and gap in its first window, and the number of windows it appears in."""
85
+ ev = split.events.merge(split.windows[["window_id", "network"]], on="window_id")
86
+ uses = ev.groupby("src_event_id").size().rename("n_windows")
87
+ first = ev.sort_values(["src_event_id", "is_edge"]).drop_duplicates("src_event_id")
88
+ out = first.set_index("src_event_id").join(uses).reset_index()
89
+ out["depth_km"] = out.depth / 1e3
90
+ out["scale"] = out.network.map(network_scales(split))
91
+ return out[["src_event_id", "network", "scale", "latitude", "longitude", "depth_km", "magnitude",
92
+ "magnitude_type", "last_station_km", "n_stations", "gap_deg", "n_windows"]]
93
+
94
+
95
+ def event_pairs(split):
96
+ """Every pair of earthquakes in the same window: epicentral distance and origin-time difference.
97
+
98
+ Pairs close in both are the hardest to associate: their picks overlap in time at the same
99
+ stations."""
100
+ ev = split.events.merge(split.windows[["window_id", "network", "scenario"]], on="window_id")
101
+ rows = []
102
+ for wid, g in ev.groupby("window_id"):
103
+ if len(g) < 2:
104
+ continue
105
+ la, lo, t = g.latitude.to_numpy(), g.longitude.to_numpy(), g.t0_rel_s.to_numpy()
106
+ i, j = np.triu_indices(len(g), 1)
107
+ rows.append(pd.DataFrame({"window_id": wid, "network": g.network.iloc[0], "scenario": g.scenario.iloc[0],
108
+ "distance_km": haversine_km(la[i], lo[i], la[j], lo[j]),
109
+ "dt_s": np.abs(t[i] - t[j])}))
110
+ out = pd.concat(rows, ignore_index=True) if rows else pd.DataFrame(
111
+ columns=["window_id", "network", "scenario", "distance_km", "dt_s"])
112
+ out["scale"] = out.network.map(network_scales(split))
113
+ return out
114
+
115
+
116
+ def window_geometry(split, max_windows=300):
117
+ """Per window: number of stations, median nearest-neighbour station spacing, aperture
118
+ (largest inter-station distance), and median distance from the earthquakes to the stations
119
+ that picked them."""
120
+ ids = _sample_ids(split, max_windows)
121
+ w = split.windows[split.windows.window_id.isin(ids)]
122
+ ws = split.window_stations[split.window_stations.window_id.isin(ids)]
123
+ real = split.picks[split.picks.window_id.isin(ids) & (split.picks.real == 1)]
124
+ from scipy.spatial import cKDTree
125
+
126
+ n_pick = real.groupby("window_id").station.nunique()
127
+ rows = []
128
+ for wid, g in ws.groupby("window_id"):
129
+ la, lo = np.radians(g.latitude.to_numpy()), np.radians(g.longitude.to_numpy())
130
+ xyz = np.c_[np.cos(la) * np.cos(lo), np.cos(la) * np.sin(lo), np.sin(la)] # unit sphere
131
+ chord, _ = cKDTree(xyz).query(xyz, k=2)
132
+ nn_km = 2 * 6371.0 * np.arcsin(np.clip(chord[:, 1] / 2, 0, 1))
133
+ # aperture: farthest station from the farthest station of an arbitrary one (two passes)
134
+ a = np.argmax(haversine_km(g.latitude.iloc[0], g.longitude.iloc[0], g.latitude, g.longitude))
135
+ aperture = float(np.max(haversine_km(g.latitude.iloc[a], g.longitude.iloc[a], g.latitude, g.longitude)))
136
+ rows.append({"window_id": wid, "n_stations": len(g), "spacing_km": float(np.median(nn_km)),
137
+ "aperture_km": aperture, "n_picking_stations": int(n_pick.get(wid, 0))})
138
+ out = pd.DataFrame(rows).merge(w[["window_id", "network", "scenario", "max_hyp_dist_km"]], on="window_id")
139
+ out["scale"] = out.network.map(network_scales(split))
140
+ return out
bepad/torch.py ADDED
@@ -0,0 +1,159 @@
1
+ """PyTorch datasets for training associators on BEPAD.
2
+
3
+ - ``ReleaseWindowDataset``: the fixed windows of a release split (train/val).
4
+ - ``GeneratedWindowDataset``: fresh windows from the pool on every epoch
5
+ (same generator as the release, different seeds), for unlimited training data.
6
+
7
+ Each item is a dict of tensors for ONE window (variable number of picks and
8
+ stations). ``collate_windows`` concatenates a batch PyG-style, with
9
+ ``pick_batch``/``station_batch`` vectors and per-window offsets applied to
10
+ ``pick_station``.
11
+
12
+ Item keys::
13
+
14
+ window_id str
15
+ station_coords (S, 3) float32 latitude, longitude [deg], elevation [m]
16
+ pick_station (N,) int64 index into station_coords
17
+ pick_phase (N,) int64 0 = P, 1 = S
18
+ pick_time (N,) float32 seconds from window start
19
+ pick_label (N,) int64 event index in the window, -1 = noise
20
+ pick_prob (N,) float32 1.0-0.5 real, >= 0.5 generated, 0 noise (soft target)
21
+ pick_real (N,) bool pick comes from UTDQuake (not generated, not noise)
22
+ pick_type (N,) int64 0 = real, 1 = generated, 2 = noise
23
+ pick_sigma (N,) float32 timing uncertainty [s] at that distance (NaN for noise)
24
+ pick_scorable (N,) bool pick belongs to a scorable event
25
+ event_origin (E, 4) float32 t0 [s from window start], latitude, longitude, depth [km]
26
+ event_scorable (E,) bool
27
+
28
+ Normalisation is left to the model.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import numpy as np
34
+ import torch
35
+ from torch.utils.data import Dataset
36
+
37
+ from .build.geo import stable_seed
38
+
39
+ PHASE_CODE = {"P": 0, "S": 1}
40
+ TYPE_CODE = {"real": 0, "generated": 1, "noise": 2}
41
+
42
+
43
+ def window_to_tensors(window_id, picks, stations, events=None):
44
+ stations = stations.reset_index(drop=True)
45
+ pos = {s: i for i, s in enumerate(stations["station"])}
46
+ item = {
47
+ "window_id": window_id,
48
+ "station_coords": torch.tensor(stations[["latitude", "longitude", "elevation"]].to_numpy(np.float32)),
49
+ "pick_station": torch.tensor(picks["station"].map(pos).to_numpy(np.int64)),
50
+ "pick_phase": torch.tensor(picks["phase"].map(PHASE_CODE).to_numpy(np.int64)),
51
+ "pick_time": torch.tensor(picks["t_rel_s"].to_numpy(np.float32)),
52
+ }
53
+ if "event_idx" in picks:
54
+ item["pick_label"] = torch.tensor(picks["event_idx"].to_numpy(np.int64))
55
+ item["pick_prob"] = torch.tensor(picks["prob"].to_numpy(np.float32))
56
+ item["pick_real"] = torch.tensor(picks["real"].to_numpy(bool))
57
+ item["pick_type"] = torch.tensor(picks["pick_type"].map(TYPE_CODE).to_numpy(np.int64))
58
+ item["pick_sigma"] = torch.tensor(picks["sigma_s"].to_numpy(np.float32))
59
+ item["pick_scorable"] = torch.tensor(picks["in_scorable_event"].to_numpy(bool))
60
+ if events is not None:
61
+ e = events.sort_values("event_idx")
62
+ item["event_origin"] = torch.tensor(np.stack([
63
+ e["t0_rel_s"], e["latitude"], e["longitude"], e["depth"] / 1e3], axis=1).astype(np.float32))
64
+ item["event_scorable"] = torch.tensor(e["scorable"].to_numpy(bool))
65
+ return item
66
+
67
+
68
+ class ReleaseWindowDataset(Dataset):
69
+ """Fixed windows of one split (use train/val; never tune on test).
70
+
71
+ ``release``: a :class:`bepad.data.Release`, a local release folder, or None
72
+ to download ``split`` from Hugging Face.
73
+ """
74
+
75
+ def __init__(self, release=None, split="train", networks=None):
76
+ from .data import Release, load
77
+
78
+ if release is None:
79
+ self.split = load(split, networks=networks, labels=True)
80
+ else:
81
+ rel = release if isinstance(release, Release) else Release(release)
82
+ self.split = rel.load(split, networks=networks, labels=True)
83
+
84
+ def __len__(self):
85
+ return len(self.split)
86
+
87
+ def __getitem__(self, i):
88
+ w = self.split[i]
89
+ return window_to_tensors(w.window_id, w.picks, w.stations, w.events)
90
+
91
+
92
+ class GeneratedWindowDataset(Dataset):
93
+ """On-the-fly windows from the pool.
94
+
95
+ Item ``i`` of epoch ``e`` uses seed sha256(base_seed, e, i), so a run is
96
+ 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.
99
+ """
100
+
101
+ def __init__(self, data_root, networks, scenarios, split="train", length=10_000, base_seed=0):
102
+ from .build.config import WINDOW_DEFAULTS
103
+ from .build.labels import LabelModel
104
+ from .build.pool import load_network, pool_dir
105
+ from .build.windows import NetworkIndex
106
+
107
+ 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)
110
+ self.scenarios = [(k, {**WINDOW_DEFAULTS, **v}) for k, v in
111
+ (scenarios.items() if isinstance(scenarios, dict) else enumerate(scenarios))]
112
+ self.split = split
113
+ self.length = length
114
+ self.base_seed = base_seed
115
+ self.epoch = 0
116
+
117
+ def set_epoch(self, epoch):
118
+ self.epoch = epoch
119
+
120
+ def __len__(self):
121
+ return self.length
122
+
123
+ def __getitem__(self, i):
124
+ from .build.windows import generate_window
125
+
126
+ seed = stable_seed(self.base_seed, self.epoch, i)
127
+ 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))
132
+ if w is not None:
133
+ break
134
+ else:
135
+ raise RuntimeError(f"could not generate a window for network {net}, scenario {name}")
136
+ st = self.index[net].stations.loc[w.stations["station"]].reset_index()
137
+ return window_to_tensors(w.window["window_id"], w.picks, st, w.events)
138
+
139
+
140
+ def collate_windows(batch):
141
+ """Concatenate variable-size windows; adds pick_batch / station_batch / event_batch."""
142
+ out = {"window_id": [b["window_id"] for b in batch]}
143
+ offset = 0
144
+ parts = {k: [] for k in batch[0] if k != "window_id"}
145
+ parts.update(pick_batch=[], station_batch=[], event_batch=[])
146
+ for k, b in enumerate(batch):
147
+ n_sta = len(b["station_coords"])
148
+ for key in parts:
149
+ if key in b:
150
+ parts[key].append(b[key] + offset if key == "pick_station" else b[key])
151
+ parts["pick_batch"].append(torch.full((len(b["pick_time"]),), k, dtype=torch.long))
152
+ parts["station_batch"].append(torch.full((n_sta,), k, dtype=torch.long))
153
+ if "event_origin" in b:
154
+ parts["event_batch"].append(torch.full((len(b["event_origin"]),), k, dtype=torch.long))
155
+ offset += n_sta
156
+ for key, vals in parts.items():
157
+ if vals:
158
+ out[key] = torch.cat(vals)
159
+ return out
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: bepad
3
+ Version: 0.1.0
4
+ Summary: BEPAD: a benchmark dataset for seismic phase association built from UTDQuake
5
+ License-Expression: GPL-3.0-or-later
6
+ Project-URL: Repository, https://github.com/ecastillot/BEPAD
7
+ Project-URL: Dataset, https://huggingface.co/datasets/ecastillot/BEPAD
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy
12
+ Requires-Dist: pandas>=2.0
13
+ Requires-Dist: pyarrow
14
+ Requires-Dist: scipy
15
+ Requires-Dist: pyyaml
16
+ Requires-Dist: matplotlib
17
+ Requires-Dist: huggingface_hub
18
+ Provides-Extra: dl
19
+ Requires-Dist: torch>=2.1; extra == "dl"
20
+ Provides-Extra: baselines
21
+ Requires-Dist: pyocto; extra == "baselines"
22
+ Provides-Extra: build
23
+ Requires-Dist: utdquake>=1.0.1; extra == "build"
24
+ Provides-Extra: docs
25
+ Requires-Dist: sphinx>=7; extra == "docs"
26
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
27
+ Requires-Dist: myst-parser; extra == "docs"
28
+ Requires-Dist: sphinx-copybutton; extra == "docs"
29
+ Dynamic: license-file
30
+
31
+ # BEPAD — a benchmark dataset for seismic phase association
32
+
33
+ A phase associator groups the P and S picks of a seismic network into earthquakes. BEPAD gives
34
+ thousands of association problems built from **real earthquakes and real picks**
35
+ ([UTDQuake](https://huggingface.co/datasets/ecastillot/UTDQuake)), with the answer for every pick.
36
+
37
+ ![a BEPAD window](https://raw.githubusercontent.com/ecastillot/BEPAD/main/docs/figures/window_anatomy_tx.png)
38
+
39
+ *One window (Texas network): (a) earthquakes and stations, (b) every pick at its distance from
40
+ its earthquake, (c) picks by type.*
41
+
42
+ ```python
43
+ import bepad
44
+
45
+ test = bepad.load("test", labels=False) # download the test split from Hugging Face
46
+ for w in test: # one window = one association problem
47
+ w.picks, w.stations # picks (station, P/S, time) and station coordinates
48
+ report = bepad.evaluate(predictions, test) # score your associator
49
+ ```
50
+
51
+ Share your associator and get on the leaderboard in four commands:
52
+
53
+ ```bash
54
+ bepad new my-method # template folder
55
+ bepad run associators/my-method --split test --out results/my-method
56
+ bepad submit results/my-method --code associators/my-method # opens a Hugging Face pull request
57
+ ```
58
+
59
+ | | |
60
+ |---|---|
61
+ | **What is in a window** | several overlapping earthquakes on one network, with **real**, **generated** (plausible extra arrivals) and **noise** picks; every pick labelled with its earthquake and a confidence `prob` (≥ 0.5 arrival, 0 noise) |
62
+ | **Difficulty** | 23 scenarios: 1–20 earthquakes per window × 5 noise levels, plus earthquakes outside the network |
63
+ | **Size** | per network and scenario: 1,000 train · 200 val · 100 test windows (23 scenarios) |
64
+ | **Associator authors** | fill in `bepad.Associator`, run on `test`, submit to the leaderboard — see *Contribute your associator* |
65
+ | **Deep learning** | `bepad.torch.ReleaseWindowDataset` — see *Train a model* |
66
+
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).
69
+
70
+ ## Repository
71
+
72
+ ```
73
+ bepad/ load, evaluate, Associator, leaderboard, torch, plots (what users need)
74
+ bepad/build/ pool, labels, windows, release (how the dataset is built)
75
+ configs/ pool.yaml (quality control, split, labels), v0.1.yaml, dev.yaml (scenarios, sizes)
76
+ docs/ Read the Docs sources; docs/gallery/ = the code of every figure
77
+ associators/ contributed associators, one folder each (pyocto = worked example)
78
+ pipeline/ the recipe of a release (SLURM): pool -> release -> baselines (pipeline/README.md)
79
+ hf/README.md Hugging Face dataset card
80
+ ```
81
+
82
+ ## License and citation
83
+
84
+ Code: GPL-3.0. Data: the terms of the original providers, as in UTDQuake. Please cite UTDQuake and
85
+ the network operators of the data you use.
@@ -0,0 +1,23 @@
1
+ bepad/__init__.py,sha256=whHazCC_dwmQI0QLibRwsC1t1Ab-IZaG6wHCOui6h00,829
2
+ bepad/__main__.py,sha256=aNVf11co3xU3vDlQu7bhwOiRr7j9aKcC6J3PDW8GkMo,9414
3
+ bepad/associator.py,sha256=xw0cQcU8PyRQuPptRAgqk4P9-l4lIBPZjm4Ke5_3T3w,14902
4
+ bepad/data.py,sha256=Pm3ZmwrnxMtIScYyC5HQqENtghHttSsk0bXUHUQ4ge4,8178
5
+ bepad/leaderboard.py,sha256=o_VUDWEsqddIZJrGj6NMNzZ-8mxh31qnO47d6B8vx2k,18525
6
+ bepad/metrics.py,sha256=t0_cEt7cSncw3XjEHff-sLVUpqsrD4QqYY-Ju1dz15Q,15272
7
+ bepad/plots.py,sha256=5J_ahnncE7tLMk0ebL1ldbQNhkGBVzffr1Gedy7MbVY,50491
8
+ bepad/stats.py,sha256=O5NEGcznxb831AYNJgcSKFEZODKQAYv6tUmDy7UuY3c,7223
9
+ bepad/torch.py,sha256=l1lHSYLU0rn5smLeDb3M7A_zw4xgPpbDNINDPpXWcJY,6989
10
+ bepad/build/__init__.py,sha256=bs2kYx4znsNu4mI6yc5fg1hV17sOD6os4pq2M-QTeM0,513
11
+ bepad/build/config.py,sha256=mmG04H-7MFQIVyZ0qyg4GabY_gXO6m8r7ZRu-DdMXv0,3891
12
+ bepad/build/geo.py,sha256=ucen45viVT8u_qxw1R4cjCIN4qSCGzzobVyx0UbiBZI,2130
13
+ bepad/build/labels.py,sha256=7d6C5R1-efcP1RoTtWHvZZvwlj05HECApZWBeJ6KkNg,15812
14
+ bepad/build/pool.py,sha256=Ugz1md1bmndQZNuAEVWrXXo4eUZSscxX8_4wFZBeAlg,16731
15
+ bepad/build/release.py,sha256=2oEtph0Jy9ljsscEVoN_slkEVg6dumQMvc-cDvBUeZw,11282
16
+ bepad/build/utdquake.py,sha256=1PGuJYP1eqW0PRnqDFVSCR5kcGCi8CbS-mbkFGciFI4,2957
17
+ bepad/build/windows.py,sha256=1Ml4gMn6-mIT48cnj8mIaMebkY2mLXUQrIcLdRaN0Og,21640
18
+ bepad-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
19
+ bepad-0.1.0.dist-info/METADATA,sha256=mnEFOq40J3TlQlhITzQxy7BC29f1CJ9xWXqh34CUrEQ,4031
20
+ bepad-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
21
+ bepad-0.1.0.dist-info/entry_points.txt,sha256=IAY9QLOeY3bfXwKOylC9rJJ9usZHOu1uW-wyQvQKVLU,46
22
+ bepad-0.1.0.dist-info/top_level.txt,sha256=ohUvSDWJNaGyN7oCBdk_OZOdF2qgo0AtV33vCRHWvqI,6
23
+ bepad-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bepad = bepad.__main__:main