Skip to content

pysmo.classes

Concrete classes compatible with pysmo types.

The pysmo.classes module provides classes that implement one or more pysmo protocol types. These classes can be used directly with any pysmo function or tool that operates on pysmo types.

Each class is designed with the protocol(s) it implements in mind, not to reproduce its native format's full specification. The scope isn't strictly limited to protocol attributes — pysmo.classes.StationXML, for example, also carries epoch bookkeeping (start_date/end_date) and a nested instrument response — but the protocol is the organising goal, not fidelity to the format. Reconstructing a complete file for every supported format is explicitly not a goal: where a class supports writing, the guarantee is only that the output round-trips through that same class's own reader, not that it satisfies the format's full external specification.

Classes:

Name Description
GeoCsvSeismogram

Import/export class for seismograms in the GeoCSV timeseries format.

MSeed

Import/export class for one contiguous miniSEED trace segment.

QuakeML

Import class for FDSN QuakeML event metadata.

SAC

Access and modify data stored in SAC files.

SacEvent

Helper class for SAC event attributes.

SacPZ

Import class for SAC PZ (pole-zero) files.

SacSeismogram

Helper class for SAC seismogram attributes.

SacStation

Helper class for SAC station attributes.

SacTimestamps

Helper class to access times stored in SAC headers as Timestamp objects.

StationXML

Import class for FDSN StationXML station metadata.

Functions:

Name Description
resolve_epochs

Collapse station epochs to the one per NSLC valid at a given time.

GeoCsvSeismogram

Bases: SeismogramEndtimeMixin

Import/export class for seismograms in the GeoCSV timeseries format.

Reads a waveform from the timeseries flavour of GeoCSV and exposes it as a Seismogram-compatible object.

This class is intended as a data-ingestion step. Once loaded, use clone_to_mini to convert the waveform to a MiniSeismogram, which can then be passed to copy_from_mini to populate another object such as a SAC instance. Use write to serialise the instance back to a GeoCSV 2.0 file, or pysmo.lib.io.write_geocsv to write one or more Seismogram-compatible objects in a single call.

Examples:

>>> from pysmo.classes import GeoCsvSeismogram
>>> text = '''\
... # dataset: GeoCSV 2.0
... # delimiter: ,
... # field_unit: UTC, Counts
... # field_type: datetime, INTEGER
... # SID: IU_ANMO_00_LHZ
... # sample_count: 3
... # sample_rate_hz: 1.0
... # start_time: 2010-02-27T06:30:00Z
... Time, Sample
... 2010-02-27T06:30:00Z, -47297
... 2010-02-27T06:30:01Z, -47298
... 2010-02-27T06:30:02Z, -47299'''
>>> seismogram = GeoCsvSeismogram.from_text(text)
>>> seismogram.sourceid
'IU_ANMO_00_LHZ'
>>> seismogram.data
array([-47297., -47298., -47299.])
>>> seismogram.end_time
Timestamp('2010-02-27 06:30:02+0000', tz='UTC')
>>> import pathlib
>>> seismogram.write("out.geocsv"); recovered = GeoCsvSeismogram.from_text(pathlib.Path("out.geocsv").read_text())
>>> recovered.sourceid
'IU_ANMO_00_LHZ'
>>>

Methods:

Name Description
fetch

Fetch and parse a seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

from_text

Create a new instance from a GeoCSV text body.

write

Write this seismogram to a GeoCSV 2.0 file.

Attributes:

Name Type Description
begin_time UtcTimestamp

Seismogram begin time.

data NDArray[floating]

Seismogram data.

delta PositiveTimedelta

Seismogram sampling interval.

sample_count int

Number of samples, always equal to len(data).

sourceid str

FDSN Source Identifier as carried in the GeoCSV SID header.

Source code in src/pysmo/classes/_geocsv.py
@define(kw_only=True)
class GeoCsvSeismogram(SeismogramEndtimeMixin):
    r"""Import/export class for seismograms in the GeoCSV timeseries format.

    Reads a waveform from the timeseries flavour of
    [GeoCSV](https://ds.iris.edu/files/documents/GeoCSV.pdf) and exposes
    it as a [`Seismogram`][pysmo.Seismogram]-compatible object.

    This class is intended as a data-ingestion step. Once loaded, use
    [`clone_to_mini`][pysmo.functions.clone_to_mini] to convert the
    waveform to a [`MiniSeismogram`][pysmo.MiniSeismogram], which can then
    be passed to [`copy_from_mini`][pysmo.functions.copy_from_mini] to
    populate another object such as a [`SAC`][pysmo.classes.SAC] instance.
    Use [`write`][pysmo.classes.GeoCsvSeismogram.write] to serialise the
    instance back to a GeoCSV 2.0 file, or [`pysmo.lib.io.write_geocsv`][]
    to write one or more `Seismogram`-compatible objects in a single call.

    Examples:
        ```python
        >>> from pysmo.classes import GeoCsvSeismogram
        >>> text = '''\
        ... # dataset: GeoCSV 2.0
        ... # delimiter: ,
        ... # field_unit: UTC, Counts
        ... # field_type: datetime, INTEGER
        ... # SID: IU_ANMO_00_LHZ
        ... # sample_count: 3
        ... # sample_rate_hz: 1.0
        ... # start_time: 2010-02-27T06:30:00Z
        ... Time, Sample
        ... 2010-02-27T06:30:00Z, -47297
        ... 2010-02-27T06:30:01Z, -47298
        ... 2010-02-27T06:30:02Z, -47299'''
        >>> seismogram = GeoCsvSeismogram.from_text(text)
        >>> seismogram.sourceid
        'IU_ANMO_00_LHZ'
        >>> seismogram.data
        array([-47297., -47298., -47299.])
        >>> seismogram.end_time
        Timestamp('2010-02-27 06:30:02+0000', tz='UTC')
        >>> import pathlib
        >>> seismogram.write("out.geocsv"); recovered = GeoCsvSeismogram.from_text(pathlib.Path("out.geocsv").read_text())
        >>> recovered.sourceid
        'IU_ANMO_00_LHZ'
        >>>
        ```
    """

    begin_time: UtcTimestamp = field(
        converter=convert_to_utc_timestamp,
        on_setattr=setters.convert,
    )
    """Seismogram begin time."""

    delta: PositiveTimedelta = field(
        converter=convert_to_timedelta,
        validator=[
            validators.instance_of(pd.Timedelta),
            validators.gt(pd.Timedelta(0)),
        ],
        on_setattr=setters.pipe(setters.convert, setters.validate),
    )
    """Seismogram sampling interval."""

    data: npt.NDArray[np.floating] = field(
        converter=convert_to_ndarray,
        validator=validators.instance_of(np.ndarray),
        on_setattr=setters.pipe(setters.convert, setters.validate),
        eq=cmp_using(eq=np.array_equal),
    )
    """Seismogram data."""

    sourceid: str = field(
        validator=validators.instance_of(str),
        on_setattr=setters.validate,
    )
    """FDSN Source Identifier as carried in the GeoCSV `SID` header.

    Stored verbatim as parsed, e.g. `IU_ANMO_00_LHZ` — no `FDSN:` URN
    prefix, and the channel is not split into band/source/subsource. This
    differs from [`MSeed.sourceid`][pysmo.classes.MSeed.sourceid], which
    keeps the full URN form. This is parse-time metadata: it describes the
    GeoCSV data the instance was created from and is not updated when other
    attributes change.
    """

    @property
    def sample_count(self) -> int:
        """Number of samples, always equal to `len(data)`."""
        return len(self.data)

    @classmethod
    def from_text(cls, text: str) -> Self:
        """Create a new instance from a GeoCSV text body.

        The text may contain several timeseries datasets (the EarthScope
        dataselect service returns one dataset per contiguous segment);
        they are merged into a single continuous waveform.

        Args:
            text: GeoCSV text containing one or more timeseries datasets.

        Returns:
            A new GeoCsvSeismogram instance.

        Raises:
            ValueError: If the text contains no GeoCSV datasets, a dataset
                is not a valid timeseries, or the datasets cannot be merged
                into a continuous waveform (data gaps, differing channels
                or sample rates).
        """
        datasets = parse_geocsv(text)
        if not datasets:
            raise ValueError("No GeoCSV datasets found in text.")
        segment = merge_geocsv_timeseries(
            [extract_geocsv_timeseries(dataset) for dataset in datasets]
        )
        return cls(
            begin_time=segment.start_time,
            # integer ns keeps sub-microsecond precision the float form loses
            delta=pd.Timedelta(round(1_000_000_000 / segment.sample_rate_hz), "ns"),
            data=segment.data,
            sourceid=segment.sourceid,
        )

    @classmethod
    def fetch(
        cls, *, station: Station, starttime: pd.Timestamp, endtime: pd.Timestamp
    ) -> Self:
        """Fetch and parse a seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

        For a window relative to a predicted phase arrival instead, compute
        the window yourself (e.g. with
        [`pysmo.tools.traveltime.travel_times`][], which shows exactly this
        in its own Examples) and pass the resulting *starttime*/*endtime*
        here.

        Args:
            station: Any object satisfying the [`Station`][pysmo.Station]
                protocol. Provides the network, station code, location, and
                channel for the request.
            starttime: Start of the requested time window (UTC).
            endtime: End of the requested time window (UTC).

        Returns:
            A new GeoCsvSeismogram instance.

        Raises:
            ValueError: If no waveform data is returned for the given window,
                or the returned segments cannot be merged into a continuous
                trace (data gaps, differing channels or sample rates).
            urllib3.exceptions.ResponseError: If the dataselect web service
                returns an HTTP error.

        Examples:
            <!-- skip: start if(not run_real_web_requests) -->
            ```python
            >>> import pandas as pd
            >>> from pysmo import MiniStation
            >>> from pysmo.classes import GeoCsvSeismogram
            >>> station = MiniStation(
            ...     name="ANMO", network="IU", location="00", channel="LHZ",
            ...     latitude=34.945981, longitude=-106.457133,
            ... )
            >>> seismogram = GeoCsvSeismogram.fetch(
            ...     station=station,
            ...     starttime=pd.Timestamp("2010-02-27T06:44:00Z"),
            ...     endtime=pd.Timestamp("2010-02-27T06:54:00Z"),
            ... )
            >>>
            ```
            <!-- skip: end -->
        """
        starttime = convert_to_utc_timestamp(starttime)
        endtime = convert_to_utc_timestamp(endtime)
        waveform_bytes = fetch_geocsvseismogram(
            station=station, starttime=starttime, endtime=endtime
        )
        if not waveform_bytes.strip():
            raise ValueError(
                "No waveform data returned for "
                + f"{station.network}.{station.name}.{station.location}."
                + f"{station.channel} between {starttime} and {endtime}."
            )
        return cls.from_text(waveform_bytes.decode("utf-8"))

    def write(self, path: str | PathLike[str]) -> None:
        r"""Write this seismogram to a GeoCSV 2.0 file.

        Serialises the instance as a single GeoCSV 2.0 timeseries dataset.
        To write several seismograms into one multi-dataset file use
        [`pysmo.lib.io.write_geocsv`][] directly.

        Args:
            path: Destination file path. The file is written in UTF-8 text
                mode and any existing content is overwritten.

        Examples:
            ```python
            >>> import pathlib
            >>> from pysmo.classes import GeoCsvSeismogram
            >>> text = '''\
            ... # dataset: GeoCSV 2.0
            ... # delimiter: ,
            ... # field_unit: UTC, Counts
            ... # field_type: datetime, INTEGER
            ... # SID: IU_ANMO_00_LHZ
            ... # sample_count: 3
            ... # sample_rate_hz: 1.0
            ... # start_time: 2010-02-27T06:30:00Z
            ... Time, Sample
            ... 2010-02-27T06:30:00Z, -47297
            ... 2010-02-27T06:30:01Z, -47298
            ... 2010-02-27T06:30:02Z, -47299'''
            >>> seismogram = GeoCsvSeismogram.from_text(text)
            >>> seismogram.write("out.geocsv"); recovered = GeoCsvSeismogram.from_text(
            ...     pathlib.Path("out.geocsv").read_text()
            ... )
            >>> recovered.sourceid == seismogram.sourceid
            True
            >>>
            ```
        """
        write_geocsv(self, path)

begin_time class-attribute instance-attribute

begin_time: UtcTimestamp = field(
    converter=convert_to_utc_timestamp,
    on_setattr=setters.convert,
)

Seismogram begin time.

data class-attribute instance-attribute

Seismogram data.

delta class-attribute instance-attribute

Seismogram sampling interval.

sample_count property

sample_count: int

Number of samples, always equal to len(data).

sourceid class-attribute instance-attribute

sourceid: str = field(
    validator=validators.instance_of(str),
    on_setattr=setters.validate,
)

FDSN Source Identifier as carried in the GeoCSV SID header.

Stored verbatim as parsed, e.g. IU_ANMO_00_LHZ — no FDSN: URN prefix, and the channel is not split into band/source/subsource. This differs from MSeed.sourceid, which keeps the full URN form. This is parse-time metadata: it describes the GeoCSV data the instance was created from and is not updated when other attributes change.

fetch classmethod

fetch(
    *,
    station: Station,
    starttime: Timestamp,
    endtime: Timestamp
) -> Self

Fetch and parse a seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

For a window relative to a predicted phase arrival instead, compute the window yourself (e.g. with pysmo.tools.traveltime.travel_times, which shows exactly this in its own Examples) and pass the resulting starttime/endtime here.

Parameters:

Name Type Description Default
station Station

Any object satisfying the Station protocol. Provides the network, station code, location, and channel for the request.

required
starttime Timestamp

Start of the requested time window (UTC).

required
endtime Timestamp

End of the requested time window (UTC).

required

Returns:

Type Description
Self

A new GeoCsvSeismogram instance.

Raises:

Type Description
ValueError

If no waveform data is returned for the given window, or the returned segments cannot be merged into a continuous trace (data gaps, differing channels or sample rates).

ResponseError

If the dataselect web service returns an HTTP error.

Examples:

>>> import pandas as pd
>>> from pysmo import MiniStation
>>> from pysmo.classes import GeoCsvSeismogram
>>> station = MiniStation(
...     name="ANMO", network="IU", location="00", channel="LHZ",
...     latitude=34.945981, longitude=-106.457133,
... )
>>> seismogram = GeoCsvSeismogram.fetch(
...     station=station,
...     starttime=pd.Timestamp("2010-02-27T06:44:00Z"),
...     endtime=pd.Timestamp("2010-02-27T06:54:00Z"),
... )
>>>
Source code in src/pysmo/classes/_geocsv.py
@classmethod
def fetch(
    cls, *, station: Station, starttime: pd.Timestamp, endtime: pd.Timestamp
) -> Self:
    """Fetch and parse a seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

    For a window relative to a predicted phase arrival instead, compute
    the window yourself (e.g. with
    [`pysmo.tools.traveltime.travel_times`][], which shows exactly this
    in its own Examples) and pass the resulting *starttime*/*endtime*
    here.

    Args:
        station: Any object satisfying the [`Station`][pysmo.Station]
            protocol. Provides the network, station code, location, and
            channel for the request.
        starttime: Start of the requested time window (UTC).
        endtime: End of the requested time window (UTC).

    Returns:
        A new GeoCsvSeismogram instance.

    Raises:
        ValueError: If no waveform data is returned for the given window,
            or the returned segments cannot be merged into a continuous
            trace (data gaps, differing channels or sample rates).
        urllib3.exceptions.ResponseError: If the dataselect web service
            returns an HTTP error.

    Examples:
        <!-- skip: start if(not run_real_web_requests) -->
        ```python
        >>> import pandas as pd
        >>> from pysmo import MiniStation
        >>> from pysmo.classes import GeoCsvSeismogram
        >>> station = MiniStation(
        ...     name="ANMO", network="IU", location="00", channel="LHZ",
        ...     latitude=34.945981, longitude=-106.457133,
        ... )
        >>> seismogram = GeoCsvSeismogram.fetch(
        ...     station=station,
        ...     starttime=pd.Timestamp("2010-02-27T06:44:00Z"),
        ...     endtime=pd.Timestamp("2010-02-27T06:54:00Z"),
        ... )
        >>>
        ```
        <!-- skip: end -->
    """
    starttime = convert_to_utc_timestamp(starttime)
    endtime = convert_to_utc_timestamp(endtime)
    waveform_bytes = fetch_geocsvseismogram(
        station=station, starttime=starttime, endtime=endtime
    )
    if not waveform_bytes.strip():
        raise ValueError(
            "No waveform data returned for "
            + f"{station.network}.{station.name}.{station.location}."
            + f"{station.channel} between {starttime} and {endtime}."
        )
    return cls.from_text(waveform_bytes.decode("utf-8"))

from_text classmethod

from_text(text: str) -> Self

Create a new instance from a GeoCSV text body.

The text may contain several timeseries datasets (the EarthScope dataselect service returns one dataset per contiguous segment); they are merged into a single continuous waveform.

Parameters:

Name Type Description Default
text str

GeoCSV text containing one or more timeseries datasets.

required

Returns:

Type Description
Self

A new GeoCsvSeismogram instance.

Raises:

Type Description
ValueError

If the text contains no GeoCSV datasets, a dataset is not a valid timeseries, or the datasets cannot be merged into a continuous waveform (data gaps, differing channels or sample rates).

Source code in src/pysmo/classes/_geocsv.py
@classmethod
def from_text(cls, text: str) -> Self:
    """Create a new instance from a GeoCSV text body.

    The text may contain several timeseries datasets (the EarthScope
    dataselect service returns one dataset per contiguous segment);
    they are merged into a single continuous waveform.

    Args:
        text: GeoCSV text containing one or more timeseries datasets.

    Returns:
        A new GeoCsvSeismogram instance.

    Raises:
        ValueError: If the text contains no GeoCSV datasets, a dataset
            is not a valid timeseries, or the datasets cannot be merged
            into a continuous waveform (data gaps, differing channels
            or sample rates).
    """
    datasets = parse_geocsv(text)
    if not datasets:
        raise ValueError("No GeoCSV datasets found in text.")
    segment = merge_geocsv_timeseries(
        [extract_geocsv_timeseries(dataset) for dataset in datasets]
    )
    return cls(
        begin_time=segment.start_time,
        # integer ns keeps sub-microsecond precision the float form loses
        delta=pd.Timedelta(round(1_000_000_000 / segment.sample_rate_hz), "ns"),
        data=segment.data,
        sourceid=segment.sourceid,
    )

write

write(path: str | PathLike[str]) -> None

Write this seismogram to a GeoCSV 2.0 file.

Serialises the instance as a single GeoCSV 2.0 timeseries dataset. To write several seismograms into one multi-dataset file use pysmo.lib.io.write_geocsv directly.

Parameters:

Name Type Description Default
path str | PathLike[str]

Destination file path. The file is written in UTF-8 text mode and any existing content is overwritten.

required

Examples:

>>> import pathlib
>>> from pysmo.classes import GeoCsvSeismogram
>>> text = '''\
... # dataset: GeoCSV 2.0
... # delimiter: ,
... # field_unit: UTC, Counts
... # field_type: datetime, INTEGER
... # SID: IU_ANMO_00_LHZ
... # sample_count: 3
... # sample_rate_hz: 1.0
... # start_time: 2010-02-27T06:30:00Z
... Time, Sample
... 2010-02-27T06:30:00Z, -47297
... 2010-02-27T06:30:01Z, -47298
... 2010-02-27T06:30:02Z, -47299'''
>>> seismogram = GeoCsvSeismogram.from_text(text)
>>> seismogram.write("out.geocsv"); recovered = GeoCsvSeismogram.from_text(
...     pathlib.Path("out.geocsv").read_text()
... )
>>> recovered.sourceid == seismogram.sourceid
True
>>>
Source code in src/pysmo/classes/_geocsv.py
def write(self, path: str | PathLike[str]) -> None:
    r"""Write this seismogram to a GeoCSV 2.0 file.

    Serialises the instance as a single GeoCSV 2.0 timeseries dataset.
    To write several seismograms into one multi-dataset file use
    [`pysmo.lib.io.write_geocsv`][] directly.

    Args:
        path: Destination file path. The file is written in UTF-8 text
            mode and any existing content is overwritten.

    Examples:
        ```python
        >>> import pathlib
        >>> from pysmo.classes import GeoCsvSeismogram
        >>> text = '''\
        ... # dataset: GeoCSV 2.0
        ... # delimiter: ,
        ... # field_unit: UTC, Counts
        ... # field_type: datetime, INTEGER
        ... # SID: IU_ANMO_00_LHZ
        ... # sample_count: 3
        ... # sample_rate_hz: 1.0
        ... # start_time: 2010-02-27T06:30:00Z
        ... Time, Sample
        ... 2010-02-27T06:30:00Z, -47297
        ... 2010-02-27T06:30:01Z, -47298
        ... 2010-02-27T06:30:02Z, -47299'''
        >>> seismogram = GeoCsvSeismogram.from_text(text)
        >>> seismogram.write("out.geocsv"); recovered = GeoCsvSeismogram.from_text(
        ...     pathlib.Path("out.geocsv").read_text()
        ... )
        >>> recovered.sourceid == seismogram.sourceid
        True
        >>>
        ```
    """
    write_geocsv(self, path)

