| name | obspy-data-api |
| version | 1.1.1 |
| description | Parses seismological formats (MiniSEED, SAC, QuakeML, StationXML) into ObsPy Stream/Trace, Catalog/Event, and Inventory objects. Use when ingesting waveforms, event catalogs, or station metadata for processing or SeisBench. Not for generic non-seismic time series (NumPy/pandas). Never pass untrusted paths or URLs straight to read(). |
| risk | safe |
| source | openrouter-deepsearch |
| date_added | 2026-06-16T00:00:00.000Z |
When to Use
Reach for the ObsPy data API when you need to turn heterogeneous seismological files into a small, consistent set of in-memory objects. Seismic data ships in dozens of historical formats (MiniSEED, SAC, GSE2, SEISAN, Q, and more), and metadata arrives as QuakeML or FDSN StationXML. ObsPy normalizes all of that into three parallel object hierarchies — waveforms (Stream/Trace), event catalogs (Catalog/Event), and station inventories (Inventory) — so the rest of your pipeline can target one stable API instead of writing a parser per format.
Trigger keywords: seismology, seismic, waveform, MiniSEED, SAC, GSE2, SEISAN, QuakeML, StationXML, FDSN, earthquake, seismogram, trace, stream, catalog, inventory, instrument response, ObsPy, SeisBench.
That normalization is the real value: once data is in a Trace, downstream consumers such as ObsPy's own signal-processing routines (filtering, response removal, resampling) or SeisBench's machine-learning models all expect the same shape. Use this API whenever you are:
- Ingesting seismic data for processing.
- Converting custom arrays into standard objects.
- Round-tripping metadata between formats.
Do Not Use
Prefer something lighter for generic, non-seismic time series. ObsPy pulls in a large scientific stack (NumPy, SciPy, and optional Matplotlib) and models domain concepts — networks, stations, channels, instrument responses, QuakeML event trees — that add friction when all you have is a plain array. For non-seismic signals, NumPy or pandas is simpler and clearer; only adopt ObsPy when you actually benefit from its format support or seismology helpers.
Do not trust read() to silently sort out unexpected inputs. It auto-detects a format by trying each registered reader in turn, so feeding it an unsupported or removed format produces confusing, slow-to-surface errors. Pin the formats you expect (see SUPPORTED_WAVEFORM_FORMATS below) so a bad input fails loudly and immediately instead of being mis-parsed.
Treat any caller-controlled path or URL as hostile. read(), read_events(), and read_inventory() all accept a URL or a local path through the same argument, and ObsPy will fetch http(s)/ftp URLs server-side (it downloads to a temp file whenever "://" appears near the start of the string). If part of that string comes from an untrusted source, an attacker can request file:///etc/passwd, traverse upward with ../, or point the loader at an internal service (SSRF). That is why the helpers below validate the URL scheme against an allow-list and confine local reads to a known base directory rather than passing raw input straight to the reader.
Prerequisites
- Python 3.10+ (uses
from __future__ import annotations, frozenset[str] syntax, Path.is_relative_to).
- ObsPy installed:
pip install obspy (pulls NumPy, SciPy, and optional Matplotlib).
- No network access required for bundled example data (
read() with no arguments loads ObsPy's packaged sample seismogram).
- Windows host is primary (PowerShell). Local paths like
~\agent-skills\library\obspy-data-api\ are expected; the _looks_like_url helper correctly distinguishes Windows drive letters (e.g. C:\data\trace.mseed) from genuine URLs.
Procedure
1. Understand the Data Model
There is one mental model that covers the whole API: each kind of seismic information has a container that is iterated to yield element objects, plus a read_* entry point and a .write() method for round-tripping.
| Kind | Read with | Container | Element(s) |
|---|
| Waveforms | read() | Stream | Trace |
| Events | read_events() | Catalog | Event |
| Stations | read_inventory() | Inventory | Network → Station → Channel |
The containers are list-like because a single deployment naturally produces many elements — a three-component sensor yields three traces, a catalog holds many events, a network holds many stations. Processing helpers live on the element objects (and, for convenience, on Stream), so you can chain operations close to the data they act on.
2. Work with Waveform Data (Stream and Trace)
A Stream is a list-like collection of Trace objects, where each Trace is a gap-less, continuous time series plus its metadata.
Each Trace exposes:
data → a NumPy ndarray holding the actual samples.
stats → a dict-like Stats object holding metadata. Both stats.starttime and stats.endtime are UTCDateTime objects.
Trace.stats fields (and why they are grouped):
network, station, location, channel — the SEED identifiers that pin down the physical site and the specific instrument/component.
starttime, sampling_rate, delta, endtime, npts — these are interrelated: given starttime, sampling_rate (or its inverse delta), and npts (sample count), ObsPy derives endtime. Setting one recomputes the others, which is why you change timing through these fields rather than editing endtime directly.
Common Trace methods (each mutates the trace in place, so copy first if you need the original):
taper() — applies a window taper to reduce edge effects before filtering.
filter() — applies a frequency-domain or IIR filter.
resample() — resamples the data in the frequency domain.
integrate() — integrates with respect to time (e.g. velocity → displacement).
remove_response() — deconvolves the instrument response to recover ground motion in physical units.
3. Work with Event Metadata (Catalog and Event)
Event metadata follows the de-facto standard QuakeML. Use read_events() to load and Catalog.write() to export.
Hierarchy: Catalog → events → Event (multiple)
An Event is a tree, because a single seismic event can have several competing solutions (different agencies, methods, or revisions):
origins → Origin (multiple): time, latitude, longitude, depth (in meters), depth_type, quality, evaluation_mode, evaluation_status, creation_info, and the arrivals/comments containers.
magnitudes → Magnitude (multiple): mag, magnitude_type, station_count, azimuthal_gap, evaluation_mode, evaluation_status, creation_info.
picks → Pick (multiple): individual phase arrival picks (time, waveform_id, phase_hint, polarity, evaluation_mode).
focal_mechanisms → FocalMechanism (multiple): nodal_planes, principal_axes, moment_tensor, evaluation_mode.
- Plus
amplitudes, station_magnitudes, event_descriptions, comments, and the event_type/creation_info fields.
Because there can be many solutions, Event also stores preferred_origin_id, preferred_magnitude_id, and preferred_focal_mechanism_id, with helper methods preferred_origin(), preferred_magnitude(), and preferred_focal_mechanism() to fetch the chosen one. Prefer those helpers over indexing origins[0], which is not guaranteed to be the authoritative solution.
4. Work with Station Metadata (Inventory)
Station metadata follows FDSN StationXML, the human-readable XML replacement for Dataless SEED. Use read_inventory() to load and Inventory.write() to export.
Hierarchy: Inventory → networks → Network → stations → Station → channels → Channel
- Network:
code, description, start_date, end_date, restricted_status, total_number_of_stations, operators, source_id, and the stations container.
- Station:
code, latitude, longitude, elevation, site, creation_date, termination_date, start_date, end_date, description, and the channels container.
- Channel:
code, location_code, latitude, longitude, elevation, depth, azimuth, dip, sample_rate, sensor, data_logger, response, start_date, end_date.
The four-level nesting exists because instruments move and get replaced over time: a Channel's start_date/end_date scope a particular sensor at a particular orientation, and response carries the calibration needed by Trace.remove_response().
5. Load Data Safely
The functions below wrap the read_* entry points with explicit typing, strict parameter validation, and defensive error handling. The validation is not ceremony: it is what stops untrusted input from escaping the data sandbox or reaching the network.
from __future__ import annotations
from pathlib import Path
from typing import Final
from urllib.parse import urlparse
from obspy import Catalog, Inventory, Stream, read, read_events, read_inventory
SUPPORTED_WAVEFORM_FORMATS: Final[frozenset[str]] = frozenset(
{"MSEED", "SAC", "GSE2", "SEISAN", "Q", "SH_ASC", "SLIST", "TSPAIR", "WAV"}
)
ALLOWED_URL_SCHEMES: Final[frozenset[str]] = frozenset({"http", "https"})
def _looks_like_url(source: str) -> bool:
"""Return True only for genuine network URLs.
A single-character "scheme" is really a Windows drive letter (for example
``C:\\data\\trace.mseed``), not a URL, so we require a multi-character
scheme *and* a network location.
Misclassifying a local path as a URL would skip the path-traversal check.
"""
parsed = urlparse(source)
(parsed.scheme) > (parsed.netloc)
() -> Path:
base = base_dir.expanduser().resolve(strict=)
src = Path(source)
candidate = (src src.is_absolute() base / src).resolve()
candidate.is_relative_to(base):
ValueError()
candidate.is_file():
FileNotFoundError()
candidate
() -> :
(source, (, Path)):
TypeError()
raw = (source)
(source, ) _looks_like_url(raw):
scheme = urlparse(raw).scheme.lower()
scheme ALLOWED_URL_SCHEMES:
ValueError(
)
raw
base_dir :
ValueError()
(_resolve_local_path(source, base_dir))
() -> Stream:
fmt :
(fmt, ):
TypeError()
fmt = fmt.upper()
fmt SUPPORTED_WAVEFORM_FORMATS:
ValueError(
)
target = _resolve_source(source, base_dir)
:
stream: Stream = read(target, =fmt)
FileNotFoundError:
(TypeError, ValueError, OSError) exc:
ValueError() exc
(stream) == :
ValueError()
stream
() -> Catalog:
target = _resolve_source(source, base_dir)
:
catalog: Catalog = read_events(target)
FileNotFoundError:
(TypeError, ValueError, OSError) exc:
ValueError() exc
catalog
() -> Inventory:
target = _resolve_source(source, base_dir)
:
inventory: Inventory = read_inventory(target)
FileNotFoundError:
(TypeError, ValueError, OSError) exc:
ValueError() exc
inventory
6. Process Waveforms
Summaries and transforms should be explicitly typed and should never mutate the caller's data by surprise. The TypedDict gives downstream JSON/serialization code a precise shape, and preprocess_trace validates against the Nyquist frequency — a real constraint, since a band-pass edge at or above Nyquist is physically meaningless and will raise deep inside SciPy with a far less helpful message.
from __future__ import annotations
from typing import TypedDict
from obspy import Stream, Trace
class TraceSummary(TypedDict):
"""A JSON-serialisable description of a single trace."""
id: str
starttime: str
endtime: str
sampling_rate: float
npts: int
def describe_traces(stream: Stream) -> list[TraceSummary]:
"""Return one ``TraceSummary`` per trace in *stream*.
Iterating a ``Stream`` yields its ``Trace`` objects in order.
"""
if not isinstance(stream, Stream):
raise TypeError(f"expected Stream, got {type(stream).__name__}")
summaries: list[TraceSummary] = []
for trace in stream:
stats = trace.stats
summaries.append(
TraceSummary(
id=trace.id,
starttime=str(stats.starttime),
endtime=str(stats.endtime),
sampling_rate=float(stats.sampling_rate),
npts=int(stats.npts),
)
)
return summaries
def preprocess_trace(
trace: Trace,
*,
freqmin: ,
freqmax: ,
taper_fraction: = ,
) -> Trace:
(trace, Trace):
TypeError()
name, value ((, freqmin), (, freqmax),
(, taper_fraction)):
(value, (, )) (value, ):
TypeError()
nyquist = * (trace.stats.sampling_rate)
< freqmin < freqmax < nyquist:
ValueError(
)
<= taper_fraction <= :
ValueError()
work: Trace = trace.copy()
work.detrend()
work.taper(max_percentage=taper_fraction, =)
work.(
,
freqmin=freqmin,
freqmax=freqmax,
corners=,
zerophase=,
)
work
Examples
Calling read() with no arguments loads ObsPy's bundled three-component example seismogram, so the snippet below needs no files or network access. To read your own data, pass a path/URL to load_waveform() from above instead.
from obspy import Stream, Trace, read
stream: Stream = read()
print(stream)
The output lists every trace with its SEED id, time span, sampling rate, and sample count:
3 Trace(s) in Stream:
BW.RJOB..EHZ | 2009-08-24T00:20:03.000000Z - 2009-08-24T00:20:32.990000Z | 100.0 Hz, 3000 samples
BW.RJOB..EHN | 2009-08-24T00:20:03.000000Z - 2009-08-24T00:20:32.990000Z | 100.0 Hz, 3000 samples
BW.RJOB..EHE | 2009-08-24T00:20:03.000000Z - 2009-08-24T00:20:32.990000Z | 100.0 Hz, 3000 samples
Select a single Trace by index and inspect its metadata and samples:
trace: Trace = stream[0]
print(trace.id)
print(trace.stats.sampling_rate)
print(trace.stats.delta)
print(trace.stats.npts)
print(trace.data.shape)
print(trace.data.dtype)
first_five = trace.data[:5]
last_five = trace.data[-5:]
print(first_five)
print(last_five)
Printing trace.stats shows the core metadata fields. The location code here is the empty string (an unset SEED location):
network: BW
station: RJOB
location:
channel: EHZ
starttime: 2009-08-24T00:20:03.000000Z
endtime: 2009-08-24T00:20:32.990000Z
sampling_rate: 100.0
delta: 0.01
npts: 3000
calib: 1.0
trace.stats.starttime is a UTCDateTime, not a string, so it supports arithmetic and comparison:
from obspy import UTCDateTime
start: UTCDateTime = trace.stats.starttime
print(repr(start))
print(start + 10)
print(trace.stats.endtime - start)
Note: the in-memory default example does not set trace.stats._format (accessing it raises KeyError). When you instead read a real file, ObsPy adds a _format key (e.g. "MSEED") plus a nested, format-specific AttribDict holding that format's header fields.
Pitfalls
-
Never pass unvalidated user input to read(), read_events(), or read_inventory(). These functions accept URLs and local paths through the same argument. ObsPy fetches http(s)/ftp URLs server-side whenever "://" appears near the start of the string. An attacker can exploit this with file:///etc/passwd, ../ path traversal, or SSRF against internal services. Always use the _resolve_source helper with an allow-list of URL schemes (http, https only) and a base_dir sandbox for local reads.
-
Never let read() auto-detect formats on untrusted input. Auto-detection tries every registered reader in turn, producing confusing, slow-to-surface errors on unsupported formats. Always pass an explicit fmt from SUPPORTED_WAVEFORM_FORMATS so unknown input fails loudly and immediately.
-
Windows drive letters are not URL schemes. C:\data\trace.mseed has a single-character "scheme" (C) and no netloc. The _looks_like_url helper correctly rejects this as a URL by requiring len(parsed.scheme) > 1 and bool(parsed.netloc). Without this check, a Windows path would be misclassified as a URL and skip the path-traversal guard.
-
Trace methods mutate in place. detrend(), taper(), filter(), resample(), integrate(), and remove_response() all modify the trace's data array directly. Always call trace.copy() first if the caller or any other code still holds a reference to the original.
-
Do not index origins[0] for the authoritative solution. An Event can have multiple competing origins from different agencies. Use event.preferred_origin(), event.preferred_magnitude(), and event.preferred_focal_mechanism() instead.
-
Event depths are in meters, not kilometers. QuakeML specifies depth in meters. Mixing this up introduces a factor-of-1000 error.
Verification
Confirm the API behaves as expected before relying on it. All three smoke tests run offline since the no-argument readers load ObsPy's packaged sample data.
Run the test suite:
python -m pytest test_obspy_data_api.py -v
Or run directly:
python test_obspy_data_api.py
Expected output: all three tests pass with OK:
test_read_events_returns_catalog ... ok
test_read_inventory_returns_inventory ... ok
test_read_waveform_returns_non_empty_stream ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.XXXs
OK
Verification checklist:
Full test file:
import unittest
from obspy import (
Catalog,
Inventory,
Stream,
read,
read_events,
read_inventory,
)
class TestObsPyDataAPI(unittest.TestCase):
"""Smoke tests over ObsPy's bundled example data (no network required)."""
def test_read_waveform_returns_non_empty_stream(self) -> None:
stream: Stream = read()
self.assertIsInstance(stream, Stream)
self.assertEqual(len(stream), 3)
self.assertEqual(stream[0].stats.npts, 3000)
self.assertAlmostEqual(float(stream[0].stats.sampling_rate), 100.0)
def test_read_events_returns_catalog(self) -> None:
catalog: Catalog = read_events()
self.assertIsInstance(catalog, Catalog)
self.assertGreater(len(catalog), 0)
def test_read_inventory_returns_inventory(self) -> None:
inventory: Inventory = read_inventory()
self.assertIsInstance(inventory, Inventory)
self.assertGreater(len(inventory.networks), 0)
if __name__ == "__main__":
unittest.main()
Classes & Functions
| Class/Function | Description |
|---|
read | Read waveform files (or URLs) into an ObsPy Stream object. |
Stream | List-like container of multiple ObsPy Trace objects. |
Trace | A continuous time series (data) plus its metadata (stats). |
Stats | Dict-like header container for a Trace (obspy.core.trace.Stats). |
UTCDateTime | A UTC-based datetime supporting arithmetic and comparison. |
read_events | Read event files (or URLs) into an ObsPy Catalog object. |
Catalog | Container for Event objects. |
Event | A seismic event (not necessarily a tectonic earthquake). |
read_inventory | Read station metadata files (or URLs) into an Inventory. |
Inventory | Root of the Network → Station → Channel hierarchy. |
Modules
| Module | Description |
|---|
obspy.core.trace | Handles Trace and Stats objects. |
obspy.core.stream | Handles Stream objects. |
obspy.core.utcdatetime | Provides the UTC-based UTCDateTime class. |
obspy.core.event | Handles event metadata (Catalog, Event, and friends). |
obspy.core.inventory | Handles station metadata (Inventory and friends). |
obspy.core.util | Various ObsPy utilities, including the format readers. |
obspy.core.preview | Tools for creating and merging waveform previews. |
Related skills
The ObsPy data API is the on-ramp to the rest of the seismology toolchain. Once data is in Stream/Trace form, it feeds ObsPy's own signal-processing routines (filtering, instrument-response removal, resampling) and machine-learning frameworks such as SeisBench, whose modeling API consumes Stream objects directly. The Inventory produced by read_inventory() supplies the instrument responses those processing steps require.