Roadmap¶
SpecMod is being rebuilt in stages, each one independently usable. This page says what has shipped, in which version, what is being worked on, and what 1.0 will mean.
No dates. The order is by dependency, not by calendar, and what comes after
the current milestone can still change. Shipped work names the version that
carries it, so it can be installed and checked rather than taken on trust;
unreleased work says so plainly.
REFACTOR_PLAN.md
is the working document behind this one and carries the reasoning, the
measurements and the open questions.
Where it is now¶
SpecMod is on PyPI, with a Zenodo DOI per release
and its own documentation version
for each. Install it with pip install specmod; v0.2.0 was the first release
of the rebuilt package.
This page was built from version 0.5.0.post1.dev3+g3b4db69. On stable that is the
release you are reading about; on latest it is a development build ahead of
it, which is the honest answer to which code the milestones below describe.
Still alpha, still pre-1.0: names and signatures move between 0.x releases
without a deprecation cycle. Everything below marked shipped is released,
tested against golden references, and usable — but pin an exact version for
anything you intend to publish. See
Releasing the software for what a version number means here.
Shipped in v0.2.0¶
The version before this one, 0.1.1, is the pre-refactor code that produced
the published Magna results. It is preserved unchanged on the
master branch, which is
protected and frozen — it was never tagged or published to PyPI, so that
branch is the only reference to it. Its tip,
453c77c, is the commit
the paper cites as v0.1.1; cite that rather than the branch name, which is a
moving target in principle even though this one will not move. Everything below is the delta between it
and v0.2.0, which is why the first changelog is enormous and correctly so.
An installable package¶
A real pyproject.toml, src/ layout, snake_case modules, linting, type
checking and a test suite on CI. Before this, the package could not be
installed or imported without editing paths by hand.
Configuration without globals¶
Settings live in a config/ package with semantic sections and layered
overrides, instead of module-level constants read at import time. Every output
records the configuration that produced it, so a locally-overridden run is
reproducible from its own outputs.
Publishing: docs, PyPI, DOI¶
This site, built from the repository on every merge; automated changelog and version derivation from tags; PyPI upload through Trusted Publishing; a Zenodo DOI per release. Deliberately built early, while the package was small enough that debugging the pipeline was cheap — and v0.2.0 is the release that proved every leg of it end to end.
A fresh clone still needs the one-time repository settings listed in Releasing the software; they are account and repository state, not something a commit can carry.
The transform layer¶
One SpectralEstimator protocol with interchangeable implementations —
FFTEstimator, WelchEstimator, MultitaperEstimator and Prieto’s — plus
Konno–Ohmachi smoothing and log-binning as separate, composable steps. The
mtspec Fortran dependency, which no longer builds on current toolchains, is
dropped rather than being the only path: multitaper is implemented natively on
SciPy’s DPSS tapers, and Prieto’s multitaper package is available through the
optional [multitaper] extra as the same-lineage substitute. Asking the
pipeline for mtspec by name raises, and says which two to use instead.
What each estimator does to real data is measured in Choosing a transform; the differences are large enough to matter to a magnitude.
Wavelets¶
A continuous wavelet transform alongside the Fourier estimators:
CWTEstimator, a Scalogram that tracks its cone of influence, quality
checks over it, and time-averaging back to a spectrum with ground-motion units
preserved.
Typed, portable persistence¶
HDF5 for arrays and Parquet for tables, replacing pickle everywhere — nothing in the package can write a pickle any more, so a saved result is readable without the code that wrote it and cannot execute anything on load.
A stable import surface¶
specmod.api — a path-free, deterministic, non-mutating import surface with
typed errors, for downstream packages that need something narrower than the
whole package and less volatile than its internals.
Shipped in v0.2.2¶
The tutorial, executed¶
The tutorial is published as part of this site and re-executed on every build, so every figure and number on the page came from running the code being documented. A notebook that stops working fails the build rather than going quietly stale, which is why it was a 1.0 requirement.
Shipped in v0.3.0¶
The decomposition, finished¶
This was the last item 1.0 was waiting on.
io.py and plotting.py are now packages, alongside the spectral core, the
fitting layer and the pick readers; the type-checking backlog is empty. The
split is by responsibility rather than line count — for io/, what a file
contains against how one is written against where the optional dependency
lives.
specmod.preprocess no longer modifies the stream it is given. Every
function returns a new one, which is the package-wide rule the containers have
always followed and the only place it did not hold. Ten functions were renamed
to say so — cut_s is s_window, set_picks is with_picks — because a
function that starts returning instead of mutating while keeping its name
leaves every existing call running and silently doing nothing.
Upgrading is the table.
The documentation 1.0 asked for¶
The conventions are stated explicitly and can be found: the amplitude
convention, the Parseval contract, and now the moment and magnitude equations
with their constants, in
How a spectrum is processed — in the sections that apply them,
signposted from Guides rather than split onto a page of their own.
Upgrading covers moving code off the pre-refactor
master.
Shipped in v0.4.0¶
Smoothing became a choice, and the choice is applied¶
[smoothing] method selected nothing. The registry mapped the names, the config
validated against them, and no code read the result: every spectrum was
log-binned whatever the setting said. Measured on the 28 PNR windows,
log_bins, konno_ohmachi and none produced bit-identical output — none
included, which reads as “leave my spectra unsmoothed”.
It is wired now, with three methods beside the two:
log_window— constant relative bandwidth over a window you name: boxcar, Bartlett, Hann, Hamming, Blackman or Gaussian, a fraction of an octave wide everywhere. Konno–Ohmachi is this family with its own window.statistic="median"discards a spike rather than averaging it in, and discards a genuine narrow peak with it.savitzky_golay— a local polynomial fit in log–log, which keeps peak amplitude and width where a running mean flattens both.none— no smoothing, which now means it.
Log-binning replaces the frequency axis; the others preserve it. That is the distinction the setting had no way to express, and the reason wiring it up was a design change rather than a patch.
One detail that is easy to get wrong and is worth knowing about: a window
symmetric in log frequency is not a symmetric average over samples, because a
Fourier grid is uniform in Hz. Weighting samples equally tilts the result by
1.5e-3 dex on an f**-2 power law; weighting each by its share of the log
axis brings that to 3.2e-8 dex. The latter is the default. Konno–Ohmachi
applies the former, by definition, and keeps it.
Shipped in v0.5.0¶
The station selection means what the config says¶
Three ways a fetched dataset did not match the config that asked for it, all found by a live fetch rather than by the suite:
max_radius_kmreached the station query and never the waveform request, because FDSN dataselect has no geographic parameters. A 50 km fetch returned channels from 225 km out, with no metadata beside them. Waveforms are now asked for by name, from the inventory the station query returned.The radius was converted at 111.195 km per degree. A degree of arc is 110.574 km at the equator and 111.691 km at the poles, so the boundary moved with latitude — ±0.3 km at 50 km, ±2 km at 400 km. The query is now sent deliberately wide and the cut made against the true WGS84 distance.
A
channelpattern naming several instruments returned all of them at every station, so a broadband and the accelerometer beside it entered the pipeline as two independent records of one ground motion.channel_prioritiesandlocation_prioritiesrank them, first match wins per station.
Planned¶
1.0 — the API stops moving¶
Everything this was waiting on shipped in v0.3.0 — the documentation, and the decomposition above with the signature changes it implied. What remains is the release itself, which says that names and signatures stop moving without a deprecation cycle.
That promise is the whole content of the number, which is why it has to be a decision rather than something a breaking commit does on its way past. Until it is made, breaking changes bump the minor. The honest test before making it is whether a release goes by without one — the API is not settled because the backlog is empty, it is settled when it stops moving.
By that test, 1.0 is not ready. v0.3.0 is the release that emptied the
backlog and it carried two breaking changes itself: the ten renamed
preprocess functions, and the tutorial’s page moving. So what 1.0 now waits
on is a release going out without one — which is a thing that has to be
observed rather than declared, and cannot be brought forward by finishing
anything.
v0.4.0 is the first observation, and it is not clean. No name or signature
moved: the smoothing work was additive, and the SpectrumPair.compare
parameter it added is optional. But [smoothing] method changed what it does,
so a config that set konno_ohmachi or none gets different numbers out of
the same code path. Nothing breaks at import or at the call; results move. That
is a weaker promise than the one 1.0 makes and a real one to have broken, so
the count starts again rather than standing at one.
v0.5.0 is not clean either, in the same way. Nothing was renamed and no
signature moved. But max_radius_km now cuts against true WGS84 distance
rather than a fixed kilometres-per-degree, so the same config can select a
different set of stations than it did before, and three radius values that used
to be accepted — zero, negative, and a minimum outside the maximum — are now
refused when the config is read. A config is as much a caller as a call site.
Twice in a row makes it worth naming: what keeps moving is not the API, it is what the API does with a setting that was already there. A deprecation cycle does not protect against that, since there is nothing to deprecate; the upgrading guide is the only thing that does, and 1.0 will not change that. The count stands at zero.
After 1.0¶
Designed but deliberately not on the path to 1.0, because each is blocked on an input rather than on effort — mostly a real file from a real tool, which a guess written from documentation cannot substitute for:
Presets for picker output (PhaseNet, EQTransformer, SeisBench), so those users write no column mapping. The configurable reader already exists; a preset is a mapping and a test against one real file.
Confirming two more pick formats (NonLinLoc
.hyp, IMS/GSE bulletins) that cannot be round-tripped through ObsPy and so are listed as unconfirmed rather than claimed.SeisComP picks, which ObsPy’s SCML reader currently discards. A test fails when that starts working upstream.
Recording pick-resolution policy in the configuration, alongside the provenance stamp.
A fuller
specmod fetch --verifythat re-fetches and diffs against the manifest, rather than only detecting local tampering.
How this page works¶
Each shipped entry names the version that carries it, so a reader can install that version and check the claim. Work that is merged but unreleased sits under In progress and gets its version when a release goes out — merged is not shipped, and this page does not blur the two. There is no such section right now, because nothing is merged and unreleased; that is the state the page should be in between releases, not a section that went missing.
The move at release time is the step this page got wrong once. The v0.3.0 work
sat under In progress — not yet released for a week after v0.3.0 was
released, because release-please writes the changelog and the manifest and
knows nothing about this file. tests/test_release_config.py now fails when
the released minor version is ahead of every version this page names, which is
the check that was missing rather than a rule that was.
Before v0.2.0 this was a list of stages, because there was no released version
to point at and “done” could only mean merged. The full changelog for every
release is in
CHANGELOG.md.