MSeed

Bases: SeismogramEndtimeMixin

Import/export class for one contiguous miniSEED trace segment.

Wraps EarthScope's pymseed and exposes a single regularly-sampled segment as a Seismogram-compatible object. The pymseed trace-list hierarchy is flattened at read time: each contiguous segment becomes one MSeed.

miniSEED carries no station coordinates and no event data — only channel identity, timing and samples. MSeed exposes the network, station, location and channel codes as read-only properties derived from sourceid (an MSeed is a StationCode at runtime), but not Station. sourceid is the single authoritative identity value; to relabel, set it directly. For a Station, build a MiniStation (or fetch a StationXML) separately and combine.

Use from_file / from_bytes to read a single segment, their all_* counterparts to read every segment, and fetch to read directly from the EarthScope dataselect web service. Use write to serialise back to a miniSEED file, or pysmo.lib.io.write_mseed to write several segments in a single call.

Examples:

>>> from pysmo.classes import MSeed
>>> seismogram = MSeed.from_file("example.mseed")
>>> seismogram.sourceid
'FDSN:IU_ANMO_00_B_H_Z'
>>> seismogram.network, seismogram.name, seismogram.location, seismogram.channel
('IU', 'ANMO', '00', 'BHZ')
>>>

Methods:

Name Description
all_from_bytes

Create one instance per contiguous segment in miniSEED bytes.

all_from_file

Create one instance per contiguous segment in a miniSEED file.

fetch

Fetch and parse a seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

from_bytes

Create a new instance from miniSEED bytes holding exactly one contiguous segment.

from_file

Create a new instance from a miniSEED file holding exactly one contiguous segment.

write

Write this seismogram to a miniSEED file.

Attributes:

Name Type Description
begin_time UtcTimestamp

Seismogram begin time.

channel str

Channel code, derived from sourceid (read-only).

data NDArray[floating]

Seismogram data, always float64.

delta PositiveTimedelta

Seismogram sampling interval.

location str

Location code, derived from sourceid (read-only).

name str

Station code, derived from sourceid (read-only).

network str

Network code, derived from sourceid (read-only).

publication_version int

miniSEED publication (quality) version this segment was read from.

sample_count int

Number of samples, always equal to len(data).

sourceid str

FDSN Source Identifier this segment was read from.

Source code in src/pysmo/classes/_mseed.py
@define(kw_only=True)
class MSeed(SeismogramEndtimeMixin):
    """Import/export class for one contiguous miniSEED trace segment.

    Wraps EarthScope's `pymseed` and exposes a single regularly-sampled
    segment as a [`Seismogram`][pysmo.Seismogram]-compatible object. The
    `pymseed` trace-list hierarchy is flattened at read time: each
    contiguous segment becomes one `MSeed`.

    miniSEED carries no station coordinates and no event data — only
    channel identity, timing and samples. `MSeed` exposes the network,
    station, location and channel codes as read-only properties derived
    from `sourceid` (an `MSeed` is a [`StationCode`][pysmo.StationCode] at
    runtime), but not [`Station`][pysmo.Station]. `sourceid` is the single
    authoritative identity value; to relabel, set it directly. For a
    `Station`, build a [`MiniStation`][pysmo.MiniStation] (or fetch a
    [`StationXML`][pysmo.classes.StationXML]) separately and combine.

    Use [`from_file`][pysmo.classes.MSeed.from_file] /
    [`from_bytes`][pysmo.classes.MSeed.from_bytes] to read a single
    segment, their `all_*` counterparts to read every segment, and
    [`fetch`][pysmo.classes.MSeed.fetch] to read directly from the
    EarthScope dataselect web service. Use
    [`write`][pysmo.classes.MSeed.write] to serialise back to a miniSEED
    file, or [`pysmo.lib.io.write_mseed`][] to write several segments in a
    single call.

    Examples:
        ```python
        >>> from pysmo.classes import MSeed
        >>> seismogram = MSeed.from_file("example.mseed")
        >>> seismogram.sourceid
        'FDSN:IU_ANMO_00_B_H_Z'
        >>> seismogram.network, seismogram.name, seismogram.location, seismogram.channel
        ('IU', 'ANMO', '00', 'BHZ')
        >>>
        ```
    """

    begin_time: UtcTimestamp = field(
        converter=convert_to_utc_timestamp,
        on_setattr=setters.convert,
    )
    """Seismogram begin time."""

    delta: PositiveTimedelta = field(
        converter=convert_to_timedelta,
        validator=[
            validators.instance_of(pd.Timedelta),
            validators.gt(pd.Timedelta(0)),
        ],
        on_setattr=setters.pipe(setters.convert, setters.validate),
    )
    """Seismogram sampling interval."""

    data: npt.NDArray[np.floating] = field(
        converter=_convert_mseed_data,
        validator=validators.instance_of(np.ndarray),
        on_setattr=setters.pipe(setters.convert, setters.validate),
        eq=cmp_using(eq=np.array_equal),
    )
    """Seismogram data, always `float64`."""

    sourceid: str = field(
        validator=validators.instance_of(str),
        on_setattr=setters.validate,
    )
    """FDSN Source Identifier this segment was read from.

    The full URN form as carried in miniSEED and returned by `pymseed`,
    e.g. `FDSN:IU_ANMO_00_B_H_Z` — the `FDSN:` prefix is kept and the
    channel is split into band/source/subsource. This differs from
    [`GeoCsvSeismogram.sourceid`][pysmo.classes.GeoCsvSeismogram.sourceid],
    which keeps the shorter GeoCSV `SID` header form. This is parse-time
    metadata and is not updated when other attributes change.
    """

    publication_version: int = field(converter=int)
    """miniSEED publication (quality) version this segment was read from."""

    @property
    def network(self) -> str:
        """Network code, derived from `sourceid` (read-only)."""
        return pymseed.sourceid2nslc(self.sourceid)[0]

    @property
    def name(self) -> str:
        """Station code, derived from `sourceid` (read-only)."""
        return pymseed.sourceid2nslc(self.sourceid)[1]

    @property
    def location(self) -> str:
        """Location code, derived from `sourceid` (read-only)."""
        return pymseed.sourceid2nslc(self.sourceid)[2]

    @property
    def channel(self) -> str:
        """Channel code, derived from `sourceid` (read-only)."""
        return pymseed.sourceid2nslc(self.sourceid)[3]

    @property
    def sample_count(self) -> int:
        """Number of samples, always equal to `len(data)`."""
        return len(self.data)

    @classmethod
    def _from_segment(
        cls, sourceid: str, publication_version: int, segment: MS3TraceSeg
    ) -> Self:
        return cls(
            begin_time=pd.Timestamp(segment.starttime, unit="ns", tz="UTC"),
            delta=pd.Timedelta(seconds=1.0 / segment.samprate),
            data=segment.np_datasamples,
            sourceid=sourceid,
            publication_version=publication_version,
        )

    @classmethod
    def _all_from_tracelist(cls, tracelist: MS3TraceList) -> list[Self]:
        return [cls._from_segment(*entry) for entry in _segments(tracelist)]

    @classmethod
    def _one_from_tracelist(cls, tracelist: MS3TraceList, source: str) -> Self:
        segments = cls._all_from_tracelist(tracelist)
        if len(segments) == 1:
            return segments[0]
        if not segments:
            raise ValueError(f"No miniSEED data found in {source}.")
        segment_lines = "\n".join(
            f"  {s.network}.{s.name}.{s.location}.{s.channel}  "
            + f"{s.begin_time} -- {s.end_time}"
            for s in segments
        )
        raise ValueError(
            f"{source} holds {len(segments)} contiguous segments; "
            + "MSeed.from_bytes()/from_file() requires exactly one. Use "
            + "MSeed.all_from_bytes()/all_from_file() instead. Segments found:\n"
            + f"{segment_lines}"
        )

    @classmethod
    def from_file(cls, filename: str | PathLike[str]) -> Self:
        """Create a new instance from a miniSEED file holding exactly one contiguous segment.

        Args:
            filename: Path to the miniSEED file to read.

        Returns:
            A new MSeed instance.

        Raises:
            ValueError: If the file holds zero, or more than one, contiguous
                segment (a data gap, or more than one channel).
            pymseed.MiniSEEDError: If the file cannot be read as miniSEED.
        """
        with MS3TraceList.from_file(filename, unpack_data=True) as tracelist:
            return cls._one_from_tracelist(tracelist, f"file {filename!r}")

    @classmethod
    def all_from_file(cls, filename: str | PathLike[str]) -> list[Self]:
        """Create one instance per contiguous segment in a miniSEED file.

        Args:
            filename: Path to the miniSEED file to read.

        Returns:
            One MSeed instance per contiguous segment, grouped by source
            identifier and ordered by time within each. Empty if the file
            holds no data.

        Raises:
            pymseed.MiniSEEDError: If the file cannot be read as miniSEED.
        """
        with MS3TraceList.from_file(filename, unpack_data=True) as tracelist:
            return cls._all_from_tracelist(tracelist)

    @classmethod
    def from_bytes(cls, data: bytes) -> Self:
        """Create a new instance from miniSEED bytes holding exactly one contiguous segment.

        Args:
            data: Raw miniSEED bytes.

        Returns:
            A new MSeed instance.

        Raises:
            ValueError: If the data holds zero, or more than one, contiguous
                segment (a data gap, or more than one channel).
            pymseed.MiniSEEDError: If the data cannot be read as miniSEED.
        """
        with MS3TraceList.from_buffer(data, unpack_data=True) as tracelist:
            return cls._one_from_tracelist(tracelist, "the given bytes")

    @classmethod
    def all_from_bytes(cls, data: bytes) -> list[Self]:
        """Create one instance per contiguous segment in miniSEED bytes.

        Args:
            data: Raw miniSEED bytes.

        Returns:
            One MSeed instance per contiguous segment, grouped by source
            identifier and ordered by time within each. Empty if the data
            holds no segments.

        Raises:
            pymseed.MiniSEEDError: If the data cannot be read as miniSEED.
        """
        with MS3TraceList.from_buffer(data, unpack_data=True) as tracelist:
            return cls._all_from_tracelist(tracelist)

    @classmethod
    def fetch(
        cls, *, station: Station, starttime: pd.Timestamp, endtime: pd.Timestamp
    ) -> Self:
        """Fetch and parse a seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

        For a window relative to a predicted phase arrival instead, compute
        the window yourself (e.g. with
        [`pysmo.tools.traveltime.travel_times`][], which shows exactly this
        in its own Examples) and pass the resulting *starttime*/*endtime*
        here.

        To fetch once and interpret later (e.g. offline, or without
        repeating the network request), use
        [`pysmo.tools.web.fetch_mseed`][] and
        [`from_bytes`][pysmo.classes.MSeed.from_bytes] /
        [`all_from_bytes`][pysmo.classes.MSeed.all_from_bytes] directly
        instead.

        Args:
            station: Any object satisfying the [`Station`][pysmo.Station]
                protocol. Provides the network, station code, location, and
                channel for the request.
            starttime: Start of the requested time window (UTC).
            endtime: End of the requested time window (UTC).

        Returns:
            A new MSeed instance.

        Raises:
            ValueError: If no waveform data is returned for the given
                window, or more than one contiguous segment is returned
                (a data gap, or a wildcarded channel/location code matching
                more than one channel).
            urllib3.exceptions.ResponseError: If the dataselect web service
                returns an HTTP error.

        Examples:
            <!-- skip: start if(not run_real_web_requests) -->
            ```python
            >>> import pandas as pd
            >>> from pysmo import MiniStation
            >>> from pysmo.classes import MSeed
            >>> station = MiniStation(
            ...     name="ANMO", network="IU", location="00", channel="LHZ",
            ...     latitude=34.945981, longitude=-106.457133,
            ... )
            >>> seismogram = MSeed.fetch(
            ...     station=station,
            ...     starttime=pd.Timestamp("2010-02-27T06:44:00Z"),
            ...     endtime=pd.Timestamp("2010-02-27T06:54:00Z"),
            ... )
            >>>
            ```
            <!-- skip: end -->
        """
        starttime = convert_to_utc_timestamp(starttime)
        endtime = convert_to_utc_timestamp(endtime)
        waveform_bytes = fetch_mseed(
            station=station, starttime=starttime, endtime=endtime
        )
        if not waveform_bytes:
            raise ValueError(
                "No waveform data returned for "
                + f"{station.network}.{station.name}.{station.location}."
                + f"{station.channel} between {starttime} and {endtime}."
            )
        return cls.from_bytes(waveform_bytes)

    def write(self, path: str | PathLike[str]) -> None:
        """Write this seismogram to a miniSEED file.

        Samples are written as `float64` (uncompressed) and the
        publication version is set to 1 — `sourceid` and timing are
        preserved, `publication_version` is not. For STEIM integer
        compression, or to write several seismograms into one file, use
        [`pysmo.lib.io.write_mseed`][] directly.

        Args:
            path: Destination file path. Any existing content is
                overwritten.
        """
        identity = MiniStationCode(
            network=self.network,
            name=self.name,
            location=self.location,
            channel=self.channel,
        )
        write_mseed([(identity, self)], path)

begin_time class-attribute instance-attribute

begin_time: UtcTimestamp = field(
    converter=convert_to_utc_timestamp,
    on_setattr=setters.convert,
)

Seismogram begin time.

channel property

channel: str

Channel code, derived from sourceid (read-only).

data class-attribute instance-attribute

data: NDArray[floating] = field(
    converter=_convert_mseed_data,
    validator=validators.instance_of(np.ndarray),
    on_setattr=setters.pipe(
        setters.convert, setters.validate
    ),
    eq=cmp_using(eq=np.array_equal),
)

Seismogram data, always float64.

delta class-attribute instance-attribute

Seismogram sampling interval.

location property

location: str

Location code, derived from sourceid (read-only).

name property

name: str

Station code, derived from sourceid (read-only).

network property

network: str

Network code, derived from sourceid (read-only).

publication_version class-attribute instance-attribute

publication_version: int = field(converter=int)

miniSEED publication (quality) version this segment was read from.

sample_count property

sample_count: int

Number of samples, always equal to len(data).

sourceid class-attribute instance-attribute

sourceid: str = field(
    validator=validators.instance_of(str),
    on_setattr=setters.validate,
)

FDSN Source Identifier this segment was read from.

The full URN form as carried in miniSEED and returned by pymseed, e.g. FDSN:IU_ANMO_00_B_H_Z — the FDSN: prefix is kept and the channel is split into band/source/subsource. This differs from GeoCsvSeismogram.sourceid, which keeps the shorter GeoCSV SID header form. This is parse-time metadata and is not updated when other attributes change.

all_from_bytes classmethod

all_from_bytes(data: bytes) -> list[Self]

Create one instance per contiguous segment in miniSEED bytes.

Parameters:

Name Type Description Default
data bytes

Raw miniSEED bytes.

required

Returns:

Type Description
list[Self]

One MSeed instance per contiguous segment, grouped by source

list[Self]

identifier and ordered by time within each. Empty if the data

list[Self]

holds no segments.

Raises:

Type Description
MiniSEEDError

If the data cannot be read as miniSEED.

Source code in src/pysmo/classes/_mseed.py
@classmethod
def all_from_bytes(cls, data: bytes) -> list[Self]:
    """Create one instance per contiguous segment in miniSEED bytes.

    Args:
        data: Raw miniSEED bytes.

    Returns:
        One MSeed instance per contiguous segment, grouped by source
        identifier and ordered by time within each. Empty if the data
        holds no segments.

    Raises:
        pymseed.MiniSEEDError: If the data cannot be read as miniSEED.
    """
    with MS3TraceList.from_buffer(data, unpack_data=True) as tracelist:
        return cls._all_from_tracelist(tracelist)

all_from_file classmethod

all_from_file(filename: str | PathLike[str]) -> list[Self]

Create one instance per contiguous segment in a miniSEED file.

Parameters:

Name Type Description Default
filename str | PathLike[str]

Path to the miniSEED file to read.

required

Returns:

Type Description
list[Self]

One MSeed instance per contiguous segment, grouped by source

list[Self]

identifier and ordered by time within each. Empty if the file

list[Self]

holds no data.

Raises:

Type Description
MiniSEEDError

If the file cannot be read as miniSEED.

Source code in src/pysmo/classes/_mseed.py
@classmethod
def all_from_file(cls, filename: str | PathLike[str]) -> list[Self]:
    """Create one instance per contiguous segment in a miniSEED file.

    Args:
        filename: Path to the miniSEED file to read.

    Returns:
        One MSeed instance per contiguous segment, grouped by source
        identifier and ordered by time within each. Empty if the file
        holds no data.

    Raises:
        pymseed.MiniSEEDError: If the file cannot be read as miniSEED.
    """
    with MS3TraceList.from_file(filename, unpack_data=True) as tracelist:
        return cls._all_from_tracelist(tracelist)

fetch classmethod

fetch(
    *,
    station: Station,
    starttime: Timestamp,
    endtime: Timestamp
) -> Self

Fetch and parse a seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

For a window relative to a predicted phase arrival instead, compute the window yourself (e.g. with pysmo.tools.traveltime.travel_times, which shows exactly this in its own Examples) and pass the resulting starttime/endtime here.

To fetch once and interpret later (e.g. offline, or without repeating the network request), use pysmo.tools.web.fetch_mseed and from_bytes / all_from_bytes directly instead.

Parameters:

Name Type Description Default
station Station

Any object satisfying the Station protocol. Provides the network, station code, location, and channel for the request.

required
starttime Timestamp

Start of the requested time window (UTC).

required
endtime Timestamp

End of the requested time window (UTC).

required

Returns:

Type Description
Self

A new MSeed instance.

Raises:

Type Description
ValueError

If no waveform data is returned for the given window, or more than one contiguous segment is returned (a data gap, or a wildcarded channel/location code matching more than one channel).

ResponseError

If the dataselect web service returns an HTTP error.

Examples:

>>> import pandas as pd
>>> from pysmo import MiniStation
>>> from pysmo.classes import MSeed
>>> station = MiniStation(
...     name="ANMO", network="IU", location="00", channel="LHZ",
...     latitude=34.945981, longitude=-106.457133,
... )
>>> seismogram = MSeed.fetch(
...     station=station,
...     starttime=pd.Timestamp("2010-02-27T06:44:00Z"),
...     endtime=pd.Timestamp("2010-02-27T06:54:00Z"),
... )
>>>
Source code in src/pysmo/classes/_mseed.py
@classmethod
def fetch(
    cls, *, station: Station, starttime: pd.Timestamp, endtime: pd.Timestamp
) -> Self:
    """Fetch and parse a seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

    For a window relative to a predicted phase arrival instead, compute
    the window yourself (e.g. with
    [`pysmo.tools.traveltime.travel_times`][], which shows exactly this
    in its own Examples) and pass the resulting *starttime*/*endtime*
    here.

    To fetch once and interpret later (e.g. offline, or without
    repeating the network request), use
    [`pysmo.tools.web.fetch_mseed`][] and
    [`from_bytes`][pysmo.classes.MSeed.from_bytes] /
    [`all_from_bytes`][pysmo.classes.MSeed.all_from_bytes] directly
    instead.

    Args:
        station: Any object satisfying the [`Station`][pysmo.Station]
            protocol. Provides the network, station code, location, and
            channel for the request.
        starttime: Start of the requested time window (UTC).
        endtime: End of the requested time window (UTC).

    Returns:
        A new MSeed instance.

    Raises:
        ValueError: If no waveform data is returned for the given
            window, or more than one contiguous segment is returned
            (a data gap, or a wildcarded channel/location code matching
            more than one channel).
        urllib3.exceptions.ResponseError: If the dataselect web service
            returns an HTTP error.

    Examples:
        <!-- skip: start if(not run_real_web_requests) -->
        ```python
        >>> import pandas as pd
        >>> from pysmo import MiniStation
        >>> from pysmo.classes import MSeed
        >>> station = MiniStation(
        ...     name="ANMO", network="IU", location="00", channel="LHZ",
        ...     latitude=34.945981, longitude=-106.457133,
        ... )
        >>> seismogram = MSeed.fetch(
        ...     station=station,
        ...     starttime=pd.Timestamp("2010-02-27T06:44:00Z"),
        ...     endtime=pd.Timestamp("2010-02-27T06:54:00Z"),
        ... )
        >>>
        ```
        <!-- skip: end -->
    """
    starttime = convert_to_utc_timestamp(starttime)
    endtime = convert_to_utc_timestamp(endtime)
    waveform_bytes = fetch_mseed(
        station=station, starttime=starttime, endtime=endtime
    )
    if not waveform_bytes:
        raise ValueError(
            "No waveform data returned for "
            + f"{station.network}.{station.name}.{station.location}."
            + f"{station.channel} between {starttime} and {endtime}."
        )
    return cls.from_bytes(waveform_bytes)

