Metadata-Version: 2.4
Name: seisfetch
Version: 0.5.0
Summary: Fast seismic miniSEED from EarthScope, SCEDC, NCEDC, GeoNet, and 30+ FDSN servers. Core: numpy + boto3 + pymseed. No ObsPy required.
Author: UW ESS Denolle Group
License-Expression: MIT AND LGPL-3.0-only
Keywords: seismology,earthscope,scedc,ncedc,geonet,miniseed,fdsn,s3,numpy,xarray,zarr
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: boto3>=1.26
Requires-Dist: pymseed<0.10,>=0.6
Provides-Extra: obspy
Requires-Dist: obspy>=1.4; extra == "obspy"
Provides-Extra: noisepy
Requires-Dist: scipy>=1.10; extra == "noisepy"
Provides-Extra: fdsn
Requires-Dist: httpx>=0.25; extra == "fdsn"
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == "pandas"
Provides-Extra: xarray
Requires-Dist: xarray>=2023.1; extra == "xarray"
Provides-Extra: zarr
Requires-Dist: xarray>=2023.1; extra == "zarr"
Requires-Dist: zarr>=2.14; extra == "zarr"
Provides-Extra: auth
Requires-Dist: earthscope-sdk>=1.8; extra == "auth"
Provides-Extra: earth2
Requires-Dist: earth2studio>=0.13; extra == "earth2"
Requires-Dist: xarray>=2023.1; extra == "earth2"
Provides-Extra: notebooks
Requires-Dist: jupyter>=1.0; extra == "notebooks"
Requires-Dist: ipykernel>=6; extra == "notebooks"
Requires-Dist: matplotlib>=3.8; extra == "notebooks"
Requires-Dist: pandas>=2.0; extra == "notebooks"
Requires-Dist: xarray>=2023.1; extra == "notebooks"
Requires-Dist: zarr>=2.14; extra == "notebooks"
Requires-Dist: obspy>=1.4; extra == "notebooks"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: moto[s3]>=5; extra == "dev"
Requires-Dist: httpx>=0.25; extra == "dev"
Requires-Dist: matplotlib>=3.8; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: obspy>=1.4; extra == "dev"
Requires-Dist: pandas>=2.0; extra == "dev"
Requires-Dist: xarray>=2023.1; extra == "dev"
Requires-Dist: zarr>=2.14; extra == "dev"
Requires-Dist: jupyter>=1.0; extra == "dev"
Requires-Dist: ipykernel>=6; extra == "dev"
Provides-Extra: bench
Requires-Dist: httpx>=0.25; extra == "bench"
Requires-Dist: tabulate; extra == "bench"
Provides-Extra: all
Requires-Dist: seisfetch[auth,bench,dev,earth2,fdsn,notebooks,obspy,pandas,xarray,zarr]; extra == "all"
Dynamic: license-file

# seisfetch

Cloud-first seismic waveform access for EarthScope, SCEDC, NCEDC, GeoNet, and fallback FDSN services.

`seisfetch` is built around one core path:

```text
cloud archive / HTTP  ->  raw miniSEED bytes  ->  pymseed  ->  numpy arrays
                                                      |
                                                      +-> xarray
                                                      +-> zarr
                                                      +-> ObsPy
                                                      +-> Earth2Studio adapters
```

The design goal is simple:

- prefer cloud-native archive access first
- use FDSN second, as a fallback path
- decode miniSEED straight into numpy without requiring ObsPy in the main pipeline
- stay compatible with sparse sensor and Earth2Studio workflows

## Overall Structure

The intended acquisition order is:

1. `s3_open` for the anonymous archives: SCEDC, NCEDC, GeoNet, and the
   EarthScope Open Data networks (`AK`, `II`, `IU`, `N4`, `PB`, `TA`, `UU`, `UW`)
2. `s3_auth` for every other EarthScope network, which sits behind a
   credentialed access point (it still reads the Open Data networks anonymously)
3. `fdsn` only when the archive-backed path is unavailable or the network is not served from those buckets

At the package level, the main interfaces are:

