obspy-free pre-processing at rm_resp=NO; obspy becomes an extra - #357
Draft
mdenolle wants to merge 1 commit into
Draft
obspy-free pre-processing at rm_resp=NO; obspy becomes an extra#357mdenolle wants to merge 1 commit into
mdenolle wants to merge 1 commit into
Conversation
Step (i) and the NoisePy half of step (iii) of the migration in seisfetch's
docs/noisepy-obspy-replacement-report.md, which the 2026-08 evaluation
justified. Requires noisepy-seis-io with the array-backed ChannelData.
noise_module.preprocess_raw_np is the obspy-free pre-processing chain for
rm_resp=NO: gap check, per-segment nan/inf zeroing, float32 cast, demean,
detrend, 5% taper, merge with zero fill, 5%/50 s taper, bandpass pre-filter,
Fourier resample with the sub-sample segment_interpolate alignment, and a
nearest-sample trim/pad. correlate.preprocess dispatches to it and now fetches
the inventory only when rm_resp != NO, since response removal is the only step
that needs it. Any other rm_resp setting falls back to the obspy chain, as does
an ImportError or a window the numpy chain declines.
The operations themselves are IMPORTED from seisfetch.contrib rather than copied
in. They are translations of obspy routines and live in an LGPL-licensed file
there, so importing keeps this MIT tree clear of it, and both projects test one
implementation instead of two copies drifting.
The bar is bit-identity, not similarity, because these outputs get published.
tests/test_preprocess_equivalence.py asserts np.array_equal against the obspy
chain — plus equal dtype, sampling rate and start time — on both fixture sets at
40, 20 and 1 Hz. The fixtures earn their keep: CI.CCA/CI.HEC 2022-002 start at
00:00:00.0195, which is 0.218 samples off the requested window at 40 Hz and not
a whole microsecond, so the resample branch, the sub-sample alignment inside it
and the nearest-sample trim all fire. A companion test fails if those fixtures
ever become sample-aligned and stop covering that.
Getting there needed two fixes beyond the plan, both on paths the report did not
flag. taper() called obspy's ("taper", "hann") entry point, which resolves to
scipy.signal.windows.hann — now called directly, with a test asserting the two
windows are equal. cc_parameters called obspy.geodetics.base.gps2dist_azimuth
for every station pair, writing dist/azi/baz into every saved CCF; it now uses
seisfetch's Vincenty port, which is exactly equal to obspy on 2000 random pairs.
obspy is now an extra (obspy / asdf / all), needed only for response removal,
the FDSN downloader, StationXML catalogs, the obspy-backed stores, and ASDF I/O.
Every import of it is lazy: noise_module and correlate import it inside the
functions that use it, annotations are deferred via TYPE_CHECKING, and
noisepy.seis exposes `download` through a module __getattr__ so importing the
package does not pull in the obspy-based downloader.
tests/test_no_obspy.py is the acceptance test: in a subprocess where importing
obspy and pyasdf RAISES, it imports noisepy.seis and runs read -> preprocess ->
FFT -> cross-correlate to a non-empty CCF. It blocks the module rather than
inspecting sys.modules afterwards, because a lazy import inside a rarely-taken
branch would pass an "was it imported?" check while still making obspy a hard
requirement for whoever takes that branch. This retires the evaluation's
two-env caveat: the claim is no longer "the data path does not use obspy" but
"the pipeline runs without it installed".
Pins seisfetch[noisepy]==0.4.1 — the first release whose boto3 floor (>=1.26)
co-exists with the aiobotocore==2.5.2 pin here. Verified against the published
wheel, not just a local checkout.
Tests: 201 passed (188 before, 13 new); the 3 mpi4py errors pre-date this work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step (i) and the NoisePy half of step (iii) of the migration justified by seisfetch's obspy-replacement evaluation: pre-process without obspy at
rm_resp=NO, and demote obspy to an optional extra.Opened as a draft on purpose. It needs
TraceSegmentfrom noisepy/noisepy-io#51, so CI will fail against the currently pinnednoisepy-seis-io==0.3.5. Sequence: merge #51 → release noisepy-seis-io → bump the pin here → mark ready. Reviewing the code now is useful; merging it now is not possible.What changes
noise_module.preprocess_raw_npis the obspy-free pre-processing chain forrm_resp=NO: gap check, per-segment nan/inf zeroing, float32 cast, demean, detrend, 5% taper, merge with zero fill, 5%/50 s taper, bandpass pre-filter, Fourier resample with the sub-samplesegment_interpolatealignment, and a nearest-sample trim/pad.correlate.preprocessdispatches to it, and now fetches the inventory only whenrm_resp != NO— response removal is the one step that needs it. Any otherrm_respsetting falls back to the obspy chain, as does anImportErroror a window the numpy chain declines.The operations are imported from
seisfetch.contrib, not copied in. They are translations of obspy routines and live in an LGPL-licensed file there, so importing keeps this MIT tree clear of it, and both projects test one implementation instead of two copies drifting apart.obspy becomes an extra (
obspy/asdf/all), needed only for response removal, the FDSN downloader, StationXML catalogs, the obspy-backed stores, and ASDF I/O. Every import of it is lazy, andnoisepy.seisexposesdownloadthrough a module__getattr__so importing the package does not pull in the obspy-based downloader.Why you can trust the numbers are unchanged
The bar is bit-identity, not similarity, because these outputs get published.
tests/test_preprocess_equivalence.pyassertsnp.array_equalagainst the obspy chain — plus equal dtype, sampling rate and start time — on both fixture sets at 40, 20 and 1 Hz. The fixtures earn their keep:CI.CCA/CI.HEC2022-002 start at00:00:00.0195, which is 0.218 samples off the requested window at 40 Hz and not a whole microsecond, so the resample branch, the sub-sample alignment inside it, and the nearest-sample trim all fire. A companion test fails if those fixtures ever become sample-aligned and stop covering that.Independently, seisfetch's harnesses run identical SCEDC/NCEDC/EarthScope bytes through both paths and then through NoisePy's own
compute_fft/correlate:max|diff|/peak = 0.00e+00on single-station EN/EZ/NZ/ZZ and on three cross-station pairs.Two problems the evaluation had not flagged
Both were found by running the real pipeline rather than the ports in isolation:
cc_parameterscalledobspy.geodetics.base.gps2dist_azimuthfor every station pair, writing dist/azi/baz into every saved CCF. It now uses seisfetch's Vincenty port (exactly equal to obspy on 2000 random pairs).taper()similarly called obspy's("taper", "hann")entry point, which resolves toscipy.signal.windows.hann— now called directly, with a test asserting the two windows are equal.fricfromUTCDateTime.microsecond; pymseed keeps exact nanoseconds. On a day file starting at00:00:00.019537920the chains computednfric1.6e-6 apart and outputs differed by ~1e-8 relative — small, but not bit-identical. Fixed upstream in seisfetch 0.4.1.The acceptance test
tests/test_no_obspy.pyruns, in a subprocess where importing obspy and pyasdf raises, the whole default cloud path: importnoisepy.seis→ pre-process → FFT → cross-correlate to a non-empty CCF with validdist.It blocks the module rather than inspecting
sys.modulesafterwards, deliberately: a lazyimport obspyinside a rarely-taken branch would sail through a "was it imported?" check while still making obspy a hard requirement for whoever takes that branch.This retires the evaluation's two-env caveat. The claim is no longer "the data path does not use obspy" but "the pipeline runs without it installed".
Testing
204 passed, 13 of them new.
One pre-existing failure, unrelated to this branch:
tests/test_scheduler_additional.py::test_get_array_size_raises_when_size_missingfails in a full-suite run onmaintoo (verified by checking outorigin/mainand running the suite). It is order-dependent — the test patchesboto3.client, but an earlier test has already cached a client, sodescribe_jobsreturns a MagicMock instead ofNoneand the expectedTypeErrornever fires. It passes in isolation. Worth a separate fix.Verified against the published seisfetch 0.4.1 wheel from PyPI, not a local checkout.
Before merge
noisepy-seis-io==0.3.5here to that release🤖 Generated with Claude Code