from_bytes classmethod

from_bytes(data: bytes) -> Self

Create a new instance from miniSEED bytes holding exactly one contiguous segment.

Parameters:

Name Type Description Default
data bytes

Raw miniSEED bytes.

required

Returns:

Type Description
Self

A new MSeed instance.

Raises:

Type Description
ValueError

If the data holds zero, or more than one, contiguous segment (a data gap, or more than one channel).

MiniSEEDError

If the data cannot be read as miniSEED.

Source code in src/pysmo/classes/_mseed.py
@classmethod
def from_bytes(cls, data: bytes) -> Self:
    """Create a new instance from miniSEED bytes holding exactly one contiguous segment.

    Args:
        data: Raw miniSEED bytes.

    Returns:
        A new MSeed instance.

    Raises:
        ValueError: If the data holds zero, or more than one, contiguous
            segment (a data gap, or more than one channel).
        pymseed.MiniSEEDError: If the data cannot be read as miniSEED.
    """
    with MS3TraceList.from_buffer(data, unpack_data=True) as tracelist:
        return cls._one_from_tracelist(tracelist, "the given bytes")

from_file classmethod

from_file(filename: str | PathLike[str]) -> Self

Create a new instance from a miniSEED file holding exactly one contiguous segment.

Parameters:

Name Type Description Default
filename str | PathLike[str]

Path to the miniSEED file to read.

required

Returns:

Type Description
Self

A new MSeed instance.

Raises:

Type Description
ValueError

If the file holds zero, or more than one, contiguous segment (a data gap, or more than one channel).

MiniSEEDError

If the file cannot be read as miniSEED.

Source code in src/pysmo/classes/_mseed.py
@classmethod
def from_file(cls, filename: str | PathLike[str]) -> Self:
    """Create a new instance from a miniSEED file holding exactly one contiguous segment.

    Args:
        filename: Path to the miniSEED file to read.

    Returns:
        A new MSeed instance.

    Raises:
        ValueError: If the file holds zero, or more than one, contiguous
            segment (a data gap, or more than one channel).
        pymseed.MiniSEEDError: If the file cannot be read as miniSEED.
    """
    with MS3TraceList.from_file(filename, unpack_data=True) as tracelist:
        return cls._one_from_tracelist(tracelist, f"file {filename!r}")

write

write(path: str | PathLike[str]) -> None

Write this seismogram to a miniSEED file.

Samples are written as float64 (uncompressed) and the publication version is set to 1 — sourceid and timing are preserved, publication_version is not. For STEIM integer compression, or to write several seismograms into one file, use pysmo.lib.io.write_mseed directly.

Parameters:

Name Type Description Default
path str | PathLike[str]

Destination file path. Any existing content is overwritten.

required
Source code in src/pysmo/classes/_mseed.py
def write(self, path: str | PathLike[str]) -> None:
    """Write this seismogram to a miniSEED file.

    Samples are written as `float64` (uncompressed) and the
    publication version is set to 1 — `sourceid` and timing are
    preserved, `publication_version` is not. For STEIM integer
    compression, or to write several seismograms into one file, use
    [`pysmo.lib.io.write_mseed`][] directly.

    Args:
        path: Destination file path. Any existing content is
            overwritten.
    """
    identity = MiniStationCode(
        network=self.network,
        name=self.name,
        location=self.location,
        channel=self.channel,
    )
    write_mseed([(identity, self)], path)

QuakeML

Import class for FDSN QuakeML event metadata.

Reads the hypocentre and origin time of a seismic event from a QuakeML 1.2 document (as returned by any fdsnws-event service) and exposes it as an Event-compatible object. A QuakeML document commonly describes many events; from_bytes narrows to one, all_from_bytes returns every event found.

Only the preferred origin's hypocentre and time are read. Focal mechanisms, picks, arrivals, origin uncertainties and competing origin/magnitude solutions in the document are not represented.

An object satisfying the full Station or Event protocol for another data source is built separately (e.g. a MiniEvent, or via clone_to_mini); QuakeML does not fabricate one.

Examples:

>>> from pysmo.classes import QuakeML
>>> xml = b'''<?xml version="1.0"?>
... <q:quakeml xmlns="http://quakeml.org/xmlns/bed/1.2"
...            xmlns:q="http://quakeml.org/xmlns/quakeml/1.2">
...   <eventParameters publicID="smi:example/catalogue">
...     <event publicID="smi:example/event/1">
...       <description><text>Example</text></description>
...       <origin publicID="smi:example/origin/1">
...         <time><value>2010-02-27T06:34:11.53Z</value></time>
...         <latitude><value>-36.122</value></latitude>
...         <longitude><value>-72.898</value></longitude>
...         <depth><value>22900</value></depth>
...       </origin>
...       <magnitude publicID="smi:example/magnitude/1">
...         <mag><value>8.8</value></mag>
...         <type>Mw</type>
...       </magnitude>
...       <type>earthquake</type>
...     </event>
...   </eventParameters>
... </q:quakeml>'''
>>> event = QuakeML.from_bytes(xml)
>>> event.latitude, event.longitude, event.depth
(-36.122, -72.898, 22900.0)
>>> event.magnitude, event.magnitude_type
(8.8, 'Mw')
>>> event.public_id
'smi:example/event/1'
>>>

Methods:

Name Description
all_from_bytes

Create one instance per <event> in a QuakeML document.

all_from_query

Fetch and parse a catalogue of events from the USGS fdsnws-event service.

fetch

Fetch and parse a single event from the USGS fdsnws-event service.

from_bytes

Create a new instance from a QuakeML document, narrowing to one event.

Attributes:

Name Type Description
depth float

Hypocentre depth in metres, positive downwards, as recorded in the

description str | None

