Skip to content

Make the station selection mean what the config says: radius, and one instrument per station - #45

Merged
sgjholt merged 2 commits into
mainfrom
claude/roadmap-priorities-pe63kc
Sep 18, 2026
Merged

sgjholt merged 2 commits into
mainfrom
claude/roadmap-priorities-pe63kc

Conversation

@sgjholt

@sgjholt sgjholt commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

Three bugs of one class, found by a live smoke fetch against IRIS rather than by the suite: the config promised a selection the download did not honour.

Each is a property ObsPy's MassDownloader supplies for free and the thin wrapper had to build by hand — sanitize, CircularDomain, channel_priorities.

1. The radius never reached the waveforms

max_radius_km was converted to degrees and passed to get_stations. The waveform call repeated the config's wildcards:

stream = client.get_waveforms(
    network=config.stations.network,
    station=config.stations.station,   # "*"
    ...
)

FDSN dataselect takes no geographic parameters, so this is not a missing argument — a wildcard waveform request is unbounded however the stations were selected.

On a Magna fetch with max_radius_km = 50, that returned three BH channels from UU.SRU, 225 km out. SRU is absent from the archived inventory.xml, which was radius-filtered, so the artefact held waveforms with no metadata beside them. min_radius_km had the identical hole.

Fix: the waveform request is built from the inventory the station query returned — one bulk line per channel, epochs collapsed, -- for a blank location code. The request is the metadata, so a trace without a station entry cannot be asked for.

2. A degree is not 111.195 km

The conversion was max_radius_km / 111.195. A degree of arc is not a fixed distance; measured on WGS84 across latitude and azimuth:

km per degree
equator 110.574
poles 111.691
spread 1.01%

So one constant moves the boundary by −0.44% to +0.56% — ±0.3 km on a 50 km radius, ±2 km on Magna's 400 km.

Fix: the station query is sent at the loose end of that bracket, deliberately wide so the server cannot clip what the exact cut would keep, and the cut is made locally against the true epicentral distance from gps2dist_azimuth. max_radius_km = 50 now means 50 km wherever the event is. The two bracketing constants are asserted against the geodetics rather than trusted, so tightening them back toward 111.195 fails.

Verified down to sub-kilometre arrays: at a 500 m radius the cut resolves a 2 m difference in station position, and the widening is 3.1 m.

Also fixed here: min_radius_km was only sent alongside max_radius_km, so on its own it did nothing. A non-positive radius and an inverted annulus are now refused rather than silently selecting nothing — the failure mode of a metres-for-kilometres typo on a local array.

3. Every instrument at every station

channel = "HH*,BH*,HN*,EN*" returns all of them wherever they are installed — 507 channels for Magna's 88 stations. Since the request is built from that answer, all 507 are downloaded, and a station carrying a broadband beside an accelerometer contributes the same ground motion twice: two entries in a SpectrumSet, weighted as two stations by anything that averages over them.

Fix: channel_priorities and location_priorities on [stations], first-match-wins per station:

channel = "HH*,BH*,HN*,EN*"
channel_priorities = ["HH[ZNE]", "BH[ZNE]", "HN[ZNE]", "EN[ZNE]"]

channel says what the data centre may offer; the priorities say which of what it offers to keep. Applied in ObsPy's order — channels within a location, then locations across the station — so a list carried over from Restrictions selects the same instrument. A station matching no pattern is dropped, and the written inventory.xml is pruned to match.

With no list set the fetch warns and names the stations; ["*"] keeps everything and says so deliberately. datasets/magna_2020.toml stays unranked on purpose — the near-source broadbands are the ones that clip, and the accelerometer beside them is why the paper covers that distance range — and now records why.

The manifest gains channels_available and channels_requested beside channels, so what the query offered, what was asked for and what arrived are three separate numbers.

Tests

26 new tests. Those for §1 fail against the previous implementation; the fake client no longer offers get_waveforms at all, so a return to the wildcard path cannot pass.

Verification

pytest -m "not dataset and not notebook"    1207 passed, 15 skipped
pytest --without-optional-extras            1188 passed, 18 skipped
pytest tests/test_acquire.py                60 passed
ruff check / ruff format --check            clean
mypy                                        no issues in 65 source files
sphinx-build                                clean (bar blocked intersphinx here)

Not covered: a live re-fetch confirming the radius and the priorities hold on the wire. This sandbox has no FDSN egress, so that check is the reporter's to repeat — the channel count should drop and inventory.xml should account for every waveform file.

🤖 Generated with Claude Code

…m too

`max_radius_km` reached `get_stations` and could never reach the waveform
call: FDSN dataselect takes no geographic parameters, so repeating the config's
wildcards asked the archive for the whole network however the stations had been
selected.

Found on a live fetch. A Magna smoke test with `max_radius_km = 50` came back
with three BH channels from a station 225 km out, absent from the archived
`inventory.xml` — an artefact holding waveforms with no metadata beside them,
and a config whose stated spatial restriction the download did not honour.