- `SeisfetchClient.get_raw()` -> raw miniSEED bytes
- `SeisfetchClient.get_numpy()` -> `TraceBundle` of numpy arrays
- `SeisfetchClient.get_xarray()` -> `xarray.Dataset`
- `SeisfetchClient.get_waveforms()` -> ObsPy `Stream`
- `SeismicDataFrameSource` / `SeismicDataSource` -> Earth2Studio adapters over data you already fetched
- `SeisfetchLiveSource` -> time-indexed Earth2Studio `DataSource` that fetches on demand

## Workflow

### 1. Cloud-first archive access

`seisfetch` routes by network code:

| Network family | Preferred source | Backend |
|---|---|---|
| `CI`, other SCEDC-routed networks | SCEDC open S3 | `s3_open` |
| `BK`, other NCEDC-routed networks | NCEDC open S3 | `s3_open` |
| `NZ` | GeoNet open S3 | `s3_open` |
| `AK`, `II`, `IU`, `N4`, `TA`, `UU`, `UW` (EarthScope Open Data) | EarthScope open S3 | `s3_open` |
| every other EarthScope-routed network (`US`, `CC`, temporary codes, ...) | EarthScope restricted S3 | `s3_auth` |

Networks served by both SCEDC and NCEDC (`NC`, `NP`, `PB`, ...) route to NCEDC;
`PB` is also on EarthScope Open Data and can be read there with
`datacenter="earthscope"`. `is_earthscope_open(net)` and `earthscope_tier(net)`
report which EarthScope tier a network is on, and so does
`seisfetch info --route NET`.

Archive details:

| Archive | Bucket | Region | Auth |
|---|---|---|---|
| EarthScope Open Data | `earthscope-geophysical-data` | `us-east-2` | none (8 networks) |
| EarthScope restricted | `earthscope-mseed-v2-…-s3alias` access point | `us-east-2` | EarthScope SDK, role `s3-miniseed-v2`, credentials scoped per network |
| SCEDC | `scedc-pds` | `us-west-2` | none |
| NCEDC | `ncedc-pds` | `us-west-2` | none |
| GeoNet | `geonet-open-data` | `ap-southeast-2` | none |

Notes:

- SCEDC, NCEDC and GeoNet are per-channel archives, so you should pass `channel=...`.
- GeoNet channels always carry a numeric location code (`10`, `20`, ...), so `location=` is required there — a blank location raises rather than silently missing data.
- EarthScope stores station-day miniSEED objects (every channel of a station in one file); `get_numpy()` filters to the requested channel after parse.
- The eight EarthScope Open Data networks need no credentials at all. Every other EarthScope network needs `earthscope-sdk>=1.8` and the `s3-miniseed-v2` role; credentials are scoped per network, and per network-year for temporary FDSN codes (digit/X/Y/Z prefixes). The legacy `s3-miniseed` role is retired.

### 2. FDSN second

Use `backend="fdsn"` when:

- the desired network is not available from EarthScope / SCEDC / NCEDC / GeoNet S3
- the archive-backed attempt fails and you want a fallback provider
- you need a non-US provider such as GEOFON, INGV, ETH, ORFEUS, etc.

This keeps the default workflow archive-first instead of HTTP-first.

### 3. Decode directly to numpy

The central decode path is:

- fetch raw miniSEED bytes
- decode with [`pymseed`](https://github.com/EarthScope/pymseed)
- work in numpy immediately

ObsPy is not required for this path.

### 4. Convert only when needed

Once you have a `TraceBundle`, you can convert to:

- `xarray.Dataset` for labeled arrays and ML/data workflows
- `zarr` for chunked cloud/local persistence
- ObsPy `Stream` for classical seismology tooling
- Earth2Studio adapters for sparse sensor and digital twin/data assimilation workflows

## Why This Package

Existing seismic client workflows often couple:

- transport
- decode
- metadata
- downstream object model

`seisfetch` separates those concerns:

- transport: S3 or HTTP
- decode: miniSEED -> numpy
- output: xarray / zarr / ObsPy / Earth2Studio only when requested

That makes it useful for:

- cloud-native waveform mining
- sparse sensor ingestion
- foundation-model training pipelines
- Earth2Studio interoperability
- data assimilation and digital twin workflows

## ObsPy-Free, and What That Buys

ObsPy is an optional extra here, not a dependency of the data path. The
evaluation behind that choice — including whether it changes the science — is
written up in
[docs/noisepy-obspy-replacement-report.md](docs/noisepy-obspy-replacement-report.md),
with every number traceable to a committed JSON under `benchmarks/results/` or
a test in `tests/precision/`.

| | seisfetch | obspy stack |
|---|---|---|
| Parse an 11 MB Steim2 channel-day | 21.4 ms | 37.2 ms |
| Cold `import` | 0.08 s | 0.13 s |
| Parse peak memory | 27.7 MB | 52.0 MB |
| Installed footprint | 80.4 MB | 311.4 MB |
| arm64 Linux install | wheels | needs gcc |

The footprint line is the cloud argument: AWS Lambda caps a layer at 250 MB,
so the ObsPy stack does not fit and seisfetch does. ObsPy publishes no
linux/aarch64 wheels, so on Graviton it compiles from source; seisfetch and
pymseed install from wheels.

### The science does not change

Identical archive bytes were pushed through (A) `obspy.read` plus NoisePy's own
`preprocess_raw` and (B) seisfetch's parser plus the numpy/scipy ports in
`seisfetch.contrib.noisepy_adapter`, then through NoisePy's own `compute_fft`
and `correlate`. The pass criterion is bit-identity, not closeness:

| Harness | Result |
|---|---|
| Single-station CI.PASC, EN/EZ/NZ/ZZ at 40 sps | max abs diff `0.0` |
| Cross-station SCEDC x NCEDC x EarthScope at 20 sps | max abs diff `0.0` |
| dv/v stretching | same grid cell on all pairs |

The cross-station run matters because it exercises the Fourier-resample and
sub-sample-alignment branches inside the chain. Tables, plots and the
harnesses: [benchmarks/RESULTS.md](benchmarks/RESULTS.md).

Scope note: this bit-identity result covers the preprocessing chain at
`rm_resp=NO`. Response removal is validated separately, below.

### Response removal without evalresp

Response removal was the one ObsPy capability the NoisePy migration still
needed. `seisfetch.contrib.response` provides it in ~577 lines of numpy and
stdlib `xml.etree` — no evalresp C library, no ObsPy, no lxml, and no scipy in
that module. ObsPy has no pure-Python response evaluator (`remove_response`
calls compiled evalresp), so every stage was re-derived and then checked
against the compiled implementation:

| Check | Result |
|---|---|
| `evaluate_response(mode="full")` vs compiled evalresp — both CI.PASC epochs, VEL/ACC/DISP, 1 mHz–19.9 Hz | max rel diff `1.6e-10` |
| `remove_response_np` vs `Trace.remove_response` — real 6.9M-sample Tohoku day, water_level=60, pre_filt | `6.6e-16` of peak |
| Same, CI.PASC demo hour in notebook 06 | `7.6e-16` of peak |
| Deconvolve that Tohoku day | 1.9 s vs ObsPy 3.6 s |

Two evaluation modes: `mode="full"` is evalresp-equivalent (all stages, analog
and digital poles/zeros, FIR/Coefficients with DC normalization and the
`Decimation/CorrectionApplied` phase advance). `mode="paz"` is the SACPZ
shortcut — 0.7–1.3 % error below 4 Hz but ~23 % by 16 Hz, since the FIR
anti-alias roll-off is unmodeled; don't use it above ~Nyquist/3.

Two deconvolution styles: `remove_response_np` ports ObsPy's water-level
method for drop-in equivalence, and `translate_resp_np` follows SeisIO.jl's
translation approach, taking stabilization from the target response's own
roll-off instead of a water level.

Defective metadata fails loudly — zero or missing gains, degenerate
normalization references, zero-sum FIR stages, polynomial and `ResponseList`
stages all raise with the stage number named, never a silent NaN or unity
gain. Not implemented, and raising rather than approximating: IIR
`Coefficients` stages with denominators, polynomial (blockette-62) responses,
`ResponseList` stages. Metadata is StationXML only; RESP and SACPZ files are
not parsed.

Full derivation, the conditional-A0 finding about evalresp's normalization
rule, and the SeisIO comparison:
[docs/response-removal-design.md](docs/response-removal-design.md). Tutorial:
[notebooks/05_response_removal.ipynb](notebooks/05_response_removal.ipynb).

## Quick Start

### Open S3: SCEDC / NCEDC

```python
from seisfetch import SeisfetchClient

client = SeisfetchClient(backend="s3_open")

bundle = client.get_numpy(
    "CI",
    "ABL",
    channel="BHZ",
    starttime="2024-01-15T00:00:00",
    endtime="2024-01-15T00:10:00",
)

print(bundle.ids)
arrays = bundle.to_dict()
```

### Open S3: GeoNet (New Zealand)

`NZ` auto-routes to the GeoNet open-data bucket. GeoNet channels carry a
numeric location code, so pass `location=`:

```python
bundle = SeisfetchClient(backend="s3_open").get_numpy(
    "NZ",
    "WEL",
    location="10",
    channel="HHZ",
    starttime="2022-01-02T00:00:00",
    endtime="2022-01-02T00:10:00",
)
```

### Open S3: EarthScope Open Data

`AK`, `II`, `IU`, `N4`, `PB`, `TA`, `UU` and `UW` are served anonymously from
`earthscope-geophysical-data`. One object holds every channel of a
station-day, so ask for the channel you want and the rest is dropped after
parse:

```python
bundle = SeisfetchClient(backend="s3_open").get_numpy(
    "IU",
    "ANMO",
    location="00",
    channel="BHZ",
    starttime="2024-01-15T00:00:00",
    endtime="2024-01-15T00:01:00",
)
```

### EarthScope S3 (restricted networks, authenticated)

Every other EarthScope network needs `earthscope-sdk>=1.8` and an account with
the `s3-miniseed-v2` role. See [EarthScope Credentials](#earthscope-credentials).
`s3_auth` still reads the Open Data networks anonymously, so one client covers
both tiers:

```python
client = SeisfetchClient(backend="s3_auth")

bundle = client.get_numpy(
    "US",
    "NEW",
    location="00",
    channel="BHZ",
    starttime="2024-01-15T00:00:00",
    endtime="2024-01-15T00:01:00",
)
```

### EarthScope via FDSN (no S3 role required)

Works for any logged-in EarthScope account, including those without direct S3
access. Good for `TA`, `IU`, `US`, `UW`, `_GSN`, etc.:

```python
client = SeisfetchClient(backend="fdsn", providers="EARTHSCOPE")

bundle = client.get_numpy(
    "TA",
    "034A",
    channel="BHZ",
    starttime="2010-06-01T00:00:00",
    endtime="2010-06-01T00:05:00",
)
print(bundle.ids)            # ['TA.034A..BHZ']
print(bundle.to_dict()[bundle.ids[0]].shape)
```

### Station discovery

`get_stations()` queries `fdsnws-station` and auto-routes to SCEDC, NCEDC, or
EarthScope based on the network code. No ObsPy required.

```python
rows = client.get_stations(
    "TA", channel="BHZ",
    starttime="2010-06-01", endtime="2010-06-02",
)
for r in rows[:3]:
    print(r["Network"], r["Station"], r["Channel"], r["Latitude"], r["Longitude"])
```

### FDSN fallback

```python
client = SeisfetchClient(backend="fdsn", providers="GEOFON")

bundle = client.get_numpy(
    "GE",
    "BKB",
    channel="BHZ",
    starttime="2011-03-11T06:00:00",
    endtime="2011-03-11T06:05:00",
)
```

### xarray output

```python
ds = client.get_xarray(
    "CI",
    "ABL",
    channel="BHZ",
    starttime="2024-01-15T00:00:00",
    endtime="2024-01-15T00:10:00",
)
```

### Earth2Studio-compatible sparse sensor output

```python
from datetime import datetime
from seisfetch import SeismicDataFrameSource

df_source = SeismicDataFrameSource(bundle)
df = df_source(datetime(2024, 1, 15), list(ds.data_vars))
```

### Metadata table + zarr sidecar

```python
from seisfetch import bundle_to_metadata_table, to_zarr, write_metadata_csv

metadata_table = bundle_to_metadata_table(bundle)
to_zarr(bundle, "quickstart.zarr", metadata=metadata_table)
write_metadata_csv(metadata_table, "quickstart.zarr")
```

## Dependency Tuning

You do not need every dependency for every workflow.

### Minimal core

Use this when you only want miniSEED -> numpy from S3:

```bash
pip install seisfetch
```

Includes:

- `numpy`
- `boto3`
- `pymseed`

### Add FDSN fallback

Use this if you want direct HTTP fallback providers:

```bash
pip install "seisfetch[fdsn]"
```

Adds:

- `httpx`

### Add authenticated EarthScope S3

Use this for EarthScope networks outside the Open Data set:

```bash
pip install "seisfetch[auth]"
pip install earthscope-cli
```

Adds:

- `earthscope-sdk`
- EarthScope CLI login flow

### Add xarray

Use this for labeled arrays and ML pipelines:

```bash
pip install "seisfetch[xarray]"
```

### Add metadata tables

Use this if you want canonical metadata tables, CSV sidecars, or metadata-aware zarr output:

```bash
pip install "seisfetch[pandas]"
```

### Add zarr

Use this for chunked persistent stores:

```bash
pip install "seisfetch[zarr]"
```

### Add ObsPy

Use this only if you need ObsPy interop or ObsPy-backed FDSN behavior:

```bash
pip install "seisfetch[obspy]"
```

### Suggested dependency bundles

| Need | Install |
|---|---|
| S3 open data -> numpy only | `pip install seisfetch` |
| Metadata table / metadata.csv export | `pip install "seisfetch[pandas]"` |
| Archive-first + FDSN fallback | `pip install "seisfetch[fdsn]"` |
| EarthScope networks outside Open Data | `pip install "seisfetch[auth]"` and `pip install earthscope-cli` |
| xarray / ML / Earth2Studio-style workflows | `pip install "seisfetch[xarray]"` |
| zarr persistence | `pip install "seisfetch[zarr]"` |
| ObsPy interop | `pip install "seisfetch[obspy]"` |
| Most common research stack | `pip install "seisfetch[fdsn,auth,xarray,zarr,obspy]"` |

## Installation Modes

### pip

For scripts and lightweight pipelines:

```bash
pip install seisfetch
```

From source:

```bash
git clone https://github.com/Denolle-Lab/seisfetch
cd seisfetch
pip install .
```

### pixi

For notebook work and development:

```bash
git clone https://github.com/Denolle-Lab/seisfetch
cd seisfetch
pixi install
pixi install -e notebooks
pixi run -e notebooks kernel-install
```

The notebook environment is the intended Jupyter environment for this repo.

## EarthScope Credentials

EarthScope has **three access tiers**:

1. **Open Data S3** (`backend="s3_open"`) — anonymous, no account. Only the
   networks `AK`, `II`, `IU`, `N4`, `PB`, `TA`, `UU`, `UW`
   (`seisfetch.EARTHSCOPE_OPEN_NETWORKS`, verified against the live bucket
   listing on 2026-09-09).
2. **FDSN web service** (`backend="fdsn", providers="EARTHSCOPE"`) — any
   logged-in account, any network. No S3 role required.
3. **Restricted S3** (`backend="s3_auth"`) — every network outside the Open
   Data set, from the `earthscope-mseed-v2` access point. Needs
   `earthscope-sdk>=1.8` and the `s3-miniseed-v2` role. Credentials are
   scoped per network, and per network-year for temporary FDSN codes;
   `seisfetch` exchanges one credential per scope and remembers EarthScope's
   refusals, so a denied network is asked about once rather than once per
   day. Fastest and cheapest from `us-east-2`.

### Setup

```bash
pip install "seisfetch[auth]"
pip install earthscope-cli
es login
```

The `[auth]` extra installs `earthscope-sdk>=1.8`. Earlier SDKs cannot scope a
credential to a network, and `S3AuthClient` refuses to start on them.

### Verify (CLI)

```bash
es user get-profile                # prints your name, email, institution
```

### Verify (Python)

```python
from earthscope_sdk import EarthScopeClient

with EarthScopeClient() as client:
    print(client.user.get_profile())
    try:
        creds = client.user.get_aws_credentials(
            role="s3-miniseed-v2", network="FDSN:US"
        )
        print("S3 access to US granted:", creds.aws_access_key_id[:8])
    except Exception as exc:
        print("S3 access NOT granted:", type(exc).__name__, exc)
```

What the errors mean:

- `"You are not allowed to assume role 's3-miniseed'"` — the legacy v1 role,
  now retired. Upgrade `earthscope-sdk` and `seisfetch`; only v2 is used here.
- `UnauthorizedError` (HTTP 403) on a scope — your account may not read that
  network (or that network-year). Not retried. Use
  `backend="fdsn", providers="EARTHSCOPE"` in the meantime and email
  `data-help@earthscope.org`.
- `UnauthenticatedError` (HTTP 401) — the login token was rejected; run `es login`.
- HTTP 400 on a temporary code — the scope needs a year. `seisfetch` always
  sends one, so this points at a code EarthScope does not recognise.
- HTTP 404 — the archive has no such network-year; treated as no data.

`seisfetch` raises these as `seisfetch.CredentialError` (a `FetchError`) with
`.scope` and `.status` set.

### Headless / CI

```bash
es user get-refresh-token
export ES_OAUTH2__REFRESH_TOKEN="<your-refresh-token>"
```

Override the access point or the role with `EARTHSCOPE_S3_ACCESS_POINT` and
`EARTHSCOPE_ROLE` if EarthScope issues new ones.

## Architecture

```text
SeisfetchClient
|
+- get_raw()        -> raw miniSEED bytes
+- get_numpy()      -> TraceBundle (numpy)
+- get_xarray()     -> xarray.Dataset
+- get_waveforms()  -> ObsPy Stream
|
+- backend="s3_open"
|  +- SCEDC open bucket
|  +- NCEDC open bucket
|  +- GeoNet open bucket
|  +- EarthScope Open Data bucket (AK II IU N4 PB TA UU UW)
|  +- auto-routing by network code
|
+- backend="s3_auth"
|  +- EarthScope restricted access point via earthscope-sdk
|  |  (s3-miniseed-v2, credentials scoped per network / network-year)
|  +- Open Data networks read anonymously
|
+- backend="fdsn"
|  +- single-provider HTTP client
|  +- multi-provider fan-out client
|
+- backend="obspy_fdsn"
   +- ObsPy-backed fallback for harder FDSN cases
```

## Earth2Studio Compatibility

The package includes adapters in `seisfetch.earth2` for Earth2Studio-style usage:

- `SeismicDataSource` — wraps a bundle you already fetched
- `SeismicDataFrameSource` — sparse sensor table; `auto_coords=True` fills station lat/lon from the FDSN station service
- `SeisfetchLiveSource` — time-indexed `DataSource` that fetches on demand
- `bundle_to_earth2`

These are intended for:

- sparse sensor tables
- observation pipelines
- foundation-model data preparation
- Earth2Studio / digital twin workflows

Typical path:

```text
miniSEED -> numpy -> xarray / sparse dataframe -> Earth2Studio adapter
```

### Live source

`SeisfetchLiveSource` has the shape every other Earth2Studio source (GFS,
ERA5, ...) has: you call it with timestamps and it fetches, auto-routing per
network across all four archives and caching day bundles in memory. It reads
anonymously, so EarthScope networks outside the Open Data set are not
reachable from it.

```python
from datetime import datetime
from seisfetch.earth2 import SeisfetchLiveSource

source = SeisfetchLiveSource(
    channels=["CI.PASC..BHZ", "BK.PKD.00.BHZ", "II.PFO.00.BHZ"],
    window_s=3600,
    calibrate="gain",
)
da = source(datetime(2022, 1, 2, 6))   # -> (time, variable, sample) DataArray
```

Channels are `NET.STA.LOC.CHA` strings; the returned `variable` coordinate
spells them with underscores (`CI_PASC__BHZ`), which is also what the
optional `variable=` argument accepts. All channels in one call must share a
sampling rate — request mixed rates (a 40 sps `BHZ` alongside a 100 sps
`HHZ`) in separate calls.

Physical units are **required** — this source never returns raw counts:

- `calibrate="gain"` (default): divide by the channel's total sensitivity from
  the FDSN station service. Exact at the reference frequency, one metadata
  request per channel, no extra dependencies.
- `calibrate="response"`: full spectral deconvolution through
  `seisfetch.contrib.response` (StationXML fetch plus an evalresp-equivalent
  evaluator). Agrees with ObsPy to 7.6e-16 of peak amplitude on real data.

`fetch()` is genuinely async (`asyncio.to_thread`), so pipelines can overlap
this source with others.

## Recipes

A few common end-to-end tasks the package is designed for.

### Mine a single station-day to numpy

```python
from seisfetch import SeisfetchClient

bundle = SeisfetchClient(backend="s3_open").get_numpy(
    "CI", "ABL", channel="BHZ",
    starttime="2024-01-15T00:00:00",
    endtime="2024-01-16T00:00:00",
)
data = bundle.to_dict()["CI.ABL..BHZ"]   # int32 numpy array, full day
```

### Bulk fetch many station-channels in parallel

```python
requests = [
    {"network": "CI", "station": s, "channel": c,
     "starttime": "2025-04-14T17:07:30", "endtime": "2025-04-14T17:10:30"}
    for s in ["ABL", "SDD", "PASC"]
    for c in ["BHZ", "BHN", "BHE"]
]
client = SeisfetchClient(backend="s3_open")
summary = client.get_numpy_bulk(requests, max_workers=8)
print(summary.succeeded, "/", summary.total)
```

Missing station-channel combinations are reported in `summary.failed` and do
not abort the run.

### Archive-first with FDSN fallback

```python
from seisfetch import SeisfetchClient, earthscope_tier, route_network

def get_archive_first(net, sta, *, starttime, endtime, location="*", channel="*",
                       fallback="GEOFON"):
    dc = route_network(net)
    restricted = dc == "earthscope" and earthscope_tier(net) == "restricted"
    primary = "s3_auth" if restricted else "s3_open"
    try:
        return SeisfetchClient(backend=primary).get_numpy(
            net, sta, location=location, channel=channel,
            starttime=starttime, endtime=endtime,
        )
    except Exception:
        return SeisfetchClient(backend="fdsn", providers=fallback).get_numpy(
            net, sta, location=location, channel=channel,
            starttime=starttime, endtime=endtime,
        )
```

### Persist a multi-channel package to zarr with metadata sidecar

```python
from seisfetch import bundle_to_metadata_table, to_zarr, write_metadata_csv

metadata = bundle_to_metadata_table(bundle)
to_zarr(bundle, "package.zarr", metadata=metadata)
write_metadata_csv(metadata, "package.zarr")
```

The resulting `package.zarr/` contains channel groups plus a
`metadata/channel_table` group readable with `xarray.open_zarr(...,
group="metadata/channel_table")`.

### Earth2Studio sparse-sensor handoff

```python
from datetime import datetime
from seisfetch import SeismicDataFrameSource, bundle_to_xarray

ds = bundle_to_xarray(bundle)
df = SeismicDataFrameSource(bundle)(datetime(2024, 1, 15), list(ds.data_vars))
df[["time", "variable", "network", "station", "channel",
     "sampling_rate", "amplitude_rms", "num_samples"]].head()
```

## Command Line

Examples:

```bash
# SCEDC open S3 (no auth)
seisfetch download CI ABL -s 2024-01-15 -e 2024-01-15T01:00:00 -c BHZ -o data.mseed
seisfetch numpy    CI SDD -s 2024-06-01 -c BHZ -o data.npz
seisfetch zarr     CI ABL -s 2024-01-15 -c BHZ -o data.zarr

# EarthScope Open Data (no auth): AK II IU N4 PB TA UU UW
seisfetch download IU ANMO -s 2024-01-15 -e 2024-01-15T01:00:00 -c BHZ -o anmo.mseed

# EarthScope restricted networks (requires `es login` and the s3-miniseed-v2 role)
seisfetch download US NEW -s 2024-01-15 -e 2024-01-15T01:00:00 -c BHZ --backend s3_auth -o new.mseed

# Routing and provider info (--route names the EarthScope tier)
seisfetch info --route CI
seisfetch info --route US
seisfetch info --providers

# Bulk
seisfetch bulk requests.csv -o output/ -f npz
```

## Notebooks

See [notebooks/](notebooks/) for worked examples:

- [01_quickstart.ipynb](notebooks/01_quickstart.ipynb)
- [02_bulk_mining.ipynb](notebooks/02_bulk_mining.ipynb)
- [03_xarray_zarr_pipeline.ipynb](notebooks/03_xarray_zarr_pipeline.ipynb)
- [04_earth2studio_interop.ipynb](notebooks/04_earth2studio_interop.ipynb)
- [05_response_removal.ipynb](notebooks/05_response_removal.ipynb) — instrument response removal without ObsPy
- [06_cross_correlation_three_archives.ipynb](notebooks/06_cross_correlation_three_archives.ipynb) — four stations, three archives, two months of NoisePy cross-correlations

Notebook setup instructions are in [notebooks/README.md](notebooks/README.md).

## Tests

```bash
pixi run test
pixi run test-cov
pixi run test-int
```

## Dependencies

| Package | Status | Role |
|---|---|---|
| `numpy` | core | array container |
| `boto3` | core | S3 transport |
| `pymseed` | core | miniSEED decode |
| `httpx` | optional `[fdsn]` | FDSN HTTP client |
| `pandas` | optional `[pandas]` | canonical metadata tables and CSV export |
| `xarray` | optional `[xarray]` | labeled dataset output |
| `zarr` | optional `[zarr]` | persistent chunked storage |
| `obspy` | optional `[obspy]` | ObsPy interop and alternative FDSN backend |
| `earthscope-sdk` (>=1.8) | optional `[auth]` | EarthScope restricted-tier credentials |

See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for attribution and licenses.

Benchmarks with plots: [benchmarks/RESULTS.md](benchmarks/RESULTS.md) (or the
self-contained [RESULTS.html](benchmarks/RESULTS.html)). Changes:
[CHANGELOG.md](CHANGELOG.md). Response-removal tutorial:
[notebooks/05_response_removal.ipynb](notebooks/05_response_removal.ipynb).

## Citation

When using data accessed through `seisfetch`:

- EarthScope: cite the network operators and NSF SAGE facility
- SCEDC: doi:[10.7909/C3WD3xH1](https://doi.org/10.7909/C3WD3xH1)
- NCEDC: doi:[10.7932/NCEDC](https://doi.org/10.7932/NCEDC)
- GeoNet: GNS Science, [GeoNet open data](https://www.geonet.org.nz/data/supplementary/channels) (CC BY 4.0) — cite per GeoNet's data policy
- other FDSN providers: cite the underlying network/provider

Software references:

- `pymseed` / `libmseed`: EarthScope Data Services
- NoisePy S3 access pattern: Jiang & Denolle (2020), doi:[10.1785/0220190364](https://doi.org/10.1785/0220190364)
- ObsPy: Beyreuther et al. (2010), doi:[10.1785/gssrl.81.3.530](https://doi.org/10.1785/gssrl.81.3.530)

## License

`MIT AND LGPL-3.0-only`. The package is MIT ([LICENSE](LICENSE)) with one
exception: [`seisfetch/contrib/obspy_ports.py`](seisfetch/contrib/obspy_ports.py)
contains numerically exact translations of ObsPy routines and is
LGPL-3.0-only (ObsPy is © The ObsPy Development Team, LGPL v3). Using
seisfetch as a library is unaffected; redistributors of modified versions of
that one file must honor LGPL terms. Details in
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