First event/description/text (e.g. a Flinn-Engdahl region or event

event_type str | None

QuakeML event/type (e.g. "earthquake", "explosion"), or None.

latitude float

Event latitude from -90 to 90 degrees.

longitude float

Event longitude from -180 to 180 degrees (-180 is stored as +180).

magnitude float | None

Preferred magnitude value, or None if the document has none.

magnitude_type str | None

Preferred magnitude type (e.g. "Mw"), or None.

public_id str

QuakeML publicID of the event this instance was parsed from,

time UtcTimestamp

Event origin time.

Source code in src/pysmo/classes/_quakeml.py
@define(kw_only=True)
class QuakeML:
    """Import class for FDSN QuakeML event metadata.

    Reads the hypocentre and origin time of a seismic event from a
    QuakeML 1.2 document (as returned by any `fdsnws-event` service) and
    exposes it as an [`Event`][pysmo.Event]-compatible object. A QuakeML
    document commonly describes many events;
    [`from_bytes`][pysmo.classes.QuakeML.from_bytes] narrows to one,
    [`all_from_bytes`][pysmo.classes.QuakeML.all_from_bytes] returns every
    event found.

    Only the preferred origin's hypocentre and time are read. Focal
    mechanisms, picks, arrivals, origin uncertainties and competing
    origin/magnitude solutions in the document are not represented.

    An object satisfying the full [`Station`][pysmo.Station] or
    [`Event`][pysmo.Event] protocol for another data source is built
    separately (e.g. a [`MiniEvent`][pysmo.MiniEvent], or via
    [`clone_to_mini`][pysmo.functions.clone_to_mini]); `QuakeML` does not
    fabricate one.

    Examples:
        ```python
        >>> from pysmo.classes import QuakeML
        >>> xml = b'''<?xml version="1.0"?>
        ... <q:quakeml xmlns="http://quakeml.org/xmlns/bed/1.2"
        ...            xmlns:q="http://quakeml.org/xmlns/quakeml/1.2">
        ...   <eventParameters publicID="smi:example/catalogue">
        ...     <event publicID="smi:example/event/1">
        ...       <description><text>Example</text></description>
        ...       <origin publicID="smi:example/origin/1">
        ...         <time><value>2010-02-27T06:34:11.53Z</value></time>
        ...         <latitude><value>-36.122</value></latitude>
        ...         <longitude><value>-72.898</value></longitude>
        ...         <depth><value>22900</value></depth>
        ...       </origin>
        ...       <magnitude publicID="smi:example/magnitude/1">
        ...         <mag><value>8.8</value></mag>
        ...         <type>Mw</type>
        ...       </magnitude>
        ...       <type>earthquake</type>
        ...     </event>
        ...   </eventParameters>
        ... </q:quakeml>'''
        >>> event = QuakeML.from_bytes(xml)
        >>> event.latitude, event.longitude, event.depth
        (-36.122, -72.898, 22900.0)
        >>> event.magnitude, event.magnitude_type
        (8.8, 'Mw')
        >>> event.public_id
        'smi:example/event/1'
        >>>
        ```
    """

    time: UtcTimestamp = field(
        converter=convert_to_utc_timestamp,
        on_setattr=setters.pipe(setters.convert, setters.validate),
    )
    """Event origin time."""

    latitude: float = field(
        converter=float,
        validator=[validators.ge(-90), validators.le(90)],
        on_setattr=setters.pipe(setters.convert, setters.validate),
    )
    """Event latitude from -90 to 90 degrees."""

    longitude: float = field(
        converter=convert_to_longitude,
        validator=[validators.gt(-180), validators.le(180)],
        on_setattr=setters.pipe(setters.convert, setters.validate),
    )
    """Event longitude from -180 to 180 degrees (-180 is stored as +180)."""

    depth: float = field(
        converter=float, on_setattr=setters.pipe(setters.convert, setters.validate)
    )
    """Hypocentre depth in metres, positive downwards, as recorded in the
    source catalogue — relative to sea level; may be negative for events
    above sea level. (The datum detail lives here, not on
    [`Event.depth`][pysmo.Event.depth], because it is format-specific.)"""

    public_id: str = field(
        validator=validators.instance_of(str), on_setattr=setters.validate
    )
    """QuakeML `publicID` of the event this instance was parsed from,
    preserved verbatim. It is a dependable unique key for records from one
    catalogue, but not a canonical per-earthquake key across a catalogue
    merged from several sources (each agency assigns its own). Parse-time
    provenance: not updated when other attributes change."""

    magnitude: float | None = field(default=None, converter=converters.optional(float))
    """Preferred magnitude value, or `None` if the document has none."""

    magnitude_type: str | None = field(default=None)
    """Preferred magnitude type (e.g. `"Mw"`), or `None`."""

    event_type: str | None = field(default=None)
    """QuakeML `event/type` (e.g. `"earthquake"`, `"explosion"`), or `None`."""

    description: str | None = field(default=None)
    """First `event/description/text` (e.g. a Flinn-Engdahl region or event
    name), or `None`."""

    @classmethod
    def from_bytes(
        cls, xml: bytes, *, event_id: str | None = None, strict: bool = True
    ) -> Self:
        """Create a new instance from a QuakeML document, narrowing to one event.

        Args:
            xml: Raw QuakeML 1.2 document bytes.
            event_id: Event to select when `xml` describes more than one.
                Matched against each event's full `publicID`, or — as a
                short form — against the trailing `eventid=` query-parameter
                value or the final path segment of the `publicID`. If
                `None`, `xml` must describe exactly one event.
            strict: If `True` (default), any unrepresentable event in `xml`
                fails the call. If `False`, such events are skipped (with a
                `UserWarning`) before narrowing — so a bad *other* event
                doesn't block selecting the one you asked for.

        Returns:
            A new QuakeML instance for the selected event.

        Raises:
            ValueError: If `xml` is malformed; if `strict` and it describes
                an event that cannot be represented (see
                [`pysmo.classes.QuakeML`][]); if `event_id` is `None` and
                `xml` does not describe exactly one (representable) event; or
                if `event_id` matches zero or (in its short form) more than
                one event.

        Tip: See Also
            [`QuakeML.all_from_bytes`][pysmo.classes.QuakeML.all_from_bytes]:
            Return every event in the document without narrowing to one.
        """
        events = cls.all_from_bytes(xml, strict=strict)
        if event_id is None:
            if len(events) != 1:
                raise ValueError(
                    "Expected exactly one event in the given QuakeML, found "
                    + f"{len(events)}: {[event.public_id for event in events]}."
                )
            return events[0]

        exact = [event for event in events if event.public_id == event_id]
        if len(exact) == 1:
            return exact[0]
        if len(exact) > 1:
            raise ValueError(
                f"event_id {event_id!r} matches {len(exact)} events by publicID."
            )

        short = [event for event in events if _short_id(event.public_id) == event_id]
        if len(short) == 1:
            return short[0]
        if len(short) > 1:
            raise ValueError(
                f"event_id {event_id!r} matches {len(short)} events: "
                + f"{[event.public_id for event in short]}."
            )
        raise ValueError(
            f"Expected exactly one event matching event_id {event_id!r}, found 0."
        )

    @classmethod
    def all_from_bytes(cls, xml: bytes, *, strict: bool = True) -> list[Self]:
        """Create one instance per `<event>` in a QuakeML document.

        Args:
            xml: Raw QuakeML 1.2 document bytes.
            strict: If `True` (default), a single unrepresentable event
                fails the whole parse. If `False`, unrepresentable events
                are skipped and a `UserWarning` reports how many — useful
                for a broad catalogue where a few malformed origins should
                not discard the rest.

        Returns:
            One QuakeML instance per representable event, in document order.

        Raises:
            ValueError: If `xml` is malformed, or — when `strict` is `True`
                — contains any event that cannot be represented (see
                [`pysmo.classes.QuakeML`][]).
        """
        return [cls._from_raw(raw) for raw in parse_quakeml(xml, strict=strict)]

    @classmethod
    def fetch(cls, *, event_id: str) -> Self:
        """Fetch and parse a single event from the USGS fdsnws-event service.

        Fetches exactly one event by the service's event id. To fetch a
        catalogue, use
        [`all_from_query`][pysmo.classes.QuakeML.all_from_query]; to fetch
        once and parse later (e.g. offline), use
        [`pysmo.tools.web.fetch_quakeml`][] with
        [`from_bytes`][pysmo.classes.QuakeML.from_bytes].

        Args:
            event_id: The service's event id.

        Returns:
            A new QuakeML instance for the fetched event.

        Raises:
            ValueError: If the response cannot be parsed or does not
                describe exactly one event.
            urllib3.exceptions.ResponseError: If the event web service
                returns an HTTP error.
        """
        return cls.from_bytes(fetch_quakeml(eventid=event_id))

    @classmethod
    def all_from_query(
        cls,
        *,
        starttime: pd.Timestamp | None = None,
        endtime: pd.Timestamp | None = None,
        updatedafter: pd.Timestamp | None = None,
        minlatitude: float | None = None,
        maxlatitude: float | None = None,
        minlongitude: float | None = None,
        maxlongitude: float | None = None,
        latitude: float | None = None,
        longitude: float | None = None,
        minradius: float | None = None,
        maxradius: float | None = None,
        mindepth_km: float | None = None,
        maxdepth_km: float | None = None,
        minmagnitude: float | None = None,
        maxmagnitude: float | None = None,
        magnitudetype: str | None = None,
        eventtype: str | None = None,
        eventid: str | None = None,
        limit: int | None = None,
        offset: int | None = None,
        orderby: QuakeMLOrderBy | None = None,
        catalog: str | None = None,
        contributor: str | None = None,
        strict: bool = True,
    ) -> list[Self]:
        """Fetch and parse a catalogue of events from the USGS fdsnws-event service.

        A one-step convenience over
        [`pysmo.tools.web.fetch_quakeml`][] followed by
        [`all_from_bytes`][pysmo.classes.QuakeML.all_from_bytes]. All
        parameters other than `strict` are those of `fetch_quakeml`, with
        the same meanings; `mindepth_km` / `maxdepth_km` are in
        **kilometres** while the parsed [`depth`][pysmo.classes.QuakeML] is
        in metres.

        `strict` (default `True`) matches
        [`all_from_bytes`][pysmo.classes.QuakeML.all_from_bytes]: pass
        `False` for a broad query where a few unrepresentable events should
        be skipped (with a `UserWarning`) rather than discarding the whole
        catalogue.

        Returns:
            One QuakeML instance per representable event, in the service's order.

        Raises:
            ValueError: If the response cannot be parsed, or — when `strict`
                is `True` — any event in it cannot be represented.
            urllib3.exceptions.ResponseError: If the event web service
                returns an HTTP error, including a 404 when nothing matches.
        """
        return cls.all_from_bytes(
            fetch_quakeml(
                starttime=starttime,
                endtime=endtime,
                updatedafter=updatedafter,
                minlatitude=minlatitude,
                maxlatitude=maxlatitude,
                minlongitude=minlongitude,
                maxlongitude=maxlongitude,
                latitude=latitude,
                longitude=longitude,
                minradius=minradius,
                maxradius=maxradius,
                mindepth_km=mindepth_km,
                maxdepth_km=maxdepth_km,
                minmagnitude=minmagnitude,
                maxmagnitude=maxmagnitude,
                magnitudetype=magnitudetype,
                eventtype=eventtype,
                eventid=eventid,
                limit=limit,
                offset=offset,
                orderby=orderby,
                catalog=catalog,
                contributor=contributor,
            ),
            strict=strict,
        )

    @classmethod
    def _from_raw(cls, raw: _RawEvent) -> Self:
        return cls(
            time=raw.time,
            latitude=raw.latitude,
            longitude=raw.longitude,
            depth=raw.depth,
            public_id=raw.public_id,
            magnitude=raw.magnitude,
            magnitude_type=raw.magnitude_type,
            event_type=raw.event_type,
            description=raw.description,
        )

depth class-attribute instance-attribute

depth: float = field(
    converter=float,
    on_setattr=setters.pipe(
        setters.convert, setters.validate
    ),
)

Hypocentre depth in metres, positive downwards, as recorded in the source catalogue — relative to sea level; may be negative for events above sea level. (The datum detail lives here, not on Event.depth, because it is format-specific.)

description class-attribute instance-attribute

description: str | None = field(default=None)

First event/description/text (e.g. a Flinn-Engdahl region or event name), or None.

event_type class-attribute instance-attribute

event_type: str | None = field(default=None)

QuakeML event/type (e.g. "earthquake", "explosion"), or None.

latitude class-attribute instance-attribute

latitude: float = field(
    converter=float,
    validator=[validators.ge(-90), validators.le(90)],
    on_setattr=setters.pipe(
        setters.convert, setters.validate
    ),
)

Event latitude from -90 to 90 degrees.

longitude class-attribute instance-attribute

longitude: float = field(
    converter=convert_to_longitude,
    validator=[validators.gt(-180), validators.le(180)],
    on_setattr=setters.pipe(
        setters.convert, setters.validate
    ),
)

Event longitude from -180 to 180 degrees (-180 is stored as +180).

magnitude class-attribute instance-attribute

magnitude: float | None = field(
    default=None, converter=converters.optional(float)
)

Preferred magnitude value, or None if the document has none.

magnitude_type class-attribute instance-attribute

magnitude_type: str | None = field(default=None)

Preferred magnitude type (e.g. "Mw"), or None.

public_id class-attribute instance-attribute

public_id: str = field(
    validator=validators.instance_of(str),
    on_setattr=setters.validate,
)

QuakeML publicID of the event this instance was parsed from, preserved verbatim. It is a dependable unique key for records from one catalogue, but not a canonical per-earthquake key across a catalogue merged from several sources (each agency assigns its own). Parse-time provenance: not updated when other attributes change.

time class-attribute instance-attribute

Event origin time.

all_from_bytes classmethod

all_from_bytes(
    xml: bytes, *, strict: bool = True
) -> list[Self]

Create one instance per <event> in a QuakeML document.

Parameters:

Name Type Description Default
xml bytes

Raw QuakeML 1.2 document bytes.

required
strict bool

If True (default), a single unrepresentable event fails the whole parse. If False, unrepresentable events are skipped and a UserWarning reports how many — useful for a broad catalogue where a few malformed origins should not discard the rest.

True

Returns:

Type Description
list[Self]

One QuakeML instance per representable event, in document order.

Raises:

Type Description
ValueError

If xml is malformed, or — when strict is True — contains any event that cannot be represented (see pysmo.classes.QuakeML).

Source code in src/pysmo/classes/_quakeml.py
@classmethod
def all_from_bytes(cls, xml: bytes, *, strict: bool = True) -> list[Self]:
    """Create one instance per `<event>` in a QuakeML document.

    Args:
        xml: Raw QuakeML 1.2 document bytes.
        strict: If `True` (default), a single unrepresentable event
            fails the whole parse. If `False`, unrepresentable events
            are skipped and a `UserWarning` reports how many — useful
            for a broad catalogue where a few malformed origins should
            not discard the rest.

    Returns:
        One QuakeML instance per representable event, in document order.

    Raises:
        ValueError: If `xml` is malformed, or — when `strict` is `True`
            — contains any event that cannot be represented (see
            [`pysmo.classes.QuakeML`][]).
    """
    return [cls._from_raw(raw) for raw in parse_quakeml(xml, strict=strict)]

all_from_query classmethod

all_from_query(
    *,
    starttime: Timestamp | None = None,
    endtime: Timestamp | None = None,
    updatedafter: Timestamp | None = None,
    minlatitude: float | None = None,
    maxlatitude: float | None = None,
    minlongitude: float | None = None,
    maxlongitude: float | None = None,
    latitude: float | None = None,
    longitude: float | None = None,
    minradius: float | None = None,
    maxradius: float | None = None,
    mindepth_km: float | None = None,
    maxdepth_km: float | None = None,
    minmagnitude: float | None = None,
    maxmagnitude: float | None = None,
    magnitudetype: str | None = None,
    eventtype: str | None = None,
    eventid: str | None = None,
    limit: int | None = None,
    offset: int | None = None,
    orderby: QuakeMLOrderBy | None = None,
    catalog: str | None = None,
    contributor: str | None = None,
    strict: bool = True
) -> list[Self]

Fetch and parse a catalogue of events from the USGS fdsnws-event service.

A one-step convenience over pysmo.tools.web.fetch_quakeml followed by all_from_bytes. All parameters other than strict are those of fetch_quakeml, with the same meanings; mindepth_km / maxdepth_km are in kilometres while the parsed depth is in metres.

strict (default True) matches all_from_bytes: pass False for a broad query where a few unrepresentable events should be skipped (with a UserWarning) rather than discarding the whole catalogue.

Returns:

Type Description
list[Self]

One QuakeML instance per representable event, in the service's order.

Raises:

Type Description
ValueError

If the response cannot be parsed, or — when strict is True — any event in it cannot be represented.

ResponseError

If the event web service returns an HTTP error, including a 404 when nothing matches.

Source code in src/pysmo/classes/_quakeml.py
@classmethod
def all_from_query(
    cls,
    *,
    starttime: pd.Timestamp | None = None,
    endtime: pd.Timestamp | None = None,
    updatedafter: pd.Timestamp | None = None,
    minlatitude: float | None = None,
    maxlatitude: float | None = None,
    minlongitude: float | None = None,
    maxlongitude: float | None = None,
    latitude: float | None = None,
    longitude: float | None = None,
    minradius: float | None = None,
    maxradius: float | None = None,
    mindepth_km: float | None = None,
    maxdepth_km: float | None = None,
    minmagnitude: float | None = None,
    maxmagnitude: float | None = None,
    magnitudetype: str | None = None,
    eventtype: str | None = None,
    eventid: str | None = None,
    limit: int | None = None,
    offset: int | None = None,
    orderby: QuakeMLOrderBy | None = None,
    catalog: str | None = None,
    contributor: str | None = None,
    strict: bool = True,
) -> list[Self]:
    """Fetch and parse a catalogue of events from the USGS fdsnws-event service.

    A one-step convenience over
    [`pysmo.tools.web.fetch_quakeml`][] followed by
    [`all_from_bytes`][pysmo.classes.QuakeML.all_from_bytes]. All
    parameters other than `strict` are those of `fetch_quakeml`, with
    the same meanings; `mindepth_km` / `maxdepth_km` are in
    **kilometres** while the parsed [`depth`][pysmo.classes.QuakeML] is
    in metres.

    `strict` (default `True`) matches
    [`all_from_bytes`][pysmo.classes.QuakeML.all_from_bytes]: pass
    `False` for a broad query where a few unrepresentable events should
    be skipped (with a `UserWarning`) rather than discarding the whole
    catalogue.

    Returns:
        One QuakeML instance per representable event, in the service's order.

    Raises:
        ValueError: If the response cannot be parsed, or — when `strict`
            is `True` — any event in it cannot be represented.
        urllib3.exceptions.ResponseError: If the event web service
            returns an HTTP error, including a 404 when nothing matches.
    """
    return cls.all_from_bytes(
        fetch_quakeml(
            starttime=starttime,
            endtime=endtime,
            updatedafter=updatedafter,
            minlatitude=minlatitude,
            maxlatitude=maxlatitude,
            minlongitude=minlongitude,
            maxlongitude=maxlongitude,
            latitude=latitude,
            longitude=longitude,
            minradius=minradius,
            maxradius=maxradius,
            mindepth_km=mindepth_km,
            maxdepth_km=maxdepth_km,
            minmagnitude=minmagnitude,
            maxmagnitude=maxmagnitude,
            magnitudetype=magnitudetype,
            eventtype=eventtype,
            eventid=eventid,
            limit=limit,
            offset=offset,
            orderby=orderby,
            catalog=catalog,
            contributor=contributor,
        ),
        strict=strict,
    )

fetch classmethod

fetch(*, event_id: str) -> Self

Fetch and parse a single event from the USGS fdsnws-event service.

Fetches exactly one event by the service's event id. To fetch a catalogue, use all_from_query; to fetch once and parse later (e.g. offline), use pysmo.tools.web.fetch_quakeml with from_bytes.

Parameters:

Name Type Description Default
event_id str

The service's event id.

required

Returns:

Type Description
Self

A new QuakeML instance for the fetched event.

Raises:

Type Description
ValueError

If the response cannot be parsed or does not describe exactly one event.

ResponseError

If the event web service returns an HTTP error.

Source code in src/pysmo/classes/_quakeml.py
@classmethod
def fetch(cls, *, event_id: str) -> Self:
    """Fetch and parse a single event from the USGS fdsnws-event service.

    Fetches exactly one event by the service's event id. To fetch a
    catalogue, use
    [`all_from_query`][pysmo.classes.QuakeML.all_from_query]; to fetch
    once and parse later (e.g. offline), use
    [`pysmo.tools.web.fetch_quakeml`][] with
    [`from_bytes`][pysmo.classes.QuakeML.from_bytes].

    Args:
        event_id: The service's event id.

    Returns:
        A new QuakeML instance for the fetched event.

    Raises:
        ValueError: If the response cannot be parsed or does not
            describe exactly one event.
        urllib3.exceptions.ResponseError: If the event web service
            returns an HTTP error.
    """
    return cls.from_bytes(fetch_quakeml(eventid=event_id))

from_bytes classmethod

from_bytes(
    xml: bytes,
    *,
    event_id: str | None = None,
    strict: bool = True
) -> Self

Create a new instance from a QuakeML document, narrowing to one event.

Parameters:

Name Type Description Default
xml bytes

Raw QuakeML 1.2 document bytes.

required
event_id str | None

Event to select when xml describes more than one. Matched against each event's full publicID, or — as a short form — against the trailing eventid= query-parameter value or the final path segment of the publicID. If None, xml must describe exactly one event.

None
strict bool

If True (default), any unrepresentable event in xml fails the call. If False, such events are skipped (with a UserWarning) before narrowing — so a bad other event doesn't block selecting the one you asked for.

True

Returns:

Type Description
Self

A new QuakeML instance for the selected event.

Raises:

Type Description
ValueError

If xml is malformed; if strict and it describes an event that cannot be represented (see pysmo.classes.QuakeML); if event_id is None and xml does not describe exactly one (representable) event; or if event_id matches zero or (in its short form) more than one event.

See Also

QuakeML.all_from_bytes: Return every event in the document without narrowing to one.

Source code in src/pysmo/classes/_quakeml.py
@classmethod
def from_bytes(
    cls, xml: bytes, *, event_id: str | None = None, strict: bool = True
) -> Self:
    """Create a new instance from a QuakeML document, narrowing to one event.

    Args:
        xml: Raw QuakeML 1.2 document bytes.
        event_id: Event to select when `xml` describes more than one.
            Matched against each event's full `publicID`, or — as a
            short form — against the trailing `eventid=` query-parameter
            value or the final path segment of the `publicID`. If
            `None`, `xml` must describe exactly one event.
        strict: If `True` (default), any unrepresentable event in `xml`
            fails the call. If `False`, such events are skipped (with a
            `UserWarning`) before narrowing — so a bad *other* event
            doesn't block selecting the one you asked for.

    Returns:
        A new QuakeML instance for the selected event.

    Raises:
        ValueError: If `xml` is malformed; if `strict` and it describes
            an event that cannot be represented (see
            [`pysmo.classes.QuakeML`][]); if `event_id` is `None` and
            `xml` does not describe exactly one (representable) event; or
            if `event_id` matches zero or (in its short form) more than
            one event.

    Tip: See Also
        [`QuakeML.all_from_bytes`][pysmo.classes.QuakeML.all_from_bytes]:
        Return every event in the document without narrowing to one.
    """
    events = cls.all_from_bytes(xml, strict=strict)
    if event_id is None:
        if len(events) != 1:
            raise ValueError(
                "Expected exactly one event in the given QuakeML, found "
                + f"{len(events)}: {[event.public_id for event in events]}."
            )
        return events[0]

    exact = [event for event in events if event.public_id == event_id]
    if len(exact) == 1:
        return exact[0]
    if len(exact) > 1:
        raise ValueError(
            f"event_id {event_id!r} matches {len(exact)} events by publicID."
        )

    short = [event for event in events if _short_id(event.public_id) == event_id]
    if len(short) == 1:
        return short[0]
    if len(short) > 1:
        raise ValueError(
            f"event_id {event_id!r} matches {len(short)} events: "
            + f"{[event.public_id for event in short]}."
        )
    raise ValueError(
        f"Expected exactly one event matching event_id {event_id!r}, found 0."
    )

SAC

Access and modify data stored in SAC files.

SAC wraps a SacIO instance and adds attributes alongside it that allow using pysmo types. The extra attributes are themselves instances of "helper" classes that should not be instantiated directly.

Examples:

SAC instances are typically created by reading a SAC file:

>>> from pysmo.classes import SAC
>>> sac = SAC.from_file("example.sac")
>>> sac.seismogram.delta
Timedelta('0 days 00:00:00.050000000')
>>> sac.seismogram.data
array([-47201., -47361., -47511., ..., -82144., -71072., -59960.],
      shape=(57465,))
>>>

Raw SAC header values are not compatible with pysmo types. For example, event coordinates are stored in the evla and evlo headers, which do not match the pysmo Location type. Renaming or aliasing evla to latitude and evlo to longitude would solve the problem for the event coordinates, but since the SAC format also specifies station coordinates (stla, stlo), the same compatibility issue remains.

The SAC class solves this with helper classes that map these incompatible attributes to ones compatible with pysmo types, accessible under different names:

>>> from pysmo import Seismogram
>>>
>>> def sample_count(seismogram: Seismogram) -> int:
...     return len(seismogram.data)
...
>>> # A bare SAC instance is not a Seismogram: a type checker rejects
>>> # this, and at runtime the function fails on the missing member:
>>> sample_count(sac)
Traceback (most recent call last):
    ...
AttributeError: 'SAC' object has no attribute 'data'
>>> # The sac.seismogram helper is a Seismogram:
>>> sample_count(sac.seismogram)
57465
>>>

Because the SAC file format defines a large number of header fields for metadata, many of them are optional. Since the helper classes are more specific (and intended to be used with pysmo types), their attributes typically may not be None:

>>> # No error: a SAC file doesn't have to contain event information:
>>> sac.native.evla = None
>>>
A curated surface, not the full header set

SAC only exposes a small, curated surface directly (file I/O, and the pysmo-typed station, event, seismogram and timestamps helpers) rather than the full raw SAC header set. Seismogram data and sampling interval are available via seismogram. Users familiar with the SAC file format who want direct access to a header by its native name (e.g. evla, stla, kstnm) can reach the underlying SacIO instance via SAC.native.

Methods:

Name Description
all_from_zip

Create one instance per SAC file in a zip archive.

fetch

Fetch and parse a SAC seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

from_bytes

Create a new SAC instance from SAC file content as bytes.

from_file

Create a new SAC instance from a SAC file.

from_zip

Create a new instance from a zip archive containing exactly one continuous SAC segment.

read

Read data and headers from a SAC file into an existing SAC instance.

read_bytes

Read SAC file content as bytes into an existing SAC instance.

write

Write data and header values to a SAC file.

Attributes:

Name Type Description
event SacEvent

This SAC object exposed as an Event.

native SacIO

The underlying SacIO instance.

seismogram SacSeismogram

This SAC object exposed as a Seismogram.

station SacStation

This SAC object exposed as a Station.

timestamps SacTimestamps

Maps SAC time headers such as B, E, O, T0-T9 to

Source code in src/pysmo/classes/_sac.py
@define(kw_only=True)
class SAC:
    """Access and modify data stored in SAC files.

    [`SAC`][pysmo.classes.SAC] wraps a [`SacIO`][pysmo.lib.io.SacIO] instance
    and adds attributes alongside it that allow using pysmo types. The extra
    attributes are themselves instances of "helper" classes that should not
    be instantiated directly.

    Examples:
        SAC instances are typically created by reading a SAC file:

        ```python
        >>> from pysmo.classes import SAC
        >>> sac = SAC.from_file("example.sac")
        >>> sac.seismogram.delta
        Timedelta('0 days 00:00:00.050000000')
        >>> sac.seismogram.data
        array([-47201., -47361., -47511., ..., -82144., -71072., -59960.],
              shape=(57465,))
        >>>
        ```

        Raw SAC header values are *not* compatible with pysmo types. For
        example, event coordinates are stored in the
        [`evla`][pysmo.lib.io.SacIO.evla] and [`evlo`][pysmo.lib.io.SacIO.evlo]
        headers, which do not match the pysmo [`Location`][pysmo.Location]
        type. Renaming or aliasing `evla` to `latitude` and `evlo` to
        `longitude` would solve the problem for the event coordinates, but
        since the SAC format also specifies station coordinates
        ([`stla`][pysmo.lib.io.SacIO.stla], [`stlo`][pysmo.lib.io.SacIO.stlo]),
        the same compatibility issue remains.

        The [`SAC`][pysmo.classes.SAC] class solves this with helper classes
        that map these incompatible attributes to ones compatible with pysmo
        types, accessible under different names:

        ```python
        >>> from pysmo import Seismogram
        >>>
        >>> def sample_count(seismogram: Seismogram) -> int:
        ...     return len(seismogram.data)
        ...
        >>> # A bare SAC instance is not a Seismogram: a type checker rejects
        >>> # this, and at runtime the function fails on the missing member:
        >>> sample_count(sac)
        Traceback (most recent call last):
            ...
        AttributeError: 'SAC' object has no attribute 'data'
        >>> # The sac.seismogram helper is a Seismogram:
        >>> sample_count(sac.seismogram)
        57465
        >>>
        ```

        Because the SAC file format defines a large number of header fields
        for metadata, many of them are optional. Since the helper classes
        are more specific (and intended to be used with pysmo types), their
        attributes typically may *not* be [`None`][]:

        ```python
        >>> # No error: a SAC file doesn't have to contain event information:
        >>> sac.native.evla = None
        >>>
        ```

    Tip: A curated surface, not the full header set
        [`SAC`][pysmo.classes.SAC] only exposes a small, curated surface
        directly (file I/O, and the pysmo-typed
        [`station`][pysmo.classes.SAC.station],
        [`event`][pysmo.classes.SAC.event],
        [`seismogram`][pysmo.classes.SAC.seismogram] and
        [`timestamps`][pysmo.classes.SAC.timestamps] helpers) rather than
        the full raw SAC header set. Seismogram data and sampling interval
        are available via [`seismogram`][pysmo.classes.SAC.seismogram].
        Users familiar with the SAC file format who want direct access to a
        header by its native name (e.g. `evla`, `stla`, `kstnm`) can reach
        the underlying [`SacIO`][pysmo.lib.io.SacIO] instance via
        [`SAC.native`][pysmo.classes.SAC.native].
    """

    native: SacIO = field(factory=SacIO, repr=False, on_setattr=setters.frozen)
    """The underlying [`SacIO`][pysmo.lib.io.SacIO] instance.

    This is the escape hatch for direct access to raw SAC headers by their
    native names (e.g. `SAC.native.evla`), for users familiar with the SAC file
    format who need it.

    Fixed for the lifetime of the instance: [`seismogram`][pysmo.classes.SAC.seismogram],
    [`station`][pysmo.classes.SAC.station], [`event`][pysmo.classes.SAC.event]
    and [`timestamps`][pysmo.classes.SAC.timestamps] are bound to this object
    at construction time, so reassigning it would silently orphan them. To
    load different data into an existing instance, use
    [`read`][pysmo.classes.SAC.read]/[`read_bytes`][pysmo.classes.SAC.read_bytes],
    which update this same object in place; otherwise construct a new
    [`SAC`][pysmo.classes.SAC] instance.
    """

    seismogram: SacSeismogram = field(init=False)
    """This SAC object exposed as a [`Seismogram`][pysmo.Seismogram]."""

    station: SacStation = field(init=False)
    """This SAC object exposed as a [`Station`][pysmo.Station]."""

    event: SacEvent = field(init=False)
    """This SAC object exposed as an [`Event`][pysmo.Event]."""

    timestamps: SacTimestamps = field(init=False)
    """Maps SAC time headers such as B, E, O, T0-T9 to
    [`Timestamp`][pandas.Timestamp] objects."""

    def __attrs_post_init__(self) -> None:
        self.seismogram = SacSeismogram(parent=self.native)
        self.station = SacStation(parent=self.native)
        self.event = SacEvent(parent=self.native)
        self.timestamps = SacTimestamps(parent=self.native)

    @classmethod
    def from_file(cls, filename: str | PathLike[str]) -> Self:
        """Create a new SAC instance from a SAC file.

        Args:
            filename: Name of the SAC file to read.

        Returns:
            A new SAC instance.

        Raises:
            NotImplementedError: If the file isn't evenly-spaced time-series
                data (IFTYPE=ITIME, LEVEN=True). Use
                [`SacIO.from_file`][pysmo.lib.io.SacIO.from_file] directly
                for other SAC file types.
        """
        native = SacIO.from_file(filename)
        _check_seismogram_compatible(native)
        return cls(native=native)

    @classmethod
    def from_bytes(cls, data: bytes) -> Self:
        """Create a new SAC instance from SAC file content as bytes.

        Args:
            data: Raw bytes of a SAC file.

        Returns:
            A new SAC instance.

        Raises:
            NotImplementedError: If the data isn't evenly-spaced
                time-series data (IFTYPE=ITIME, LEVEN=True). Use
                [`SacIO.from_buffer`][pysmo.lib.io.SacIO.from_buffer]
                directly for other SAC file types.
        """
        native = SacIO.from_buffer(data)
        _check_seismogram_compatible(native)
        return cls(native=native)

    def read(self, filename: str | PathLike[str]) -> None:
        """Read data and headers from a SAC file into an existing SAC instance.

        Args:
            filename: Name of the SAC file to read.

        Raises:
            NotImplementedError: If the file isn't evenly-spaced time-series
                data (IFTYPE=ITIME, LEVEN=True); the existing instance is left
                unchanged in this case.
        """
        _check_seismogram_compatible(SacIO.from_file(filename))
        self.native.read(filename)

    def read_bytes(self, data: bytes) -> None:
        """Read SAC file content as bytes into an existing SAC instance.

        Args:
            data: Raw bytes of a SAC file.

        Raises:
            NotImplementedError: If the data isn't evenly-spaced
                time-series data (IFTYPE=ITIME, LEVEN=True); the existing
                instance is left unchanged in this case.
        """
        _check_seismogram_compatible(SacIO.from_buffer(data))
        self.native.read_buffer(data)

    def write(self, filename: str | PathLike[str]) -> None:
        """Write data and header values to a SAC file.

        Args:
            filename: Name of the SAC file to write to.
        """
        self.native.write(filename)

    @classmethod
    def from_zip(cls, archive: bytes) -> Self:
        """Create a new instance from a zip archive containing exactly one continuous SAC segment.

        Args:
            archive: Raw zip archive bytes containing exactly one SAC file
                (as returned by the FDSN dataselect web service with
                `format=sac.zip`).

        Returns:
            A new SAC instance.

        Raises:
            ValueError: If `archive` is not a valid zip archive, contains no
                members, contains more than one member (e.g. due to a data
                gap, an instrument/metadata epoch change, overlapping
                records, or a wildcarded channel/location code matching
                more than one channel), or a member cannot be parsed as a
                SAC file.

        Tip: See Also
            [`SAC.all_from_zip`][pysmo.classes.SAC.all_from_zip]: Parse
            every segment in the archive without requiring exactly one.
        """
        segments = cls.all_from_zip(archive)
        if len(segments) == 1:
            return segments[0]
        if not segments:
            raise ValueError("Zip archive contains no SAC segments.")

        segment_lines = "\n".join(
            f"  {segment.station.network}.{segment.station.name}."
            + f"{segment.station.location}.{segment.station.channel}  "
            + f"{segment.seismogram.begin_time} -- {segment.seismogram.end_time}"
            for segment in segments
        )
        raise ValueError(
            f"Zip archive contains {len(segments)} segments; "
            + "SAC.from_zip() requires exactly one continuous segment. "
            + "Use SAC.all_from_zip() to get all segments instead. "
            + f"Segments found:\n{segment_lines}"
        )

    @classmethod
    def fetch(
        cls, *, station: Station, starttime: pd.Timestamp, endtime: pd.Timestamp
    ) -> Self:
        """Fetch and parse a SAC seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

        For a window relative to a predicted phase arrival instead, compute
        the window yourself (e.g. with
        [`pysmo.tools.traveltime.travel_times`][], which shows exactly this
        in its own Examples) and pass the resulting *starttime*/*endtime*
        here.

        To fetch once and interpret later (e.g. offline, or without
        repeating the network request), use
        [`pysmo.tools.web.fetch_sac`][] and
        [`from_zip`][pysmo.classes.SAC.from_zip] /
        [`all_from_zip`][pysmo.classes.SAC.all_from_zip] directly instead.

        Args:
            station: Any object satisfying the [`Station`][pysmo.Station]
                protocol. Provides the network, station code, location, and
                channel for the request.
            starttime: Start of the requested time window (UTC).
            endtime: End of the requested time window (UTC).

        Returns:
            A new SAC instance.

        Raises:
            ValueError: If no waveform data is returned for the given
                window, if more than one continuous segment is returned
                (e.g. due to a data gap, an instrument/metadata epoch
                change, overlapping records, or a wildcarded channel/
                location code matching more than one channel), or if a
                returned segment cannot be parsed as a SAC file.
            urllib3.exceptions.ResponseError: If the dataselect web service
                returns an HTTP error.

        Examples:
            <!-- skip: start if(not run_real_web_requests) -->
            ```python
            >>> import pandas as pd
            >>> from pysmo import MiniStation
            >>> from pysmo.classes import SAC
            >>> station = MiniStation(
            ...     name="ANMO", network="IU", location="00", channel="LHZ",
            ...     latitude=34.945981, longitude=-106.457133,
            ... )
            >>> sac = SAC.fetch(
            ...     station=station,
            ...     starttime=pd.Timestamp("2010-02-27T06:44:00Z"),
            ...     endtime=pd.Timestamp("2010-02-27T06:54:00Z"),
            ... )
            >>>
            ```
            <!-- skip: end -->
        """
        starttime = convert_to_utc_timestamp(starttime)
        endtime = convert_to_utc_timestamp(endtime)
        archive = fetch_sac(station=station, starttime=starttime, endtime=endtime)

        # dataselect returns an empty (zero-length) body, not a
        # zero-member zip archive, when a request is well-formed but
        # matches no data (HTTP 204, the FDSN default `nodata` handling) —
        # confirmed live.
        if not archive:
            raise ValueError(
                "No waveform data returned for "
                + f"{station.network}.{station.name}.{station.location}."
                + f"{station.channel} between {starttime} and {endtime}."
            )
        return cls.from_zip(archive)

    @classmethod
    def all_from_zip(cls, archive: bytes) -> list[Self]:
        """Create one instance per SAC file in a zip archive.

        Unlike [`from_zip`][pysmo.classes.SAC.from_zip], this does not
        require exactly one segment — a response covering a data gap, an
        instrument/metadata epoch change, or a wildcarded channel/location
        code returns several, which callers can inspect or merge
        themselves.

        Args:
            archive: Raw zip archive bytes, as returned by the FDSN
                dataselect web service with `format=sac.zip`.

        Returns:
            One SAC instance per member of the archive, in archive order.
            Empty if the archive has no members.

        Raises:
            ValueError: If `archive` is not a valid zip archive, or a
                member cannot be parsed as a SAC file.
        """
        try:
            with ZipFile(BytesIO(archive)) as archive_zip:
                names = archive_zip.namelist()
                segments = []
                for name in names:
                    try:
                        segments.append(cls.from_bytes(archive_zip.read(name)))
                    except Exception as error:
                        raise ValueError(
                            f"Could not parse segment {name!r} in zip archive: {error}"
                        ) from error
        except BadZipFile as error:
            raise ValueError(f"Not a valid zip archive: {error}") from error
        return segments

event class-attribute instance-attribute

event: SacEvent = field(init=False)

This SAC object exposed as an Event.

native class-attribute instance-attribute

native: SacIO = field(
    factory=SacIO, repr=False, on_setattr=setters.frozen
)

The underlying SacIO instance.

This is the escape hatch for direct access to raw SAC headers by their native names (e.g. SAC.native.evla), for users familiar with the SAC file format who need it.

Fixed for the lifetime of the instance: seismogram, station, event and timestamps are bound to this object at construction time, so reassigning it would silently orphan them. To load different data into an existing instance, use read/read_bytes, which update this same object in place; otherwise construct a new SAC instance.

seismogram class-attribute instance-attribute

seismogram: SacSeismogram = field(init=False)

This SAC object exposed as a Seismogram.

station class-attribute instance-attribute

station: SacStation = field(init=False)

This SAC object exposed as a Station.

timestamps class-attribute instance-attribute

timestamps: SacTimestamps = field(init=False)

Maps SAC time headers such as B, E, O, T0-T9 to Timestamp objects.

all_from_zip classmethod

all_from_zip(archive: bytes) -> list[Self]

Create one instance per SAC file in a zip archive.

Unlike from_zip, this does not require exactly one segment — a response covering a data gap, an instrument/metadata epoch change, or a wildcarded channel/location code returns several, which callers can inspect or merge themselves.

Parameters:

Name Type Description Default
archive bytes

Raw zip archive bytes, as returned by the FDSN dataselect web service with format=sac.zip.

required

Returns:

Type Description
list[Self]

One SAC instance per member of the archive, in archive order.

list[Self]

Empty if the archive has no members.

Raises:

Type Description
ValueError

If archive is not a valid zip archive, or a member cannot be parsed as a SAC file.

Source code in src/pysmo/classes/_sac.py
@classmethod
def all_from_zip(cls, archive: bytes) -> list[Self]:
    """Create one instance per SAC file in a zip archive.

    Unlike [`from_zip`][pysmo.classes.SAC.from_zip], this does not
    require exactly one segment — a response covering a data gap, an
    instrument/metadata epoch change, or a wildcarded channel/location
    code returns several, which callers can inspect or merge
    themselves.

    Args:
        archive: Raw zip archive bytes, as returned by the FDSN
            dataselect web service with `format=sac.zip`.

    Returns:
        One SAC instance per member of the archive, in archive order.
        Empty if the archive has no members.

    Raises:
        ValueError: If `archive` is not a valid zip archive, or a
            member cannot be parsed as a SAC file.
    """
    try:
        with ZipFile(BytesIO(archive)) as archive_zip:
            names = archive_zip.namelist()
            segments = []
            for name in names:
                try:
                    segments.append(cls.from_bytes(archive_zip.read(name)))
                except Exception as error:
                    raise ValueError(
                        f"Could not parse segment {name!r} in zip archive: {error}"
                    ) from error
    except BadZipFile as error:
        raise ValueError(f"Not a valid zip archive: {error}") from error
    return segments

fetch classmethod

fetch(
    *,
    station: Station,
    starttime: Timestamp,
    endtime: Timestamp
) -> Self

Fetch and parse a SAC seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

For a window relative to a predicted phase arrival instead, compute the window yourself (e.g. with pysmo.tools.traveltime.travel_times, which shows exactly this in its own Examples) and pass the resulting starttime/endtime here.

To fetch once and interpret later (e.g. offline, or without repeating the network request), use pysmo.tools.web.fetch_sac and from_zip / all_from_zip directly instead.

Parameters:

Name Type Description Default
station Station

Any object satisfying the Station protocol. Provides the network, station code, location, and channel for the request.

required
starttime Timestamp

Start of the requested time window (UTC).

required
endtime Timestamp

End of the requested time window (UTC).

required

Returns:

Type Description
Self

A new SAC instance.

Raises:

Type Description
ValueError

If no waveform data is returned for the given window, if more than one continuous segment is returned (e.g. due to a data gap, an instrument/metadata epoch change, overlapping records, or a wildcarded channel/ location code matching more than one channel), or if a returned segment cannot be parsed as a SAC file.

ResponseError

If the dataselect web service returns an HTTP error.

Examples:

>>> import pandas as pd
>>> from pysmo import MiniStation
>>> from pysmo.classes import SAC
>>> station = MiniStation(
...     name="ANMO", network="IU", location="00", channel="LHZ",
...     latitude=34.945981, longitude=-106.457133,
... )
>>> sac = SAC.fetch(
...     station=station,
...     starttime=pd.Timestamp("2010-02-27T06:44:00Z"),
...     endtime=pd.Timestamp("2010-02-27T06:54:00Z"),
... )
>>>
Source code in src/pysmo/classes/_sac.py
@classmethod
def fetch(
    cls, *, station: Station, starttime: pd.Timestamp, endtime: pd.Timestamp
) -> Self:
    """Fetch and parse a SAC seismogram from the EarthScope FDSN dataselect web service, for an absolute time window.

    For a window relative to a predicted phase arrival instead, compute
    the window yourself (e.g. with
    [`pysmo.tools.traveltime.travel_times`][], which shows exactly this
    in its own Examples) and pass the resulting *starttime*/*endtime*
    here.

    To fetch once and interpret later (e.g. offline, or without
    repeating the network request), use
    [`pysmo.tools.web.fetch_sac`][] and
    [`from_zip`][pysmo.classes.SAC.from_zip] /
    [`all_from_zip`][pysmo.classes.SAC.all_from_zip] directly instead.

    Args:
        station: Any object satisfying the [`Station`][pysmo.Station]
            protocol. Provides the network, station code, location, and
            channel for the request.
        starttime: Start of the requested time window (UTC).
        endtime: End of the requested time window (UTC).

    Returns:
        A new SAC instance.

    Raises:
        ValueError: If no waveform data is returned for the given
            window, if more than one continuous segment is returned
            (e.g. due to a data gap, an instrument/metadata epoch
            change, overlapping records, or a wildcarded channel/
            location code matching more than one channel), or if a
            returned segment cannot be parsed as a SAC file.
        urllib3.exceptions.ResponseError: If the dataselect web service
            returns an HTTP error.

    Examples:
        <!-- skip: start if(not run_real_web_requests) -->
        ```python
        >>> import pandas as pd
        >>> from pysmo import MiniStation
        >>> from pysmo.classes import SAC
        >>> station = MiniStation(
        ...     name="ANMO", network="IU", location="00", channel="LHZ",
        ...     latitude=34.945981, longitude=-106.457133,
        ... )
        >>> sac = SAC.fetch(
        ...     station=station,
        ...     starttime=pd.Timestamp("2010-02-27T06:44:00Z"),
        ...     endtime=pd.Timestamp("2010-02-27T06:54:00Z"),
        ... )
        >>>
        ```
        <!-- skip: end -->
    """
    starttime = convert_to_utc_timestamp(starttime)
    endtime = convert_to_utc_timestamp(endtime)
    archive = fetch_sac(station=station, starttime=starttime, endtime=endtime)

    # dataselect returns an empty (zero-length) body, not a
    # zero-member zip archive, when a request is well-formed but
    # matches no data (HTTP 204, the FDSN default `nodata` handling) —
    # confirmed live.
    if not archive:
        raise ValueError(
            "No waveform data returned for "
            + f"{station.network}.{station.name}.{station.location}."
            + f"{station.channel} between {starttime} and {endtime}."
        )
    return cls.from_zip(archive)

from_bytes classmethod

from_bytes(data: bytes) -> Self

Create a new SAC instance from SAC file content as bytes.

Parameters:

Name Type Description Default
data bytes

Raw bytes of a SAC file.

required

Returns:

Type Description
Self

A new SAC instance.

Raises:

Type Description
NotImplementedError

If the data isn't evenly-spaced time-series data (IFTYPE=ITIME, LEVEN=True). Use SacIO.from_buffer directly for other SAC file types.

Source code in src/pysmo/classes/_sac.py
@classmethod
def from_bytes(cls, data: bytes) -> Self:
    """Create a new SAC instance from SAC file content as bytes.

    Args:
        data: Raw bytes of a SAC file.

    Returns:
        A new SAC instance.

    Raises:
        NotImplementedError: If the data isn't evenly-spaced
            time-series data (IFTYPE=ITIME, LEVEN=True). Use
            [`SacIO.from_buffer`][pysmo.lib.io.SacIO.from_buffer]
            directly for other SAC file types.
    """
    native = SacIO.from_buffer(data)
    _check_seismogram_compatible(native)
    return cls(native=native)

from_file classmethod

from_file(filename: str | PathLike[str]) -> Self

Create a new SAC instance from a SAC file.

Parameters:

Name Type Description Default
filename str | PathLike[str]

Name of the SAC file to read.

required

Returns:

Type Description
Self

A new SAC instance.

Raises:

Type Description
NotImplementedError

If the file isn't evenly-spaced time-series data (IFTYPE=ITIME, LEVEN=True). Use SacIO.from_file directly for other SAC file types.

Source code in src/pysmo/classes/_sac.py
@classmethod
def from_file(cls, filename: str | PathLike[str]) -> Self:
    """Create a new SAC instance from a SAC file.

    Args:
        filename: Name of the SAC file to read.

    Returns:
        A new SAC instance.

    Raises:
        NotImplementedError: If the file isn't evenly-spaced time-series
            data (IFTYPE=ITIME, LEVEN=True). Use
            [`SacIO.from_file`][pysmo.lib.io.SacIO.from_file] directly
            for other SAC file types.
    """
    native = SacIO.from_file(filename)
    _check_seismogram_compatible(native)
    return cls(native=native)

from_zip classmethod

from_zip(archive: bytes) -> Self

Create a new instance from a zip archive containing exactly one continuous SAC segment.

Parameters:

Name Type Description Default
archive bytes

Raw zip archive bytes containing exactly one SAC file (as returned by the FDSN dataselect web service with format=sac.zip).

required

Returns:

Type Description
Self

A new SAC instance.

Raises:

Type Description
ValueError

If archive is not a valid zip archive, contains no members, contains more than one member (e.g. due to a data gap, an instrument/metadata epoch change, overlapping records, or a wildcarded channel/location code matching more than one channel), or a member cannot be parsed as a SAC file.

See Also

SAC.all_from_zip: Parse every segment in the archive without requiring exactly one.

Source code in src/pysmo/classes/_sac.py
@classmethod
def from_zip(cls, archive: bytes) -> Self:
    """Create a new instance from a zip archive containing exactly one continuous SAC segment.

    Args:
        archive: Raw zip archive bytes containing exactly one SAC file
            (as returned by the FDSN dataselect web service with
            `format=sac.zip`).

    Returns:
        A new SAC instance.

    Raises:
        ValueError: If `archive` is not a valid zip archive, contains no
            members, contains more than one member (e.g. due to a data
            gap, an instrument/metadata epoch change, overlapping
            records, or a wildcarded channel/location code matching
            more than one channel), or a member cannot be parsed as a
            SAC file.

    Tip: See Also
        [`SAC.all_from_zip`][pysmo.classes.SAC.all_from_zip]: Parse
        every segment in the archive without requiring exactly one.
    """
    segments = cls.all_from_zip(archive)
    if len(segments) == 1:
        return segments[0]
    if not segments:
        raise ValueError("Zip archive contains no SAC segments.")

    segment_lines = "\n".join(
        f"  {segment.station.network}.{segment.station.name}."
        + f"{segment.station.location}.{segment.station.channel}  "
        + f"{segment.seismogram.begin_time} -- {segment.seismogram.end_time}"
        for segment in segments
    )
    raise ValueError(
        f"Zip archive contains {len(segments)} segments; "
        + "SAC.from_zip() requires exactly one continuous segment. "
        + "Use SAC.all_from_zip() to get all segments instead. "
        + f"Segments found:\n{segment_lines}"
    )

read

read(filename: str | PathLike[str]) -> None

Read data and headers from a SAC file into an existing SAC instance.

Parameters:

Name Type Description Default
filename str | PathLike[str]

Name of the SAC file to read.

required

Raises:

Type Description
NotImplementedError

If the file isn't evenly-spaced time-series data (IFTYPE=ITIME, LEVEN=True); the existing instance is left unchanged in this case.

Source code in src/pysmo/classes/_sac.py
def read(self, filename: str | PathLike[str]) -> None:
    """Read data and headers from a SAC file into an existing SAC instance.

    Args:
        filename: Name of the SAC file to read.

    Raises:
        NotImplementedError: If the file isn't evenly-spaced time-series
            data (IFTYPE=ITIME, LEVEN=True); the existing instance is left
            unchanged in this case.
    """
    _check_seismogram_compatible(SacIO.from_file(filename))
    self.native.read(filename)

read_bytes

read_bytes(data: bytes) -> None

Read SAC file content as bytes into an existing SAC instance.

Parameters:

Name Type Description Default
data bytes

Raw bytes of a SAC file.

required

Raises:

Type Description
NotImplementedError

If the data isn't evenly-spaced time-series data (IFTYPE=ITIME, LEVEN=True); the existing instance is left unchanged in this case.

Source code in src/pysmo/classes/_sac.py
def read_bytes(self, data: bytes) -> None:
    """Read SAC file content as bytes into an existing SAC instance.

    Args:
        data: Raw bytes of a SAC file.

    Raises:
        NotImplementedError: If the data isn't evenly-spaced
            time-series data (IFTYPE=ITIME, LEVEN=True); the existing
            instance is left unchanged in this case.
    """
    _check_seismogram_compatible(SacIO.from_buffer(data))
    self.native.read_buffer(data)

write

write(filename: str | PathLike[str]) -> None

Write data and header values to a SAC file.

Parameters:

Name Type Description Default
filename str | PathLike[str]

Name of the SAC file to write to.

required
Source code in src/pysmo/classes/_sac.py
def write(self, filename: str | PathLike[str]) -> None:
    """Write data and header values to a SAC file.

    Args:
        filename: Name of the SAC file to write to.
    """
    self.native.write(filename)

SacEvent

Bases: _SacNested

Helper class for SAC event attributes.

The SacEvent class maps SAC attributes to match the pysmo Event type. An instance is created for each new SAC instance.

Examples:

A SacEvent can be passed to any function that expects the pysmo Event type:

>>> from pysmo.classes import SAC
>>> from pysmo import Event
>>>
>>> def origin_isoformat(event: Event) -> str:
...     return event.time.isoformat()
...
>>> sac = SAC.from_file("example.sac")
>>> origin_isoformat(sac.event)
'2010-02-27T06:34:11.529998536+00:00'
>>>
Event information is optional

Not all SAC files contain event information.

Attributes:

Name Type Description
depth int | float

Event depth in metres (positive downward from the surface).

latitude int | float

Event latitude.

longitude int | float

Event longitude.

time UtcTimestamp

Event origin time (UTC).

Source code in src/pysmo/classes/_sac.py
@define(kw_only=True)
class SacEvent(_SacNested):
    """Helper class for SAC event attributes.

    The `SacEvent` class maps SAC attributes to match the pysmo
    [`Event`][pysmo.Event] type. An instance is created for each new
    [`SAC`][pysmo.classes.SAC] instance.

    Examples:
        A SacEvent can be passed to any function that expects the pysmo
        [`Event`][pysmo.Event] type:

        ```python
        >>> from pysmo.classes import SAC
        >>> from pysmo import Event
        >>>
        >>> def origin_isoformat(event: Event) -> str:
        ...     return event.time.isoformat()
        ...
        >>> sac = SAC.from_file("example.sac")
        >>> origin_isoformat(sac.event)
        '2010-02-27T06:34:11.529998536+00:00'
        >>>
        ```

    Note: Event information is optional
        Not all SAC files contain event information.
    """

    @property
    def latitude(self) -> int | float:
        """Event latitude."""

        if self._parent.evla is None:
            raise TypeError("SAC object event latitude 'evla' is None.")
        return self._parent.evla

    @latitude.setter
    def latitude(self, value: int | float) -> None:
        setattr(self._parent, "evla", value)

    @property
    def longitude(self) -> int | float:
        """Event longitude."""

        if self._parent.evlo is None:
            raise TypeError("SAC object event longitude 'evlo' is None.")
        return self._parent.evlo

    @longitude.setter
    def longitude(self, value: int | float) -> None:
        setattr(self._parent, "evlo", value)

    @property
    def depth(self) -> int | float:
        """Event depth in metres (positive downward from the surface)."""

        if self._parent.evdp is None:
            raise TypeError("Sac object event depth 'evdp' is None.")
        return self._parent.evdp * 1000

    @depth.setter
    def depth(self, value: int | float) -> None:
        setattr(self._parent, "evdp", value / 1000)

    @property
    def time(self) -> UtcTimestamp:
        """Event origin time (UTC).

        Important: Fixed when iztype is "o"
            This property uses the [`SacIO.o`][pysmo.lib.io.SacIO.o] time
            header. If [`SacIO.iztype`][pysmo.lib.io.SacIO.iztype] is `"o"`,
            `SacIO.o` is the reference-time equivalence and is fixed at 0,
            so [`time`][pysmo.classes.SacEvent.time] cannot be changed
            directly in that case.
        """

        event_time = self._get_timestamp_from_sac(SAC_OPTIONAL_TIME_HEADERS.o)
        if event_time is None:
            raise TypeError("SAC object event time 'o' is None.")
        return event_time

    @time.setter
    def time(self, value: pd.Timestamp) -> None:
        self._set_sac_from_timestamp(SAC_OPTIONAL_TIME_HEADERS.o, value)

depth property writable

depth: int | float

Event depth in metres (positive downward from the surface).

latitude property writable

latitude: int | float

Event latitude.

longitude property writable

longitude: int | float

Event longitude.

time property writable

Event origin time (UTC).

Fixed when iztype is "o"

This property uses the SacIO.o time header. If SacIO.iztype is "o", SacIO.o is the reference-time equivalence and is fixed at 0, so time cannot be changed directly in that case.

SacPZ

Import class for SAC PZ (pole-zero) files.

Reads an analog instrument response from a SAC PZ file (as produced by e.g. EarthScope's fdsnws-station service) and exposes it as a Response-compatible object. SacPZ only ever satisfies Response, never StagedResponse — the SAC PZ format has no digital-stage fields to parse.

Examples:

>>> from pysmo.classes import SacPZ
>>> text = '''\
... * NETWORK   (KNETWK): IU
... * STATION    (KSTNM): ANMO
... * LOCATION   (KHOLE): 00
... * CHANNEL   (KCMPNM): BHZ
... * START             : 2018-07-09T20:45:00
... * END               :
... * INPUT UNIT        : M
... ZEROS 2
... \t+0.000000e+00\t+0.000000e+00
... \t+0.000000e+00\t+0.000000e+00
... POLES 1
... \t-1.000000e-02\t+0.000000e+00
... CONSTANT 1.0e+09
... '''
>>> response = SacPZ.from_text(text)
>>> response.network, response.station
('IU', 'ANMO')
>>>

Methods:

Name Description
all_from_text

Create one instance per record in a bulk/concatenated SAC PZ text body.

fetch

Fetch and parse an instrument response as SAC PZ from EarthScope's fdsnws-station service, selecting one epoch.

from_text

Create a new instance from a single-record SAC PZ text body.

Attributes:

Name Type Description
channel str

Channel code parsed from the SAC PZ file's comment header.

end_date Timestamp | None

End of the epoch this response applies to, or None if still open.

input_units str

Physical units produced by removing this response via full spectral

location str

Location code parsed from the SAC PZ file's comment header.

network str

Network code parsed from the SAC PZ file's comment header.

overall_sensitivity NonZeroNumber

Total system sensitivity (the SAC PZ file's CONSTANT).

poles list[complex]

Response poles.

reference_sensitivity NonZeroNumber | None

Total system sensitivity at the reference frequency, A0 excluded

start_date Timestamp

Start of the epoch this response applies to.

station str

Station code parsed from the SAC PZ file's comment header.

zeros list[complex]

Response zeros.

Source code in src/pysmo/classes/_sacpz.py
@define(kw_only=True)
class SacPZ:
    r"""Import class for SAC PZ (pole-zero) files.

    Reads an analog instrument response from a
    [SAC PZ](https://ds.iris.edu/files/sac-manual/commands/transfer.html)
    file (as produced by e.g. EarthScope's fdsnws-station service) and
    exposes it as a [`Response`][pysmo.Response]-compatible object. `SacPZ` only ever
    satisfies [`Response`][pysmo.Response], never
    [`StagedResponse`][pysmo.StagedResponse] — the SAC PZ format has no
    digital-stage fields to parse.

    Examples:
        ```python
        >>> from pysmo.classes import SacPZ
        >>> text = '''\
        ... * NETWORK   (KNETWK): IU
        ... * STATION    (KSTNM): ANMO
        ... * LOCATION   (KHOLE): 00
        ... * CHANNEL   (KCMPNM): BHZ
        ... * START             : 2018-07-09T20:45:00
        ... * END               :
        ... * INPUT UNIT        : M
        ... ZEROS 2
        ... \t+0.000000e+00\t+0.000000e+00
        ... \t+0.000000e+00\t+0.000000e+00
        ... POLES 1
        ... \t-1.000000e-02\t+0.000000e+00
        ... CONSTANT 1.0e+09
        ... '''
        >>> response = SacPZ.from_text(text)
        >>> response.network, response.station
        ('IU', 'ANMO')
        >>>
        ```
    """

    poles: list[complex] = field(converter=convert_to_complex_list)
    """Response poles.

    See [`Response.poles`][pysmo.Response.poles] for more details.
    """

    zeros: list[complex] = field(converter=convert_to_complex_list)
    """Response zeros.

    See [`Response.zeros`][pysmo.Response.zeros] for more details.
    """

    overall_sensitivity: NonZeroNumber = field(
        converter=float, validator=validate_nonzero
    )
    """Total system sensitivity (the SAC PZ file's `CONSTANT`).

    See [`Response.overall_sensitivity`][pysmo.Response.overall_sensitivity]
    for more details.
    """

    reference_sensitivity: NonZeroNumber | None = field(
        default=None,
        converter=_convert_optional_float,
        validator=validators.optional(validate_nonzero),
    )
    """Total system sensitivity at the reference frequency, `A0` excluded
    (the SAC PZ file's `SENSITIVITY` header, if present).

    See
    [`Response.reference_sensitivity`][pysmo.Response.reference_sensitivity]
    for more details.
    """

    input_units: str = field(validator=validators.instance_of(str))
    """Physical units produced by removing this response via full spectral
    deconvolution — not necessarily via the sensitivity-only path, see
    [`remove_response`][pysmo.tools.signal.remove_response] for why.

    See [`Response.input_units`][pysmo.Response.input_units] for more details.
    """

    network: str = field(validator=validators.instance_of(str))
    """Network code parsed from the SAC PZ file's comment header."""

    station: str = field(validator=validators.instance_of(str))
    """Station code parsed from the SAC PZ file's comment header."""

    location: str = field(validator=validators.instance_of(str))
    """Location code parsed from the SAC PZ file's comment header."""

    channel: str = field(validator=validators.instance_of(str))
    """Channel code parsed from the SAC PZ file's comment header."""

    start_date: pd.Timestamp = field(converter=convert_to_utc_timestamp)
    """Start of the epoch this response applies to."""

    end_date: pd.Timestamp | None = field(
        default=None, converter=converters.optional(convert_to_utc_timestamp)
    )
    """End of the epoch this response applies to, or `None` if still open."""

    @classmethod
    def from_text(cls, text: str) -> Self:
        """Create a new instance from a single-record SAC PZ text body.

        Args:
            text: SAC PZ text containing exactly one record (the common
                sidecar-file case, e.g. one `.pz`/`SACPZ.NET.STA.LOC.CHA`
                file matched to one channel epoch by filename convention).

        Returns:
            A new SacPZ instance.

        Raises:
            ValueError: If the text contains zero or more than one SAC PZ
                record.

        Tip: See Also
            [`SacPZ.all_from_text`][pysmo.classes.SacPZ.all_from_text]: Parse
            a bulk/concatenated multi-record text body.

        Examples:
            Reading a SAC PZ file already saved to disk — the common case for
            archived/legacy data, e.g. extracted from an old SEED volume with
            `rdseed -p`, rather than fetched live from EarthScope:

            ```python
            >>> from pathlib import Path
            >>> from pysmo.classes import SacPZ
            >>> text = Path("SACPZ.IU.ANMO.00.BHZ").read_text()
            >>> response = SacPZ.from_text(text)
            >>> response.network, response.station
            ('IU', 'ANMO')
            >>>
            ```
        """
        records = parse_sacpz(text)
        if len(records) != 1:
            raise ValueError(
                f"Expected exactly one SAC PZ record in text, found {len(records)}."
            )
        return cls._from_raw(records[0])

    @classmethod
    def fetch(cls, *, station: Station, time: pd.Timestamp | None = None) -> Self:
        """Fetch and parse an instrument response as SAC PZ from EarthScope's fdsnws-station service, selecting one epoch.

        The response comes from `fdsnws-station` with
        `level=response&format=sacpz`, EarthScope's designated replacement
        for the `irisws-sacpz` service.

        Unlike [`StationXML.fetch`][pysmo.classes.StationXML.fetch], epoch
        selection happens server-side: the web service's `time` parameter
        is passed through, so exactly one record is returned (the epoch
        active at *time* if given, otherwise the one currently open)
        without needing to fetch the full response history first.

        Args:
            station: Any object satisfying the [`Station`][pysmo.Station]
                protocol. Provides the network, station code, location, and
                channel for the request.
            time: Timestamp used to select the response epoch. If `None`,
                the currently-open epoch is selected.

        Returns:
            A new SacPZ instance for the response epoch active at *time*
            (or currently open, if `time` is `None`).

        Raises:
            ValueError: If the web service's response does not contain
                exactly one SAC PZ record.
            urllib3.exceptions.ResponseError: If the web service returns an
                HTTP error.

        Tip: Prefer StationXML for live fetches
            When fetching live from EarthScope rather than reading an
            existing SAC PZ file, prefer
            [`StationXML.fetch`][pysmo.classes.StationXML.fetch]:
            it also captures digital FIR/IIR stages, so it always satisfies
            [`StagedResponse`][pysmo.StagedResponse], unlike `SacPZ`.

        Examples:
            <!-- skip: start if(not run_real_web_requests) -->
            ```python
            >>> from pysmo import MiniStation
            >>> from pysmo.classes import SacPZ
            >>> station = MiniStation(
            ...     name="ANMO", network="IU", location="00", channel="BHZ",
            ...     latitude=34.945981, longitude=-106.457133,
            ... )
            >>> response = SacPZ.fetch(station=station)
            >>>
            ```
            <!-- skip: end -->
        """
        text = fetch_sacpz(station=station, time=time)
        return cls.from_text(text)

    @classmethod
    def all_from_text(cls, text: str) -> list[Self]:
        """Create one instance per record in a bulk/concatenated SAC PZ text body.

        Unlike [`from_text`][pysmo.classes.SacPZ.from_text], this does not
        require (or merge to) a single record — a SACPZ retrieval that is
        not pinned to a single channel epoch returns multiple concatenated
        records, each with its own `network`/`station`/`location`/`channel`/
        `start_date`/`end_date` provenance, which callers can filter
        themselves.

        Args:
            text: SAC PZ text containing one or more records.

        Returns:
            One SacPZ instance per record found, in order of appearance.
        """
        return [cls._from_raw(record) for record in parse_sacpz(text)]

    @classmethod
    def _from_raw(cls, record: _RawSacPzResponse) -> Self:
        return cls(
            poles=record.poles,
            zeros=record.zeros,
            overall_sensitivity=record.overall_sensitivity,
            reference_sensitivity=record.reference_sensitivity,
            input_units=record.input_units,
            network=record.network,
            station=record.station,
            location=record.location,
            channel=record.channel,
            start_date=record.start_date,
            end_date=record.end_date,
        )

channel class-attribute instance-attribute

channel: str = field(validator=validators.instance_of(str))

Channel code parsed from the SAC PZ file's comment header.

end_date class-attribute instance-attribute

end_date: Timestamp | None = field(
    default=None,
    converter=converters.optional(convert_to_utc_timestamp),
)

End of the epoch this response applies to, or None if still open.

input_units class-attribute instance-attribute

input_units: str = field(
    validator=validators.instance_of(str)
)

Physical units produced by removing this response via full spectral deconvolution — not necessarily via the sensitivity-only path, see remove_response for why.

See Response.input_units for more details.

location class-attribute instance-attribute

location: str = field(validator=validators.instance_of(str))

Location code parsed from the SAC PZ file's comment header.

network class-attribute instance-attribute

network: str = field(validator=validators.instance_of(str))

Network code parsed from the SAC PZ file's comment header.

overall_sensitivity class-attribute instance-attribute

overall_sensitivity: NonZeroNumber = field(
    converter=float, validator=validate_nonzero
)

Total system sensitivity (the SAC PZ file's CONSTANT).

See Response.overall_sensitivity for more details.

poles class-attribute instance-attribute

poles: list[complex] = field(
    converter=convert_to_complex_list
)

Response poles.

See Response.poles for more details.

reference_sensitivity class-attribute instance-attribute

reference_sensitivity: NonZeroNumber | None = field(
    default=None,
    converter=_convert_optional_float,
    validator=validators.optional(validate_nonzero),
)

Total system sensitivity at the reference frequency, A0 excluded (the SAC PZ file's SENSITIVITY header, if present).

See Response.reference_sensitivity for more details.

start_date class-attribute instance-attribute

start_date: Timestamp = field(
    converter=convert_to_utc_timestamp
)

Start of the epoch this response applies to.

station class-attribute instance-attribute

station: str = field(validator=validators.instance_of(str))

Station code parsed from the SAC PZ file's comment header.

zeros class-attribute instance-attribute

zeros: list[complex] = field(
    converter=convert_to_complex_list
)

Response zeros.

See Response.zeros for more details.

all_from_text classmethod

all_from_text(text: str) -> list[Self]

Create one instance per record in a bulk/concatenated SAC PZ text body.

Unlike from_text, this does not require (or merge to) a single record — a SACPZ retrieval that is not pinned to a single channel epoch returns multiple concatenated records, each with its own network/station/location/channel/ start_date/end_date provenance, which callers can filter themselves.

Parameters:

Name Type Description Default
text str

SAC PZ text containing one or more records.

required

Returns:

Type Description
list[Self]

One SacPZ instance per record found, in order of appearance.

Source code in src/pysmo/classes/_sacpz.py
@classmethod
def all_from_text(cls, text: str) -> list[Self]:
    """Create one instance per record in a bulk/concatenated SAC PZ text body.

    Unlike [`from_text`][pysmo.classes.SacPZ.from_text], this does not
    require (or merge to) a single record — a SACPZ retrieval that is
    not pinned to a single channel epoch returns multiple concatenated
    records, each with its own `network`/`station`/`location`/`channel`/
    `start_date`/`end_date` provenance, which callers can filter
    themselves.

    Args:
        text: SAC PZ text containing one or more records.

    Returns:
        One SacPZ instance per record found, in order of appearance.
    """
    return [cls._from_raw(record) for record in parse_sacpz(text)]

fetch classmethod

fetch(
    *, station: Station, time: Timestamp | None = None
) -> Self

Fetch and parse an instrument response as SAC PZ from EarthScope's fdsnws-station service, selecting one epoch.

The response comes from fdsnws-station with level=response&format=sacpz, EarthScope's designated replacement for the irisws-sacpz service.

Unlike StationXML.fetch, epoch selection happens server-side: the web service's time parameter is passed through, so exactly one record is returned (the epoch active at time if given, otherwise the one currently open) without needing to fetch the full response history first.

Parameters:

Name Type Description Default
station Station

Any object satisfying the Station protocol. Provides the network, station code, location, and channel for the request.

required
time Timestamp | None

Timestamp used to select the response epoch. If None, the currently-open epoch is selected.

None

Returns:

Type Description
Self

A new SacPZ instance for the response epoch active at time

Self

(or currently open, if time is None).

Raises:

Type Description
ValueError

If the web service's response does not contain exactly one SAC PZ record.

ResponseError

If the web service returns an HTTP error.

Prefer StationXML for live fetches

When fetching live from EarthScope rather than reading an existing SAC PZ file, prefer StationXML.fetch: it also captures digital FIR/IIR stages, so it always satisfies StagedResponse, unlike SacPZ.

Examples:

>>> from pysmo import MiniStation
>>> from pysmo.classes import SacPZ
>>> station = MiniStation(
...     name="ANMO", network="IU", location="00", channel="BHZ",
...     latitude=34.945981, longitude=-106.457133,
... )
>>> response = SacPZ.fetch(station=station)
>>>
Source code in src/pysmo/classes/_sacpz.py
@classmethod
def fetch(cls, *, station: Station, time: pd.Timestamp | None = None) -> Self:
    """Fetch and parse an instrument response as SAC PZ from EarthScope's fdsnws-station service, selecting one epoch.

    The response comes from `fdsnws-station` with
    `level=response&format=sacpz`, EarthScope's designated replacement
    for the `irisws-sacpz` service.

    Unlike [`StationXML.fetch`][pysmo.classes.StationXML.fetch], epoch
    selection happens server-side: the web service's `time` parameter
    is passed through, so exactly one record is returned (the epoch
    active at *time* if given, otherwise the one currently open)
    without needing to fetch the full response history first.

    Args:
        station: Any object satisfying the [`Station`][pysmo.Station]
            protocol. Provides the network, station code, location, and
            channel for the request.
        time: Timestamp used to select the response epoch. If `None`,
            the currently-open epoch is selected.

    Returns:
        A new SacPZ instance for the response epoch active at *time*
        (or currently open, if `time` is `None`).

    Raises:
        ValueError: If the web service's response does not contain
            exactly one SAC PZ record.
        urllib3.exceptions.ResponseError: If the web service returns an
            HTTP error.

    Tip: Prefer StationXML for live fetches
        When fetching live from EarthScope rather than reading an
        existing SAC PZ file, prefer
        [`StationXML.fetch`][pysmo.classes.StationXML.fetch]:
        it also captures digital FIR/IIR stages, so it always satisfies
        [`StagedResponse`][pysmo.StagedResponse], unlike `SacPZ`.

    Examples:
        <!-- skip: start if(not run_real_web_requests) -->
        ```python
        >>> from pysmo import MiniStation
        >>> from pysmo.classes import SacPZ
        >>> station = MiniStation(
        ...     name="ANMO", network="IU", location="00", channel="BHZ",
        ...     latitude=34.945981, longitude=-106.457133,
        ... )
        >>> response = SacPZ.fetch(station=station)
        >>>
        ```
        <!-- skip: end -->
    """
    text = fetch_sacpz(station=station, time=time)
    return cls.from_text(text)

from_text classmethod

from_text(text: str) -> Self

Create a new instance from a single-record SAC PZ text body.

Parameters:

Name Type Description Default
text str

SAC PZ text containing exactly one record (the common sidecar-file case, e.g. one .pz/SACPZ.NET.STA.LOC.CHA file matched to one channel epoch by filename convention).

required

Returns:

Type Description
Self

A new SacPZ instance.

Raises:

Type Description
ValueError

If the text contains zero or more than one SAC PZ record.

See Also

SacPZ.all_from_text: Parse a bulk/concatenated multi-record text body.

Examples:

Reading a SAC PZ file already saved to disk — the common case for archived/legacy data, e.g. extracted from an old SEED volume with rdseed -p, rather than fetched live from EarthScope:

>>> from pathlib import Path
>>> from pysmo.classes import SacPZ
>>> text = Path("SACPZ.IU.ANMO.00.BHZ").read_text()
>>> response = SacPZ.from_text(text)
>>> response.network, response.station
('IU', 'ANMO')
>>>
Source code in src/pysmo/classes/_sacpz.py
@classmethod
def from_text(cls, text: str) -> Self:
    """Create a new instance from a single-record SAC PZ text body.

    Args:
        text: SAC PZ text containing exactly one record (the common
            sidecar-file case, e.g. one `.pz`/`SACPZ.NET.STA.LOC.CHA`
            file matched to one channel epoch by filename convention).

    Returns:
        A new SacPZ instance.

    Raises:
        ValueError: If the text contains zero or more than one SAC PZ
            record.

    Tip: See Also
        [`SacPZ.all_from_text`][pysmo.classes.SacPZ.all_from_text]: Parse
        a bulk/concatenated multi-record text body.

    Examples:
        Reading a SAC PZ file already saved to disk — the common case for
        archived/legacy data, e.g. extracted from an old SEED volume with
        `rdseed -p`, rather than fetched live from EarthScope:

        ```python
        >>> from pathlib import Path
        >>> from pysmo.classes import SacPZ
        >>> text = Path("SACPZ.IU.ANMO.00.BHZ").read_text()
        >>> response = SacPZ.from_text(text)
        >>> response.network, response.station
        ('IU', 'ANMO')
        >>>
        ```
    """
    records = parse_sacpz(text)
    if len(records) != 1:
        raise ValueError(
            f"Expected exactly one SAC PZ record in text, found {len(records)}."
        )
    return cls._from_raw(records[0])

SacSeismogram

Bases: _SacNested, SeismogramEndtimeMixin

Helper class for SAC seismogram attributes.

The SacSeismogram class maps SAC attributes to match the pysmo Seismogram type. An instance is created for each new SAC instance.

Examples:

A SacSeismogram can be passed to any function that expects the pysmo Seismogram type:

>>> from pysmo import Seismogram
>>> from pysmo.classes import SAC
>>>
>>> def begin_time_isoformat(seismogram: Seismogram) -> str:
...     return seismogram.begin_time.isoformat()
...
>>> sac = SAC.from_file("example.sac")
>>> begin_time_isoformat(sac.seismogram)
'2010-02-27T06:44:06.069538+00:00'
>>>

Timing operations in a SAC file use a reference time, and all times (begin time, event origin time, picks, etc.) are relative to this reference time. In pysmo only absolute times are used. The example below shows the begin_time is the absolute time (in UTC) of the first data point:

>>> sac.seismogram.begin_time
Timestamp('2010-02-27 06:44:06.069538+0000', tz='UTC')
>>>

Attributes:

Name Type Description
begin_time UtcTimestamp

Seismogram begin time.

data NDArray[floating]

Seismogram data.

delta PositiveTimedelta

Sampling interval.

Source code in src/pysmo/classes/_sac.py
@define(kw_only=True)
class SacSeismogram(_SacNested, SeismogramEndtimeMixin):
    """Helper class for SAC seismogram attributes.

    The `SacSeismogram` class maps SAC attributes to match the pysmo
    [`Seismogram`][pysmo.Seismogram] type. An instance is created for each
    new [`SAC`][pysmo.classes.SAC] instance.

    Examples:
        A SacSeismogram can be passed to any function that expects the pysmo
        [`Seismogram`][pysmo.Seismogram] type:

        ```python
        >>> from pysmo import Seismogram
        >>> from pysmo.classes import SAC
        >>>
        >>> def begin_time_isoformat(seismogram: Seismogram) -> str:
        ...     return seismogram.begin_time.isoformat()
        ...
        >>> sac = SAC.from_file("example.sac")
        >>> begin_time_isoformat(sac.seismogram)
        '2010-02-27T06:44:06.069538+00:00'
        >>>
        ```

        Timing operations in a SAC file use a reference time, and all times
        (begin time, event origin time, picks, etc.) are relative to this
        reference time. In pysmo only absolute times are used. The example
        below shows the `begin_time` is the absolute time (in UTC) of the first
        data point:

        ```python
        >>> sac.seismogram.begin_time
        Timestamp('2010-02-27 06:44:06.069538+0000', tz='UTC')
        >>>
        ```
    """

    @property
    def data(self) -> npt.NDArray[np.floating]:
        """Seismogram data."""

        return self._parent.data

    @data.setter
    def data(self, value: npt.NDArray[np.floating]) -> None:
        self._parent.data = value

    @property
    def delta(self) -> PositiveTimedelta:
        """Sampling interval."""
        return pd.Timedelta(seconds=self._parent.delta)

    @delta.setter
    def delta(self, value: pd.Timedelta) -> None:
        if value <= pd.Timedelta(0):
            raise ValueError("delta must be a positive Timedelta.")
        self._parent.delta = value.total_seconds()

    @property
    def begin_time(self) -> UtcTimestamp:
        """Seismogram begin time."""

        return self._get_timestamp_from_sac(SAC_REQUIRED_TIME_HEADERS.b)

    @begin_time.setter
    def begin_time(self, value: pd.Timestamp) -> None:
        self._set_sac_from_timestamp(SAC_REQUIRED_TIME_HEADERS.b, value)

begin_time property writable

begin_time: UtcTimestamp

Seismogram begin time.

data property writable

Seismogram data.

delta property writable

Sampling interval.

SacStation

Bases: _SacNested

Helper class for SAC station attributes.

The SacStation class maps SAC attributes to match the pysmo Station type. An instance is created for each new SAC instance.

Examples:

A SacStation can be passed to any function that expects the pysmo Station type:

>>> from pysmo.classes import SAC
>>> from pysmo import Station
>>>
>>> def station_id(station: Station) -> str:
...     return f"{station.network}.{station.name}"
...
>>> sac = SAC.from_file("example.sac")
>>> station_id(sac.station)
'IU.ANMO'
>>>

Attributes:

Name Type Description
channel str

Channel code.

elevation int | float | None

Station elevation in metres.

latitude int | float

Station latitude.

location str

Location code.

longitude int | float

Station longitude.

name str

Station name or code.

network str

Network name or code.

Source code in src/pysmo/classes/_sac.py
@define(kw_only=True)
class SacStation(_SacNested):
    """Helper class for SAC station attributes.

    The `SacStation` class maps SAC attributes to match the pysmo
    [`Station`][pysmo.Station] type. An instance is created for each new
    [`SAC`][pysmo.classes.SAC] instance.

    Examples:
        A SacStation can be passed to any function that expects the pysmo
        [`Station`][pysmo.Station] type:

        ```python
        >>> from pysmo.classes import SAC
        >>> from pysmo import Station
        >>>
        >>> def station_id(station: Station) -> str:
        ...     return f"{station.network}.{station.name}"
        ...
        >>> sac = SAC.from_file("example.sac")
        >>> station_id(sac.station)
        'IU.ANMO'
        >>>
        ```
    """

    @property
    def name(self) -> str:
        """Station name or code."""

        if self._parent.kstnm is None:
            raise TypeError("SAC object station name 'kstnm' is None.")
        return self._parent.kstnm

    @name.setter
    def name(self, value: str) -> None:
        setattr(self._parent, "kstnm", value)

    @property
    def network(self) -> str:
        """Network name or code."""

        if self._parent.knetwk is None:
            raise TypeError("SAC object network name 'knetwk' is None.")

        return self._parent.knetwk

    @network.setter
    def network(self, value: str) -> None:
        setattr(self._parent, "knetwk", value)

    @property
    def location(self) -> str:
        """Location code.

        Unlike the other station identifiers, a missing location code
        (`khole` not set) is common in real-world SAC files and is not
        treated as an error - it is returned as an empty string.
        """

        return self._parent.khole or ""

    @location.setter
    def location(self, value: str) -> None:
        setattr(self._parent, "khole", value)

    @property
    def channel(self) -> str:
        """Channel code."""

        if self._parent.kcmpnm is None:
            raise TypeError("SAC object channel code 'kcmpnm' is None.")
        return self._parent.kcmpnm

    @channel.setter
    def channel(self, value: str) -> None:
        setattr(self._parent, "kcmpnm", value)

    @property
    def latitude(self) -> int | float:
        """Station latitude."""

        if self._parent.stla is None:
            raise TypeError("SAC object station latitude 'stla' is None.")
        return self._parent.stla

    @latitude.setter
    def latitude(self, value: int | float) -> None:
        setattr(self._parent, "stla", value)

    @property
    def longitude(self) -> int | float:
        """Station longitude."""

        if self._parent.stlo is None:
            raise TypeError("SAC object station longitude 'stlo' is None.")
        return self._parent.stlo

    @longitude.setter
    def longitude(self, value: int | float) -> None:
        setattr(self._parent, "stlo", value)

    @property
    def elevation(self) -> int | float | None:
        """Station elevation in metres."""

        return self._parent.stel

    @elevation.setter
    def elevation(self, value: int | float | None) -> None:
        setattr(self._parent, "stel", value)

channel property writable

channel: str

Channel code.

elevation property writable

elevation: int | float | None

Station elevation in metres.

latitude property writable

latitude: int | float

Station latitude.

location property writable

location: str

Location code.

Unlike the other station identifiers, a missing location code (khole not set) is common in real-world SAC files and is not treated as an error - it is returned as an empty string.

longitude property writable

longitude: int | float

Station longitude.

name property writable

name: str

Station name or code.

network property writable

network: str

Network name or code.

SacTimestamps

Bases: _SacNested

Helper class to access times stored in SAC headers as Timestamp objects.

The SacTimestamps class maps raw SAC time headers — relative to a file's own reference time — to absolute Timestamp objects. An instance of this class is created for each new SAC instance.

Examples:

Relative seismogram begin time as a float vs absolute begin time as a Timestamp object.

>>> from pysmo.classes import SAC
>>> sac = SAC.from_file("example.sac")
>>>
>>> # SAC header "B" as stored in a SAC file
>>> sac.native.b
0.0005380000220611691
>>>
>>> # the output above is the number of seconds relative
>>> # to the reference time and date:
>>> sac.native.kzdate , sac.native.kztime
('2010-02-27', '06:44:06.069')
>>>
>>> # Accessing the same SAC header via a `SacTimestamps` object
>>> # yields a corresponding Timestamp object with the absolute time:
>>> sac.timestamps.b
Timestamp('2010-02-27 06:44:06.069538+0000', tz='UTC')
>>>

Changing timestamp values:

>>> import pandas as pd
>>> sac = SAC.from_file("example.sac")
>>>
>>> # Original value of the "B" SAC header:
>>> sac.native.b
0.0005380000220611691
>>>
>>> # Add 30 seconds to the absolute time:
>>> sac.timestamps.b += pd.Timedelta(seconds=30)
>>>
>>> # The relative time also changes by the same amount:
>>> sac.native.b
30.000538
>>>
>>> # Changing b to None is not allowed (it is a required time header):
>>> sac.timestamps.b = None
Traceback (most recent call last):
...
TypeError: ...
>>>

Attributes:

Name Type Description
a OptionalSacTimestamp

First arrival time.

b RequiredSacTimestamp

Beginning time of the independent variable.

e RequiredSacTimestamp

Ending time of the independent variable (read-only).

f OptionalSacTimestamp

Fini or end of event time.

o OptionalSacTimestamp

Event origin time.

t0 OptionalSacTimestamp

User defined time pick or marker 0.

t1 OptionalSacTimestamp

User defined time pick or marker 1.

t2 OptionalSacTimestamp

User defined time pick or marker 2.

t3 OptionalSacTimestamp

User defined time pick or marker 3.

t4 OptionalSacTimestamp

User defined time pick or marker 4.

t5 OptionalSacTimestamp

User defined time pick or marker 5.

t6 OptionalSacTimestamp

User defined time pick or marker 6.

t7 OptionalSacTimestamp

User defined time pick or marker 7.

t8 OptionalSacTimestamp

User defined time pick or marker 8.

t9 OptionalSacTimestamp

User defined time pick or marker 9.

Source code in src/pysmo/classes/_sac.py
class SacTimestamps(_SacNested):
    """Helper class to access times stored in SAC headers as [`Timestamp`][pandas.Timestamp] objects.

    The `SacTimestamps` class maps raw SAC time headers — relative to a
    file's own reference time — to absolute [`Timestamp`][pandas.Timestamp]
    objects. An instance of this class is created for each new
    [`SAC`][pysmo.classes.SAC] instance.

    Examples:
        Relative seismogram begin time as a float vs absolute begin time
        as a [`Timestamp`][pandas.Timestamp] object.

        ```python
        >>> from pysmo.classes import SAC
        >>> sac = SAC.from_file("example.sac")
        >>>
        >>> # SAC header "B" as stored in a SAC file
        >>> sac.native.b
        0.0005380000220611691
        >>>
        >>> # the output above is the number of seconds relative
        >>> # to the reference time and date:
        >>> sac.native.kzdate , sac.native.kztime
        ('2010-02-27', '06:44:06.069')
        >>>
        >>> # Accessing the same SAC header via a `SacTimestamps` object
        >>> # yields a corresponding Timestamp object with the absolute time:
        >>> sac.timestamps.b
        Timestamp('2010-02-27 06:44:06.069538+0000', tz='UTC')
        >>>
        ```

        Changing timestamp values:

        ```python
        >>> import pandas as pd
        >>> sac = SAC.from_file("example.sac")
        >>>
        >>> # Original value of the "B" SAC header:
        >>> sac.native.b
        0.0005380000220611691
        >>>
        >>> # Add 30 seconds to the absolute time:
        >>> sac.timestamps.b += pd.Timedelta(seconds=30)
        >>>
        >>> # The relative time also changes by the same amount:
        >>> sac.native.b
        30.000538
        >>>
        >>> # Changing b to None is not allowed (it is a required time header):
        >>> sac.timestamps.b = None
        Traceback (most recent call last):
        ...
        TypeError: ...
        >>>
        ```
    """

    __slots__ = ()

    b: RequiredSacTimestamp = RequiredSacTimestamp()
    """Beginning time of the independent variable."""

    e: RequiredSacTimestamp = RequiredSacTimestamp(readonly=True)
    """Ending time of the independent variable (read-only)."""

    o: OptionalSacTimestamp = OptionalSacTimestamp()
    """Event origin time."""

    a: OptionalSacTimestamp = OptionalSacTimestamp()
    """First arrival time."""

    f: OptionalSacTimestamp = OptionalSacTimestamp()
    """Fini or end of event time."""

    t0: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 0."""

    t1: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 1."""

    t2: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 2."""

    t3: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 3."""

    t4: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 4."""

    t5: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 5."""

    t6: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 6."""

    t7: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 7."""

    t8: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 8."""

    t9: OptionalSacTimestamp = OptionalSacTimestamp()
    """User defined time pick or marker 9."""

a class-attribute instance-attribute

a: OptionalSacTimestamp = OptionalSacTimestamp()

First arrival time.

b class-attribute instance-attribute

b: RequiredSacTimestamp = RequiredSacTimestamp()

Beginning time of the independent variable.

e class-attribute instance-attribute

e: RequiredSacTimestamp = RequiredSacTimestamp(
    readonly=True
)

Ending time of the independent variable (read-only).

f class-attribute instance-attribute

f: OptionalSacTimestamp = OptionalSacTimestamp()

Fini or end of event time.

o class-attribute instance-attribute

o: OptionalSacTimestamp = OptionalSacTimestamp()

Event origin time.

t0 class-attribute instance-attribute

t0: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 0.

t1 class-attribute instance-attribute

t1: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 1.

t2 class-attribute instance-attribute

t2: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 2.

t3 class-attribute instance-attribute

t3: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 3.

t4 class-attribute instance-attribute

t4: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 4.

t5 class-attribute instance-attribute

t5: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 5.

t6 class-attribute instance-attribute

t6: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 6.

t7 class-attribute instance-attribute

t7: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 7.

t8 class-attribute instance-attribute

t8: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 8.

t9 class-attribute instance-attribute

t9: OptionalSacTimestamp = OptionalSacTimestamp()

User defined time pick or marker 9.

StationXML

Import class for FDSN StationXML station metadata.

Reads one <Channel> epoch from a FDSN StationXML document and exposes it as a Station-compatible object: NSLC identity, coordinates, the epoch's validity window, and — when the document was fetched at level=response — the instrument response.

A document commonly covers a channel's full history, i.e. several epochs. from_bytes narrows to one (matching a time, or the currently-open one); all_from_bytes returns every epoch found. Accessing response raises for an epoch parsed from a level=channel / level=station document (e.g. a bulk inventory fetched with pysmo.tools.web.fetch_station_inventory) — guard it with has_response. fetch always populates it.

Examples:

>>> from pysmo.classes import StationXML
>>> xml = b'''\
... <?xml version="1.0"?>
... <FDSNStationXML xmlns="http://www.fdsn.org/xml/station/1">
...   <Network code="IU">
...     <Station code="ANMO">
...       <Latitude>34.9</Latitude><Longitude>-106.5</Longitude>
...       <Channel code="BHZ" locationCode="00"
...                startDate="2018-07-09T20:45:00.0000">
...         <Latitude>34.945981</Latitude><Longitude>-106.457133</Longitude>
...         <Elevation>1632.7</Elevation>
...         <Response>
...           <InstrumentSensitivity>
...             <Value>1.98475E9</Value>
...             <InputUnits><Name>m/s</Name></InputUnits>
...           </InstrumentSensitivity>
...           <Stage number="1">
...             <PolesZeros>
...               <PzTransferFunctionType>LAPLACE (RADIANS/SECOND)</PzTransferFunctionType>
...               <NormalizationFactor>5.03773E14</NormalizationFactor>
...               <Zero number="0"><Real>0.0</Real><Imaginary>0.0</Imaginary></Zero>
...               <Pole number="0"><Real>-0.037</Real><Imaginary>0.037</Imaginary></Pole>
...             </PolesZeros>
...             <Decimation><InputSampleRate>40.0</InputSampleRate><Factor>1</Factor></Decimation>
...           </Stage>
...         </Response>
...       </Channel>
...     </Station>
...   </Network>
... </FDSNStationXML>'''
>>> station = StationXML.from_bytes(xml)
>>> station.network, station.name, station.channel
('IU', 'ANMO', 'BHZ')
>>> station.response.input_units
'm/s'
>>>

Methods:

Name Description
all_from_bytes

Create one instance per <Channel> epoch in a StationXML document.

fetch

Fetch one channel's response epoch from the EarthScope FDSN station web service.

from_bytes

Create a new instance from a StationXML document, selecting one epoch.

Attributes:

Name Type Description
channel str

Channel code (empty for a level=station epoch).

elevation float | None

Elevation in metres, or None if the document omits it.

end_date Timestamp | None

End of this metadata epoch, or None if still open.

has_response bool

Whether this epoch carries an instrument response.

latitude float

Latitude in degrees.

location str

Location code (empty for a level=station epoch).

longitude float

Longitude in degrees.

name str

Station code.

network str

Network code.

response MiniStagedResponse

This epoch's instrument response.

start_date Timestamp

Start of this metadata epoch.

Source code in src/pysmo/classes/_stationxml.py
@define(kw_only=True)
class StationXML:
    r"""Import class for FDSN StationXML station metadata.

    Reads one `<Channel>` epoch from a
    [FDSN StationXML](http://www.fdsn.org/xml/station/) document and exposes
    it as a [`Station`][pysmo.Station]-compatible object: NSLC identity,
    coordinates, the epoch's validity window, and — when the document was
    fetched at `level=response` — the instrument
    [`response`][pysmo.classes.StationXML.response].

    A document commonly covers a channel's full history, i.e. several
    epochs. [`from_bytes`][pysmo.classes.StationXML.from_bytes] narrows to
    one (matching a time, or the currently-open one);
    [`all_from_bytes`][pysmo.classes.StationXML.all_from_bytes] returns every
    epoch found. Accessing
    [`response`][pysmo.classes.StationXML.response] raises for an epoch
    parsed from a `level=channel` / `level=station` document (e.g. a bulk
    inventory fetched with
    [`pysmo.tools.web.fetch_station_inventory`][]) — guard it with
    [`has_response`][pysmo.classes.StationXML.has_response].
    [`fetch`][pysmo.classes.StationXML.fetch] always populates it.

    Examples:
        ```python
        >>> from pysmo.classes import StationXML
        >>> xml = b'''\
        ... <?xml version="1.0"?>
        ... <FDSNStationXML xmlns="http://www.fdsn.org/xml/station/1">
        ...   <Network code="IU">
        ...     <Station code="ANMO">
        ...       <Latitude>34.9</Latitude><Longitude>-106.5</Longitude>
        ...       <Channel code="BHZ" locationCode="00"
        ...                startDate="2018-07-09T20:45:00.0000">
        ...         <Latitude>34.945981</Latitude><Longitude>-106.457133</Longitude>
        ...         <Elevation>1632.7</Elevation>
        ...         <Response>
        ...           <InstrumentSensitivity>
        ...             <Value>1.98475E9</Value>
        ...             <InputUnits><Name>m/s</Name></InputUnits>
        ...           </InstrumentSensitivity>
        ...           <Stage number="1">
        ...             <PolesZeros>
        ...               <PzTransferFunctionType>LAPLACE (RADIANS/SECOND)</PzTransferFunctionType>
        ...               <NormalizationFactor>5.03773E14</NormalizationFactor>
        ...               <Zero number="0"><Real>0.0</Real><Imaginary>0.0</Imaginary></Zero>
        ...               <Pole number="0"><Real>-0.037</Real><Imaginary>0.037</Imaginary></Pole>
        ...             </PolesZeros>
        ...             <Decimation><InputSampleRate>40.0</InputSampleRate><Factor>1</Factor></Decimation>
        ...           </Stage>
        ...         </Response>
        ...       </Channel>
        ...     </Station>
        ...   </Network>
        ... </FDSNStationXML>'''
        >>> station = StationXML.from_bytes(xml)
        >>> station.network, station.name, station.channel
        ('IU', 'ANMO', 'BHZ')
        >>> station.response.input_units
        'm/s'
        >>>
        ```
    """

    network: str = field(validator=validators.instance_of(str))
    """Network code."""

    name: str = field(validator=validators.instance_of(str))
    """Station code."""

    location: str = field(validator=validators.instance_of(str))
    """Location code (empty for a `level=station` epoch)."""

    channel: str = field(validator=validators.instance_of(str))
    """Channel code (empty for a `level=station` epoch)."""

    latitude: float = field(converter=float)
    """Latitude in degrees."""

    longitude: float = field(converter=float)
    """Longitude in degrees."""

    elevation: float | None = field(default=None, converter=converters.optional(float))
    """Elevation in metres, or `None` if the document omits it."""

    start_date: pd.Timestamp = field(converter=convert_to_utc_timestamp)
    """Start of this metadata epoch."""

    end_date: pd.Timestamp | None = field(
        default=None, converter=converters.optional(convert_to_utc_timestamp)
    )
    """End of this metadata epoch, or `None` if still open."""

    _response: MiniStagedResponse | None = field(
        default=None,
        alias="response",
        repr=lambda value: "None" if value is None else "<MiniStagedResponse>",
    )
    """Backing store for [`response`][pysmo.classes.StationXML.response];
    `None` when the source document carried no `<Response>`."""

    @property
    def has_response(self) -> bool:
        """Whether this epoch carries an instrument response.

        Guard [`response`][pysmo.classes.StationXML.response] with this when
        an epoch might have come from a `level=channel` / `level=station`
        document (e.g. a bulk inventory).
        """
        return self._response is not None

    @property
    def response(self) -> MiniStagedResponse:
        """This epoch's instrument response.

        Satisfies [`Response`][pysmo.Response] and
        [`StagedResponse`][pysmo.StagedResponse] (`stages` is empty for a
        document with no digital decimation stages).

        Raises:
            ValueError: If this epoch was parsed from a document with no
                `<Response>` — check
                [`has_response`][pysmo.classes.StationXML.has_response]
                first, or fetch at `level=response`
                ([`StationXML.fetch`][pysmo.classes.StationXML.fetch]).
        """
        if self._response is None:
            raise ValueError(
                f"{self.network}.{self.name}.{self.location}.{self.channel} was "
                + "parsed from a document with no <Response>; fetch it at "
                + "level=response."
            )
        return self._response

    @classmethod
    def from_bytes(
        cls,
        xml: bytes,
        *,
        time: pd.Timestamp | None = None,
        network: str | None = None,
        station: str | None = None,
        location: str | None = None,
        channel: str | None = None,
    ) -> Self:
        """Create a new instance from a StationXML document, selecting one epoch.

        A document is not guaranteed to cover a single channel — a bulk or
        wildcard query can cover several networks and stations, each with
        every location/channel combination and its own epoch history.
        `network`/`station`/`location`/`channel` narrow to one before *time*
        is applied; without them, a document covering more than one raises
        the same "more than one epoch" error as an ambiguous *time*.

        Args:
            xml: Raw StationXML document bytes.
            time: Timestamp used to select the epoch. If `None`, the
                currently-open epoch (no end date) is selected.
            network: Network code to narrow to, if `xml` covers more than one.
            station: Station code to narrow to, if `xml` covers more than one.
            location: Location code to narrow to, if `xml` covers more than one.
            channel: Channel code to narrow to, if `xml` covers more than one.

        Returns:
            A new StationXML instance for the epoch active at *time* (or
            currently open, if *time* is `None`).

        Raises:
            ValueError: If, after narrowing, zero or more than one epoch
                matches *time* (or "currently open", if *time* is `None`).

        Tip: See Also
            [`StationXML.all_from_bytes`][pysmo.classes.StationXML.all_from_bytes]:
            Parse every epoch in the document without narrowing to one.
        """
        matches = _matching_epochs(
            parse_stationxml(xml),
            time,
            network=network,
            station=station,
            location=location,
            channel=channel,
        )
        if len(matches) != 1:
            raise ValueError(
                "Expected exactly one epoch in the given StationXML at "
                + f"{'the currently open epoch' if time is None else time}"
                + f"{f', network {network!r}' if network is not None else ''}"
                + f"{f', station {station!r}' if station is not None else ''}"
                + f"{f', location {location!r}' if location is not None else ''}"
                + f"{f', channel {channel!r}' if channel is not None else ''}, "
                + f"found {len(matches)}."
            )
        return cls._from_raw(matches[0])

    @classmethod
    def all_from_bytes(cls, xml: bytes) -> list[Self]:
        """Create one instance per `<Channel>` epoch in a StationXML document.

        Unlike [`from_bytes`][pysmo.classes.StationXML.from_bytes], this does
        not narrow — a document covering a channel's full history returns
        several, each with its own NSLC / `start_date` / `end_date`.

        Args:
            xml: Raw StationXML document bytes.

        Returns:
            One StationXML instance per epoch found, in document order.
        """
        return [cls._from_raw(raw) for raw in parse_stationxml(xml)]

    @classmethod
    def fetch(cls, *, station: Station, time: pd.Timestamp | None = None) -> Self:
        """Fetch one channel's response epoch from the EarthScope FDSN station web service.

        Fetches the full response history for the channel in one
        `level=response` request and narrows client-side to the epoch active
        at *time* (or the currently-open one). To fetch once and interpret
        later, use [`pysmo.tools.web.fetch_stationxml`][] with
        [`from_bytes`][pysmo.classes.StationXML.from_bytes] /
        [`all_from_bytes`][pysmo.classes.StationXML.all_from_bytes].

        Args:
            station: Any object satisfying the [`Station`][pysmo.Station]
                protocol. Provides the network, station, location and
                channel for the request.
            time: Timestamp used to select the epoch. If `None`, the
                currently-open epoch is selected.

        Returns:
            A new StationXML instance with `response` populated.

        Raises:
            ValueError: If zero or more than one epoch matches *time*, or if
                the fetched document carries no `<Response>`.
            urllib3.exceptions.ResponseError: If the station web service
                returns an HTTP error.

        Examples:
            <!-- skip: start if(not run_real_web_requests) -->
            ```python
            >>> from pysmo import MiniStation
            >>> from pysmo.classes import StationXML
            >>> station = MiniStation(
            ...     name="ANMO", network="IU", location="00", channel="BHZ",
            ...     latitude=34.945981, longitude=-106.457133,
            ... )
            >>> epoch = StationXML.fetch(station=station)
            >>> epoch.has_response
            True
            >>>
            ```
            <!-- skip: end -->
        """
        epoch = cls.from_bytes(fetch_stationxml(station=station), time=time)
        if not epoch.has_response:
            raise ValueError(
                "fetched StationXML at level=response but it carried no "
                + "<Response> element."
            )
        return epoch

    @classmethod
    def _from_raw(cls, raw: _RawStationEpoch) -> Self:
        response = None
        if raw.response is not None:
            response = MiniStagedResponse(
                poles=raw.response.poles,
                zeros=raw.response.zeros,
                overall_sensitivity=(
                    raw.response.normalization_factor * raw.response.sensitivity_value
                ),
                reference_sensitivity=raw.response.sensitivity_value,
                input_units=raw.response.sensitivity_input_units,
                stages=[
                    MiniResponseStage(
                        input_sample_rate=stage.input_sample_rate,
                        decimation_factor=stage.decimation_factor,
                        numerator=stage.numerator,
                        denominator=stage.denominator,
                        correction=stage.correction,
                    )
                    for stage in raw.response.digital_stages
                ],
            )
        return cls(
            network=raw.network,
            name=raw.station,
            location=raw.location,
            channel=raw.channel,
            latitude=raw.latitude,
            longitude=raw.longitude,
            elevation=raw.elevation,
            start_date=raw.start_date,
            end_date=raw.end_date,
            response=response,
        )

channel class-attribute instance-attribute

channel: str = field(validator=validators.instance_of(str))

Channel code (empty for a level=station epoch).

elevation class-attribute instance-attribute

elevation: float | None = field(
    default=None, converter=converters.optional(float)
)

Elevation in metres, or None if the document omits it.

end_date class-attribute instance-attribute

end_date: Timestamp | None = field(
    default=None,
    converter=converters.optional(convert_to_utc_timestamp),
)

End of this metadata epoch, or None if still open.

has_response property

has_response: bool

Whether this epoch carries an instrument response.

Guard response with this when an epoch might have come from a level=channel / level=station document (e.g. a bulk inventory).

latitude class-attribute instance-attribute

latitude: float = field(converter=float)

Latitude in degrees.

location class-attribute instance-attribute

location: str = field(validator=validators.instance_of(str))

Location code (empty for a level=station epoch).

longitude class-attribute instance-attribute

longitude: float = field(converter=float)

Longitude in degrees.

name class-attribute instance-attribute

name: str = field(validator=validators.instance_of(str))

Station code.

network class-attribute instance-attribute

network: str = field(validator=validators.instance_of(str))

Network code.

response property

This epoch's instrument response.

Satisfies Response and StagedResponse (stages is empty for a document with no digital decimation stages).

Raises:

Type Description
ValueError

If this epoch was parsed from a document with no <Response> — check has_response first, or fetch at level=response (StationXML.fetch).

start_date class-attribute instance-attribute

start_date: Timestamp = field(
    converter=convert_to_utc_timestamp
)

Start of this metadata epoch.

all_from_bytes classmethod

all_from_bytes(xml: bytes) -> list[Self]

Create one instance per <Channel> epoch in a StationXML document.

Unlike from_bytes, this does not narrow — a document covering a channel's full history returns several, each with its own NSLC / start_date / end_date.

Parameters:

Name Type Description Default
xml bytes

Raw StationXML document bytes.

required

Returns:

Type Description
list[Self]

One StationXML instance per epoch found, in document order.

Source code in src/pysmo/classes/_stationxml.py
@classmethod
def all_from_bytes(cls, xml: bytes) -> list[Self]:
    """Create one instance per `<Channel>` epoch in a StationXML document.

    Unlike [`from_bytes`][pysmo.classes.StationXML.from_bytes], this does
    not narrow — a document covering a channel's full history returns
    several, each with its own NSLC / `start_date` / `end_date`.

    Args:
        xml: Raw StationXML document bytes.

    Returns:
        One StationXML instance per epoch found, in document order.
    """
    return [cls._from_raw(raw) for raw in parse_stationxml(xml)]

fetch classmethod

fetch(
    *, station: Station, time: Timestamp | None = None
) -> Self

Fetch one channel's response epoch from the EarthScope FDSN station web service.

Fetches the full response history for the channel in one level=response request and narrows client-side to the epoch active at time (or the currently-open one). To fetch once and interpret later, use pysmo.tools.web.fetch_stationxml with from_bytes / all_from_bytes.

Parameters:

Name Type Description Default
station Station

Any object satisfying the Station protocol. Provides the network, station, location and channel for the request.

required
time Timestamp | None

Timestamp used to select the epoch. If None, the currently-open epoch is selected.

None

Returns:

Type Description
Self

A new StationXML instance with response populated.

Raises:

Type Description
ValueError

If zero or more than one epoch matches time, or if the fetched document carries no <Response>.

ResponseError

If the station web service returns an HTTP error.

Examples:

>>> from pysmo import MiniStation
>>> from pysmo.classes import StationXML
>>> station = MiniStation(
...     name="ANMO", network="IU", location="00", channel="BHZ",
...     latitude=34.945981, longitude=-106.457133,
... )
>>> epoch = StationXML.fetch(station=station)
>>> epoch.has_response
True
>>>
Source code in src/pysmo/classes/_stationxml.py
@classmethod
def fetch(cls, *, station: Station, time: pd.Timestamp | None = None) -> Self:
    """Fetch one channel's response epoch from the EarthScope FDSN station web service.

    Fetches the full response history for the channel in one
    `level=response` request and narrows client-side to the epoch active
    at *time* (or the currently-open one). To fetch once and interpret
    later, use [`pysmo.tools.web.fetch_stationxml`][] with
    [`from_bytes`][pysmo.classes.StationXML.from_bytes] /
    [`all_from_bytes`][pysmo.classes.StationXML.all_from_bytes].

    Args:
        station: Any object satisfying the [`Station`][pysmo.Station]
            protocol. Provides the network, station, location and
            channel for the request.
        time: Timestamp used to select the epoch. If `None`, the
            currently-open epoch is selected.

    Returns:
        A new StationXML instance with `response` populated.

    Raises:
        ValueError: If zero or more than one epoch matches *time*, or if
            the fetched document carries no `<Response>`.
        urllib3.exceptions.ResponseError: If the station web service
            returns an HTTP error.

    Examples:
        <!-- skip: start if(not run_real_web_requests) -->
        ```python
        >>> from pysmo import MiniStation
        >>> from pysmo.classes import StationXML
        >>> station = MiniStation(
        ...     name="ANMO", network="IU", location="00", channel="BHZ",
        ...     latitude=34.945981, longitude=-106.457133,
        ... )
        >>> epoch = StationXML.fetch(station=station)
        >>> epoch.has_response
        True
        >>>
        ```
        <!-- skip: end -->
    """
    epoch = cls.from_bytes(fetch_stationxml(station=station), time=time)
    if not epoch.has_response:
        raise ValueError(
            "fetched StationXML at level=response but it carried no "
            + "<Response> element."
        )
    return epoch

from_bytes classmethod

from_bytes(
    xml: bytes,
    *,
    time: Timestamp | None = None,
    network: str | None = None,
    station: str | None = None,
    location: str | None = None,
    channel: str | None = None
) -> Self

Create a new instance from a StationXML document, selecting one epoch.

A document is not guaranteed to cover a single channel — a bulk or wildcard query can cover several networks and stations, each with every location/channel combination and its own epoch history. network/station/location/channel narrow to one before time is applied; without them, a document covering more than one raises the same "more than one epoch" error as an ambiguous time.

Parameters:

Name Type Description Default
xml bytes

Raw StationXML document bytes.

required
time Timestamp | None

Timestamp used to select the epoch. If None, the currently-open epoch (no end date) is selected.

None
network str | None

Network code to narrow to, if xml covers more than one.

None
station str | None

Station code to narrow to, if xml covers more than one.

None
location str | None

Location code to narrow to, if xml covers more than one.

None
channel str | None

Channel code to narrow to, if xml covers more than one.

None

Returns:

Type Description
Self

A new StationXML instance for the epoch active at time (or

Self

currently open, if time is None).

Raises:

Type Description
ValueError

If, after narrowing, zero or more than one epoch matches time (or "currently open", if time is None).

See Also

StationXML.all_from_bytes: Parse every epoch in the document without narrowing to one.

Source code in src/pysmo/classes/_stationxml.py
@classmethod
def from_bytes(
    cls,
    xml: bytes,
    *,
    time: pd.Timestamp | None = None,
    network: str | None = None,
    station: str | None = None,
    location: str | None = None,
    channel: str | None = None,
) -> Self:
    """Create a new instance from a StationXML document, selecting one epoch.

    A document is not guaranteed to cover a single channel — a bulk or
    wildcard query can cover several networks and stations, each with
    every location/channel combination and its own epoch history.
    `network`/`station`/`location`/`channel` narrow to one before *time*
    is applied; without them, a document covering more than one raises
    the same "more than one epoch" error as an ambiguous *time*.

    Args:
        xml: Raw StationXML document bytes.
        time: Timestamp used to select the epoch. If `None`, the
            currently-open epoch (no end date) is selected.
        network: Network code to narrow to, if `xml` covers more than one.
        station: Station code to narrow to, if `xml` covers more than one.
        location: Location code to narrow to, if `xml` covers more than one.
        channel: Channel code to narrow to, if `xml` covers more than one.

    Returns:
        A new StationXML instance for the epoch active at *time* (or
        currently open, if *time* is `None`).

    Raises:
        ValueError: If, after narrowing, zero or more than one epoch
            matches *time* (or "currently open", if *time* is `None`).

    Tip: See Also
        [`StationXML.all_from_bytes`][pysmo.classes.StationXML.all_from_bytes]:
        Parse every epoch in the document without narrowing to one.
    """
    matches = _matching_epochs(
        parse_stationxml(xml),
        time,
        network=network,
        station=station,
        location=location,
        channel=channel,
    )
    if len(matches) != 1:
        raise ValueError(
            "Expected exactly one epoch in the given StationXML at "
            + f"{'the currently open epoch' if time is None else time}"
            + f"{f', network {network!r}' if network is not None else ''}"
            + f"{f', station {station!r}' if station is not None else ''}"
            + f"{f', location {location!r}' if location is not None else ''}"
            + f"{f', channel {channel!r}' if channel is not None else ''}, "
            + f"found {len(matches)}."
        )
    return cls._from_raw(matches[0])

resolve_epochs

resolve_epochs(
    epochs: Iterable[StationXML], time: Timestamp
) -> list[StationXML]

Collapse station epochs to the one per NSLC valid at a given time.

Groups epochs by network/station/location/channel and, within each group, keeps the single epoch whose [start_date, end_date) window covers time (an epoch with no end_date is still open and covers any time at or after its start_date). An NSLC with no covering epoch is dropped — that station provably was not recording then.

Parameters:

Name Type Description Default
epochs Iterable[StationXML]

Station epochs, e.g. from StationXML.all_from_bytes.

required
time Timestamp

The time each NSLC's metadata is resolved at (UTC).

required

Returns:

Type Description
list[StationXML]

One StationXML per NSLC that has a covering epoch, in first-seen

list[StationXML]

NSLC order.

Raises:

Type Description
ValueError

If an NSLC has more than one epoch covering time (overlapping validity windows — an invalid inventory).

Source code in src/pysmo/classes/_stationxml.py
def resolve_epochs(
    epochs: Iterable[StationXML], time: pd.Timestamp
) -> list[StationXML]:
    """Collapse station epochs to the one per NSLC valid at a given time.

    Groups `epochs` by network/station/location/channel and, within each
    group, keeps the single epoch whose `[start_date, end_date)` window
    covers `time` (an epoch with no `end_date` is still open and covers any
    time at or after its `start_date`). An NSLC with no covering epoch is
    dropped — that station provably was not recording then.

    Args:
        epochs: Station epochs, e.g. from
            [`StationXML.all_from_bytes`][pysmo.classes.StationXML.all_from_bytes].
        time: The time each NSLC's metadata is resolved at (UTC).

    Returns:
        One `StationXML` per NSLC that has a covering epoch, in first-seen
        NSLC order.

    Raises:
        ValueError: If an NSLC has more than one epoch covering `time`
            (overlapping validity windows — an invalid inventory).
    """
    time = convert_to_utc_timestamp(time)
    grouped: dict[_Nslc, list[StationXML]] = defaultdict(list)
    for epoch in epochs:
        grouped[(epoch.network, epoch.name, epoch.location, epoch.channel)].append(
            epoch
        )

    resolved: list[StationXML] = []
    for nslc, group in grouped.items():
        covering = [
            epoch
            for epoch in group
            if epoch.start_date <= time
            and (epoch.end_date is None or time < epoch.end_date)
        ]
        if not covering:
            continue
        if len(covering) > 1:
            raise ValueError(
                f"{'.'.join(nslc)} has {len(covering)} epochs covering {time}."
            )
        resolved.append(covering[0])
    return resolved