The waveform request is now built from the inventory the station query
returned: one bulk line per channel, epochs collapsed. That makes the radius
apply to both halves and makes the orphan case unreachable by construction —
what was asked for is exactly what has metadata. An empty station query now
says so rather than sending a bulk request with no lines, which FDSN rejects
with a message about syntax.

`min_radius_km` had the same hole and is fixed by the same change.

The four new tests fail against the previous implementation. The fake client
drops `get_waveforms` entirely, so a return to the wildcard path cannot pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@read-the-docs-community

read-the-docs-community Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 SpecMod | 🛠️ Build #34622737 | 📁 Comparing 2bd4fdc against latest (a7ad8bd)

  🔍 Preview build  

6 files changed · ± 6 modified

± Modified

…etres

Two ways the thin wrapper handed the caller a trap that `MassDownloader`
closes for free.

`channel = "HH*,BH*,HN*,EN*"` returns every instrument at every station that
has them — 507 channels for Magna's 88 stations. The waveform request is
built from that answer, so all of them are downloaded, and a station with a
broadband beside 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` and `location_priorities` rank them
instead, first-match-wins per station, in ObsPy's order — channels within a
location, then locations — so a list carried over from `Restrictions` selects
the same instrument. The inventory is pruned to match, keeping the archive
describing exactly what it holds. With no list set, the fetch warns and names
the stations; `["*"]` is how a config says it wants all of them and means it.

`max_radius_km` was converted with a single 111.195 km/degree. A degree of
arc is not a fixed distance: measured on WGS84 across latitude and azimuth it
runs 110.574 km at the equator to 111.691 km at the poles, a 1.01% spread, so
one constant moves the boundary by -0.44% to +0.56% — 0.3 km on a 50 km
radius, 2 km on Magna's 400 km. The station query is now sent at the loose end
of that bracket, deliberately wide, and the cut made against the true
epicentral distance from `gps2dist_azimuth`. The bracketing constants are
asserted against the geodetics rather than trusted.

Also fixed: `min_radius_km` was only sent alongside `max_radius_km`, so on its
own it did nothing. A non-positive radius and an inverted annulus are now
refused rather than silently selecting nothing, which is the failure mode of a
metres-for-kilometres typo on a local array.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sgjholt sgjholt changed the title Ask for waveforms by name, so max_radius_km applies to the download too Make the station selection mean what the config says: radius, and one instrument per station Sep 17, 2026
@sgjholt
sgjholt marked this pull request as ready for review September 17, 2026 22:48
@sgjholt
sgjholt merged commit 2af57e8 into main Sep 18, 2026
14 checks passed
@sgjholt
sgjholt deleted the claude/roadmap-priorities-pe63kc branch September 18, 2026 13:37
sgjholt pushed a commit that referenced this pull request Sep 18, 2026
…ersion

The code shipped in 2af57e8, the squash merge of #45. Squashing replaced the
two commit subjects — `fix(acquire): ask for waveforms by name, so the radius
applies to them too` and `feat(acquire): rank co-sited instruments, and cut the
radius in kilometres` — with the pull request's title, which is not a
conventional commit. release-please could not parse it:

    commit could not be parsed: 2af57e8 Make the station selection mean what
      the config says: radius, and one instrument per station (#45)
    error message: Error: unexpected token ' ' at 1:5
    Considering: 0 commits
    No commits for path: ., skipping

So the work is invisible to the release machinery: no bump, and no changelog
entry for a release that contains it. That is the failure `AGENTS.md` means by
load-bearing commit messages, and it cannot be repaired in the commit itself —
the message on `main` is what it is.

This restores the record where release-please will read it, and pays the
documentation the change owed either way: `docs/upgrading.md` had no entry for
a radius that now cuts against WGS84 rather than a fixed 111.195 km/degree, for
`min_radius_km` beginning to work on its own, for the three config errors that
now raise, or for the new priority lists. The same section was missing for
0.3 to 0.4, where `[smoothing] method` began being applied — the same shape of
break, where the call is unchanged and the numbers move.

`Release-As` pins the version rather than leaving it to the type, so the bump
is right whichever way this lands: squashed, the subject is the title of this
pull request and the footer still parses.

Release-As: 0.5.0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sgjholt pushed a commit that referenced this pull request Sep 19, 2026
release-please generated the 0.5.0 section from the commits it could parse, and
the one that carries this release's behaviour changes is not among them: 2af57e8,
the squash merge of #45, has a subject that is not a Conventional Commit, so the
generated Features line described b460d19 — the commit that restored the record —
which is documentation about the change rather than the change.

Left alone, the 0.5.0 changelog and the GitHub Release minted from it would say
a radius fix, a WGS84 cut and two new config fields were "documentation". This
names them and points at the commit that carries them. `docs/releasing.md` makes
reading the changelog the review step for exactly this.

Hand-edited, like the 0.2.0 heading above it, with a comment saying why.
release-please only prepends to the file, so it survives the next release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants