Publishing a dataset¶
How to take an event from an FDSN archive to a hash-pinned entry in
specmod.datasets.REGISTRY. Written to be followed end to end in one sitting.
Datasets are versioned by registry name — magna_2020_v1 and
magna_2020_v2 are separate entries — so a published result pinned to v1 keeps
fetching v1 after v2 exists. Nothing here ever revises an entry in place.
Before you start¶
Network access to the data centres named in the config (IRIS for waveforms, USGS for the catalogue, in Magna’s case).
Push access to the repository, and permission to create releases.
specmodinstalled from the working tree:uv pip install -e '.[dev]'.pyproject.toml’stag_regexin place — it has been onmainsince v0.2.0. Step 3 creates adata-v1tag, and without that constraint the tag silently becomes the package’s version number. See “Why the tag prefix matters” below.
1. Fetch¶
specmod fetch datasets/magna_2020.toml -o build/magna_2020
This is the only command in the project that touches the network. It resolves
the hypocentre from the catalogue service named in the config, expands the
station wildcards against the data centre, cuts each channel to the window
relative to origin, and writes an EventDirectory with a manifest.json
beside it.
Expect it to take a few minutes — Magna is 88 stations across seven networks.
Check the manifest before going further. It records what came back, not just what was asked for, and this is the moment to read it:
python -m json.tool build/magna_2020/manifest.json | head -40
event.origin,event.latitude/longitude/depth_km— check these against the USGS event page foruu60363602rather than against memory. The config resolves the hypocentre from ComCat by id, so a wrong id gives a confidently wrong event rather than an error, and nothing downstream would notice.channels— the list after wildcard expansion.HH*does not tell you what you got; this does. A much shorter list than expected usually means a network was not open at that time, not that the request was malformed.channels_available,channels_requested— what the station query offered, and what was asked for after the priority lists narrowed it. A large gap betweenchannels_requestedandchannelsmeans the archive did not serve what its own metadata advertises.data_centre,obspy_version,specmod_version,fetched_at— the provenance that makes the response reproducible-ish. It cannot be fully reproducible: FDSN is not content-addressed, archives get backfilled and catalogue solutions revised.
If any of that is wrong, fix the config and re-fetch. The artefact is about to become immutable.
Picking one instrument per station¶
A channel pattern that names several instruments — HH*,BH*,HN*,EN* — returns
every one of them at every station that has them. Magna’s 88 stations return 507
channels that way, and a station with both a broadband and an accelerometer
contributes the same ground motion twice: two entries in a SpectrumSet,
weighted as two stations by anything that averages over them.
channel_priorities ranks them instead. The first pattern that matches anything
at a station wins, so the list is read as “a broadband if there is one,
otherwise an accelerometer”:
[stations]
channel = "HH*,BH*,HN*,EN*"
channel_priorities = ["HH[ZNE]", "BH[ZNE]", "HN[ZNE]", "EN[ZNE]"]
location_priorities = ["", "00", "10"]
channel still says what the data centre may offer; the priorities say which of
what it offers to keep. A station matching no pattern at all is dropped — the
list is a restriction as well as a ranking — and the written inventory.xml is
pruned to match, so the archive keeps describing exactly what it holds.
Patterns are matched case-sensitively against SEED codes, which are upper case.
["*"] keeps everything, which is how a config says it wants both instruments
and means it; with no list at all, fetch warns that it is fetching co-sited
instruments and names the stations.
What the radius means¶
max_radius_km is cut against the true WGS84 distance from the epicentre, not
against the degrees FDSN takes. A degree of arc is 110.574 km at the equator and
111.691 km at the poles, so converting with a single constant moves the boundary
by up to half a percent — ±0.3 km on a 50 km radius, ±2 km on 400 km. The
station query is sent deliberately wide and the exact cut is made locally, which
costs a few extra stations in the query and makes the radius mean kilometres
wherever the event is.
The radii are epicentral. For a local array the depth is usually the larger term: every station of a 500 m array over a 2 km-deep event is at 2 km hypocentral distance.
2. Archive and hash¶
tar -czf magna_2020_v1.tar.gz -C build magna_2020
shasum -a 256 magna_2020_v1.tar.gz
Two things that matter about this command:
-C build magna_2020so the tarball contains a single top-level directory.pooch.Untarunpacks it into the cache anddatasets.load()expects that one directory; a tarball of loose files lands them in the cache root.The digest is the pin. Keep the output of
shasum— it goes in the registry entry, and it is what makes a corrupted or substituted download fail loudly instead of being analysed quietly.
Sanity-check the size before uploading. GitHub release assets are capped at 2 GB, and anything over a few hundred MB is worth reconsidering as a dataset.
3. Publish¶
Create a release tagged data-v1 — not v1, not 1.0 — and attach the
tarball.
gh release create data-v1 magna_2020_v1.tar.gz \
--title "Data release 1" \
--notes "Magna, Utah, Mw 5.7, 2020-03-18. 88 stations, raw counts plus
response. Built with: specmod fetch datasets/magna_2020.toml"
Or through the web UI: Releases → Draft a new release → tag data-v1 →
attach → publish.
Then copy the asset’s download URL. It looks like:
https://github.com/sgjholt/SpecMod/releases/download/data-v1/magna_2020_v1.tar.gz
Why the tag prefix matters¶
data- is not decoration. Two things in this repository read tags, and both
have to be told that data tags are not code releases:
hatch-vcs, which derives
specmod.__version__fromgit describe. setuptools-scm’s defaulttag_regexhas an optional(?:[\w-]+-)?prefix group, so it stripsdata-, reads the remainingv1as a version, and the package reports1. Measured on this repository:0.1.0.post1.dev173before the tag,1after it.pyproject.tomlnow constrains bothgit_describe_command(--match "v[0-9]*") andtag_regex, andtests/test_versioning.pypins it.release-please, which is wired up and has cut three releases. It is configured manifest-first —
.release-please-manifest.jsonis the source of truth — rather than inferring the last release by scanning tags, which is what keeps adata-v1tag from being read as a code release and offered as2next.include-component-in-tagisfalse, so its tags are plainv*. See Releasing the software.
If the release feed gets noisy with data tags, the fallback in §5.2.3 of the
plan is a sibling sgjholt/specmod-data repository holding assets only — a
repo, not a package, so no extra release cycle for code.
4. Register¶
Two additions to src/specmod/datasets.py, landing in the same change as the
asset they point at — never before. A load_magna_2020() that can only 404 is
worse than its absence.
First the event itself. Only PNR_2019 exists today, and DatasetSpec needs
an Event. Take the values from the manifest, not from the paper or a web
page, so that the constant and the artefact cannot disagree:
MAGNA_2020 = Event(
origin="2020-03-18T13:09:31.000000Z", # from manifest.json
latitude=...,
longitude=...,
depth_km=...,
catalogue_magnitude=5.7,
catalogue_magnitude_type="Mw",
)
Then the registry entry:
REGISTRY: dict[str, DatasetSpec] = {
"magna_2020_v1": DatasetSpec(
name="magna_2020_v1",
url="https://github.com/sgjholt/SpecMod/releases/download/data-v1/magna_2020_v1.tar.gz",
sha256="sha256:<the digest from step 2>",
event=MAGNA_2020,
# Only if the event directory is nested inside the tarball rather than
# being its single top-level directory. Following step 2, it is not.
# member="",
),
}
Then verify it end to end from a cold cache:
SPECMOD_DATA_DIR=$(mktemp -d) python -c "
from specmod.datasets import load
d = load('magna_2020_v1')
print(len(d.stream()), 'traces')
print(d.inventory())
print(d.catalog())
"
A second run against the same SPECMOD_DATA_DIR should print the same thing
without downloading — that is pooch’s cache working, and it is what CI relies
on.
5. Test¶
Tests needing a fetched dataset are marked, so that pytest -m "not dataset"
stays a complete offline run:
@pytest.mark.dataset
def test_magna_loads() -> None:
dataset = load("magna_2020_v1")
...
Run them once locally (pytest -m dataset), then confirm the offline suite is
still complete (pytest -m "not dataset").
If something goes wrong¶
The digest does not match on download. Re-hash the local tarball. If it matches locally but not through pooch, the upload was truncated — delete the asset and re-attach it. Never “fix” this by updating the hash to whatever arrived.
You need to change a published artefact. You do not change it; you publish
data-v2 and add a magna_2020_v2 entry. The old entry stays and keeps
working, which is the entire reason versioning is by registry name.
The tag went out without the data- prefix. Delete the tag and the release
(gh release delete v1 --cleanup-tag), confirm hatchling version reports a
0.1.0.post* string again, and re-publish under the right name. Anyone who
built a wheel in between got a wrongly-versioned one.
load() says “Unknown dataset”. The registry entry is not in the installed
copy of the package — likely an editable install pointing elsewhere, or the
entry was added but not saved.