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 (network/station/location/channel/ start_date/end_date) needed to select the right response from a document — 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.

SAC

Access and modify data stored in SAC files.

SacEvent

Helper class for SAC event attributes.

SacPZ

Import/export 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 response metadata.

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 import Seismogram
>>> 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)
>>> isinstance(seismogram, Seismogram)
True
>>> seismogram.sid
'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.sid
'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

Seismogram data.

delta PositiveTimedelta

Seismogram sampling interval.

sample_count int

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

sid str

FDSN source identifier of the parsed GeoCSV data (e.g. IU_ANMO_00_LHZ).

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 import Seismogram
        >>> 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)
        >>> isinstance(seismogram, Seismogram)
        True
        >>> seismogram.sid
        '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.sid
        '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: np.ndarray = field(
        converter=convert_to_ndarray,
        validator=validators.instance_of(np.ndarray),
        on_setattr=setters.pipe(setters.convert, setters.validate),
    )
    """Seismogram data."""

    sid: str = field(
        validator=validators.instance_of(str),
        on_setattr=setters.validate,
    )
    """FDSN source identifier of the parsed GeoCSV data (e.g. `IU_ANMO_00_LHZ`).

    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,
            delta=pd.Timedelta(seconds=1.0 / segment.sample_rate_hz),
            data=segment.data,
            sid=segment.sid,
        )

    @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.web.fetch_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:
            ```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"),
            ... )  # doctest: +SKIP
            >>>
            ```
        """
        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(
                f"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) -> 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.sid == seismogram.sid
            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).

sid class-attribute instance-attribute

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

FDSN source identifier of the parsed GeoCSV data (e.g. IU_ANMO_00_LHZ).

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.web.fetch_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"),
... )  # doctest: +SKIP
>>>
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.web.fetch_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:
        ```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"),
        ... )  # doctest: +SKIP
        >>>
        ```
    """
    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(
            f"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,
        delta=pd.Timedelta(seconds=1.0 / segment.sample_rate_hz),
        data=segment.data,
        sid=segment.sid,
    )

write

write(path: str | PathLike) -> 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

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.sid == seismogram.sid
True
>>>
Source code in src/pysmo/classes/_geocsv.py
def write(self, path: str | PathLike) -> 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.sid == seismogram.sid
        True
        >>>
        ```
    """
    write_geocsv(self, path)

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:

>>> # Import the Seismogram type to check if the nested class is compatible:
>>> from pysmo import Seismogram
>>>
>>> # First verify that a SAC instance is not a pysmo Seismogram:
>>> isinstance(sac, Seismogram)
False
>>> # The sac.seismogram object is, however:
>>> isinstance(sac.seismogram, Seismogram)
True
>>>

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
>>>
Tip

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_buffer

Create a new SAC instance from a SAC data buffer.

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_buffer

Read data and headers from a SAC byte buffer 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
        >>> # Import the Seismogram type to check if the nested class is compatible:
        >>> from pysmo import Seismogram
        >>>
        >>> # First verify that a SAC instance is not a pysmo Seismogram:
        >>> isinstance(sac, Seismogram)
        False
        >>> # The sac.seismogram object is, however:
        >>> isinstance(sac.seismogram, Seismogram)
        True
        >>>
        ```

        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:
        [`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_buffer`][pysmo.classes.SAC.read_buffer],
    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) -> Self:
        """Create a new SAC instance from a SAC file.

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

        Returns:
            A new SAC instance.
        """
        return cls(native=SacIO.from_file(filename))

    @classmethod
    def from_buffer(cls, buffer: bytes) -> Self:
        """Create a new SAC instance from a SAC data buffer.

        Args:
            buffer: Buffer containing SAC file content.

        Returns:
            A new SAC instance.
        """
        return cls(native=SacIO.from_buffer(buffer))

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

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

    def read_buffer(self, buffer: bytes) -> None:
        """Read data and headers from a SAC byte buffer into an existing SAC instance.

        Args:
            buffer: Buffer containing SAC file content.
        """
        self.native.read_buffer(buffer)

    def write(self, filename: str | PathLike) -> 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; "
            f"SAC.from_zip() requires exactly one continuous segment. "
            f"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.web.fetch_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:
            ```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"),
            ... )  # doctest: +SKIP
            >>>
            ```
        """
        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(
                f"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_buffer(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_buffer, 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_buffer(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.web.fetch_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"),
... )  # doctest: +SKIP
>>>
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.web.fetch_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:
        ```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"),
        ... )  # doctest: +SKIP
        >>>
        ```
    """
    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(
            f"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_buffer classmethod

from_buffer(buffer: bytes) -> Self

Create a new SAC instance from a SAC data buffer.

Parameters:

Name Type Description Default
buffer bytes

Buffer containing SAC file content.

required

Returns:

Type Description
Self

A new SAC instance.

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

    Args:
        buffer: Buffer containing SAC file content.

    Returns:
        A new SAC instance.
    """
    return cls(native=SacIO.from_buffer(buffer))

from_file classmethod

from_file(filename: str | PathLike) -> Self

Create a new SAC instance from a SAC file.

Parameters:

Name Type Description Default
filename str | PathLike

Name of the SAC file to read.

required

Returns:

Type Description
Self

A new SAC instance.

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

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

    Returns:
        A new SAC instance.
    """
    return cls(native=SacIO.from_file(filename))

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; "
        f"SAC.from_zip() requires exactly one continuous segment. "
        f"Use SAC.all_from_zip() to get all segments instead. "
        f"Segments found:\n{segment_lines}"
    )

read

read(filename: str | PathLike) -> None

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

Parameters:

Name Type Description Default
filename str | PathLike

Name of the SAC file to read.

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

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

read_buffer

read_buffer(buffer: bytes) -> None

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

Parameters:

Name Type Description Default
buffer bytes

Buffer containing SAC file content.

required
Source code in src/pysmo/classes/_sac.py
def read_buffer(self, buffer: bytes) -> None:
    """Read data and headers from a SAC byte buffer into an existing SAC instance.

    Args:
        buffer: Buffer containing SAC file content.
    """
    self.native.read_buffer(buffer)

write

write(filename: str | PathLike) -> None

Write data and header values to a SAC file.

Parameters:

Name Type Description Default
filename str | PathLike

Name of the SAC file to write to.

required
Source code in src/pysmo/classes/_sac.py
def write(self, filename: str | PathLike) -> 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:

Checking if a SacEvent matches the pysmo Event type:

>>> from pysmo.classes import SAC
>>> from pysmo import Event
>>> sac = SAC.from_file("example.sac")
>>> isinstance(sac.event, Event)
True
>>>
Note

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:
        Checking if a SacEvent matches the pysmo
        [`Event`][pysmo.Event] type:

        ```python
        >>> from pysmo.classes import SAC
        >>> from pysmo import Event
        >>> sac = SAC.from_file("example.sac")
        >>> isinstance(sac.event, Event)
        True
        >>>
        ```

    Note:
        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:
            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).

Important

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/export class for SAC PZ (pole-zero) files.

Reads an analog instrument response from a SAC PZ file (as produced by e.g. the EarthScope SACPZ web 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.

To serialise an instance back to a SAC PZ file call write, or use pysmo.lib.io.write_sacpz for multi-record output.

Examples:

>>> from pysmo import Response
>>> 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)
>>> isinstance(response, Response)
True
>>> response.network, response.station
('IU', 'ANMO')
>>> from pathlib import Path
>>> response.write("out.pz"); recovered = SacPZ.from_text(Path("out.pz").read_text())
>>> recovered.network, recovered.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 from the EarthScope SACPZ web service, selecting one epoch.

from_text

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

write

Write this response to a SAC PZ file.

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/export 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. the EarthScope SACPZ web 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.

    To serialise an instance back to a SAC PZ file call
    [`write`][pysmo.classes.SacPZ.write], or use [`pysmo.lib.io.write_sacpz`][] for
    multi-record output.

    Examples:
        ```python
        >>> from pysmo import Response
        >>> 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)
        >>> isinstance(response, Response)
        True
        >>> response.network, response.station
        ('IU', 'ANMO')
        >>> from pathlib import Path
        >>> response.write("out.pz"); recovered = SacPZ.from_text(Path("out.pz").read_text())
        >>> recovered.network, recovered.station
        ('IU', 'ANMO')
        >>>
        ```
    """

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

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

    zeros: list[complex] = field()
    """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()
    """Start of the epoch this response applies to."""

    end_date: pd.Timestamp | None = field(default=None)
    """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 import Response
            >>> from pysmo.classes import SacPZ
            >>> text = Path("SACPZ.IU.ANMO.00.BHZ").read_text()
            >>> response = SacPZ.from_text(text)
            >>> isinstance(response, Response)
            True
            >>> 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 from the EarthScope SACPZ web service, selecting one epoch.

        Unlike [`StationXML.fetch`][pysmo.classes.StationXML.fetch], epoch
        selection happens server-side: the SACPZ web service's own `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:
            When fetching live from EarthScope rather than reading an
            existing SAC PZ file, prefer
            [`StationXML.fetch`][pysmo.classes.StationXML.fetch]: the
            StationXML response also captures digital FIR/IIR stages, so it
            always satisfies [`StagedResponse`][pysmo.StagedResponse], unlike
            `SacPZ`.

        Examples:
            ```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)  # doctest: +SKIP
            >>>
            ```
        """
        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,
        )

    def write(self, path: str | PathLike) -> None:
        """Write this response to a SAC PZ file.

        Serialises the instance as a single SAC PZ record using the header
        convention produced by the EarthScope SACPZ web service. If
        [`reference_sensitivity`][pysmo.classes.SacPZ.reference_sensitivity]
        is `None` the `* SENSITIVITY` header line is omitted (the file
        remains valid; `CONSTANT` carries the full system gain). To write
        several responses into one concatenated file use
        [`pysmo.lib.io.write_sacpz`][] directly.

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

        Examples:
            ```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.write("out.pz"); recovered = SacPZ.from_text(Path("out.pz").read_text())
            >>> recovered.network == response.network
            True
            >>>
            ```
        """
        write_sacpz(self, path)

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)

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()

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()

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()

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 from the EarthScope SACPZ web service, selecting one epoch.

Unlike StationXML.fetch, epoch selection happens server-side: the SACPZ web service's own 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.

Tip

When fetching live from EarthScope rather than reading an existing SAC PZ file, prefer StationXML.fetch: the StationXML response 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)  # doctest: +SKIP
>>>
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 from the EarthScope SACPZ web service, selecting one epoch.

    Unlike [`StationXML.fetch`][pysmo.classes.StationXML.fetch], epoch
    selection happens server-side: the SACPZ web service's own `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:
        When fetching live from EarthScope rather than reading an
        existing SAC PZ file, prefer
        [`StationXML.fetch`][pysmo.classes.StationXML.fetch]: the
        StationXML response also captures digital FIR/IIR stages, so it
        always satisfies [`StagedResponse`][pysmo.StagedResponse], unlike
        `SacPZ`.

    Examples:
        ```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)  # doctest: +SKIP
        >>>
        ```
    """
    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 import Response
>>> from pysmo.classes import SacPZ
>>> text = Path("SACPZ.IU.ANMO.00.BHZ").read_text()
>>> response = SacPZ.from_text(text)
>>> isinstance(response, Response)
True
>>> 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 import Response
        >>> from pysmo.classes import SacPZ
        >>> text = Path("SACPZ.IU.ANMO.00.BHZ").read_text()
        >>> response = SacPZ.from_text(text)
        >>> isinstance(response, Response)
        True
        >>> 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])

write

write(path: str | PathLike) -> None

Write this response to a SAC PZ file.

Serialises the instance as a single SAC PZ record using the header convention produced by the EarthScope SACPZ web service. If reference_sensitivity is None the * SENSITIVITY header line is omitted (the file remains valid; CONSTANT carries the full system gain). To write several responses into one concatenated file use pysmo.lib.io.write_sacpz directly.

Parameters:

Name Type Description Default
path str | PathLike

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

required

Examples:

>>> from pathlib import Path
>>> from pysmo.classes import SacPZ
>>> text = Path("SACPZ.IU.ANMO.00.BHZ").read_text()
>>> response = SacPZ.from_text(text)
>>> response.write("out.pz"); recovered = SacPZ.from_text(Path("out.pz").read_text())
>>> recovered.network == response.network
True
>>>
Source code in src/pysmo/classes/_sacpz.py
def write(self, path: str | PathLike) -> None:
    """Write this response to a SAC PZ file.

    Serialises the instance as a single SAC PZ record using the header
    convention produced by the EarthScope SACPZ web service. If
    [`reference_sensitivity`][pysmo.classes.SacPZ.reference_sensitivity]
    is `None` the `* SENSITIVITY` header line is omitted (the file
    remains valid; `CONSTANT` carries the full system gain). To write
    several responses into one concatenated file use
    [`pysmo.lib.io.write_sacpz`][] directly.

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

    Examples:
        ```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.write("out.pz"); recovered = SacPZ.from_text(Path("out.pz").read_text())
        >>> recovered.network == response.network
        True
        >>>
        ```
    """
    write_sacpz(self, path)

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:

Checking if a SacSeismogram matches the pysmo Seismogram type:

>>> from pysmo import Seismogram
>>> from pysmo.classes import SAC
>>> sac = SAC.from_file("example.sac")
>>> isinstance(sac.seismogram, Seismogram)
True
>>>

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

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:
        Checking if a SacSeismogram matches the pysmo
        [`Seismogram`][pysmo.Seismogram] type:

        ```python
        >>> from pysmo import Seismogram
        >>> from pysmo.classes import SAC
        >>> sac = SAC.from_file("example.sac")
        >>> isinstance(sac.seismogram, Seismogram)
        True
        >>>
        ```

        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) -> np.ndarray:
        """Seismogram data."""

        return self._parent.data

    @data.setter
    def data(self, value: np.ndarray) -> 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:
        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

data: ndarray

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:

Checking if a SacStation matches the pysmo Station type:

>>> from pysmo.classes import SAC
>>> from pysmo import Station
>>> sac = SAC.from_file("example.sac")
>>> isinstance(sac.station, Station)
True
>>>

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:
        Checking if a SacStation matches the pysmo
        [`Station`][pysmo.Station] type:

        ```python
        >>> from pysmo.classes import SAC
        >>> from pysmo import Station
        >>> sac = SAC.from_file("example.sac")
        >>> isinstance(sac.station, Station)
        True
        >>>
        ```
    """

    @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 response metadata.

Reads an instrument response from a FDSN StationXML document (as returned by e.g. the EarthScope station web service with level=response) and exposes it as a Response-compatible object. Unlike SacPZ, StationXML always satisfies StagedResponse too — stages is simply empty if the document has no digital FIR/IIR decimation stages.

A StationXML document commonly covers a channel's full instrument history, i.e. several response epochs (e.g. after a sensor swap). from_bytes narrows this to a single epoch (matching a given time, or the currently-open one); all_from_bytes returns every epoch found, for callers who want to do their own selection.

Examples:

>>> from pysmo import Response, StagedResponse
>>> 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">
...       <Channel code="BHZ" locationCode="00"
...                startDate="2018-07-09T20:45:00.0000">
...         <Response>
...           <InstrumentSensitivity>
...             <Value>1.98475E9</Value>
...             <Frequency>0.02</Frequency>
...             <InputUnits><Name>m/s</Name></InputUnits>
...             <OutputUnits><Name>counts</Name></OutputUnits>
...           </InstrumentSensitivity>
...           <Stage number="1">
...             <PolesZeros>
...               <InputUnits><Name>m/s</Name></InputUnits>
...               <OutputUnits><Name>V</Name></OutputUnits>
...               <PzTransferFunctionType>LAPLACE (RADIANS/SECOND)</PzTransferFunctionType>
...               <NormalizationFactor>5.03773E14</NormalizationFactor>
...               <NormalizationFrequency>0.02</NormalizationFrequency>
...               <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>
...             <StageGain><Value>1183.0</Value><Frequency>0.02</Frequency></StageGain>
...           </Stage>
...         </Response>
...       </Channel>
...     </Station>
...   </Network>
... </FDSNStationXML>'''
>>> response = StationXML.from_bytes(xml)
>>> isinstance(response, Response)
True
>>> isinstance(response, StagedResponse)
True
>>> response.network, response.station
('IU', 'ANMO')
>>>

Methods:

Name Description
all_from_bytes

Create one instance per response epoch in a StationXML document.

fetch

Fetch and parse an instrument response from the EarthScope FDSN station web service, selecting one epoch.

from_bytes

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

Attributes:

Name Type Description
channel str

Channel code parsed from the StationXML document.

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.

location str

Location code parsed from the StationXML document.

network str

Network code parsed from the StationXML document.

overall_sensitivity NonZeroNumber

Scale factor combined with poles/zeros to reconstruct H(f)

poles list[complex]

Response poles.

reference_sensitivity NonZeroNumber | None

Total system sensitivity at the reference frequency, A0 excluded

stages list[ResponseStage]

Digital decimation stages, in signal order. Empty if the document has

start_date Timestamp

Start of the epoch this response applies to.

station str

Station code parsed from the StationXML document.

zeros list[complex]

Response zeros.

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

    Reads an instrument response from a
    [FDSN StationXML](http://www.fdsn.org/xml/station/) document (as
    returned by e.g. the EarthScope station web service with
    `level=response`) and exposes it as a
    [`Response`][pysmo.Response]-compatible object. Unlike
    [`SacPZ`][pysmo.classes.SacPZ], `StationXML` always satisfies
    [`StagedResponse`][pysmo.StagedResponse] too — `stages` is simply empty
    if the document has no digital FIR/IIR decimation stages.

    A StationXML document commonly covers a channel's full instrument
    history, i.e. several response epochs (e.g. after a sensor swap).
    [`from_bytes`][pysmo.classes.StationXML.from_bytes] narrows this to a
    single epoch (matching a given time, or the currently-open one);
    [`all_from_bytes`][pysmo.classes.StationXML.all_from_bytes] returns
    every epoch found, for callers who want to do their own selection.

    Examples:
        ```python
        >>> from pysmo import Response, StagedResponse
        >>> 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">
        ...       <Channel code="BHZ" locationCode="00"
        ...                startDate="2018-07-09T20:45:00.0000">
        ...         <Response>
        ...           <InstrumentSensitivity>
        ...             <Value>1.98475E9</Value>
        ...             <Frequency>0.02</Frequency>
        ...             <InputUnits><Name>m/s</Name></InputUnits>
        ...             <OutputUnits><Name>counts</Name></OutputUnits>
        ...           </InstrumentSensitivity>
        ...           <Stage number="1">
        ...             <PolesZeros>
        ...               <InputUnits><Name>m/s</Name></InputUnits>
        ...               <OutputUnits><Name>V</Name></OutputUnits>
        ...               <PzTransferFunctionType>LAPLACE (RADIANS/SECOND)</PzTransferFunctionType>
        ...               <NormalizationFactor>5.03773E14</NormalizationFactor>
        ...               <NormalizationFrequency>0.02</NormalizationFrequency>
        ...               <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>
        ...             <StageGain><Value>1183.0</Value><Frequency>0.02</Frequency></StageGain>
        ...           </Stage>
        ...         </Response>
        ...       </Channel>
        ...     </Station>
        ...   </Network>
        ... </FDSNStationXML>'''
        >>> response = StationXML.from_bytes(xml)
        >>> isinstance(response, Response)
        True
        >>> isinstance(response, StagedResponse)
        True
        >>> response.network, response.station
        ('IU', 'ANMO')
        >>>
        ```
    """

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

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

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

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

    overall_sensitivity: NonZeroNumber = field(
        converter=float, validator=validate_nonzero
    )
    """Scale factor combined with `poles`/`zeros` to reconstruct `H(f)`
    (`NormalizationFactor * InstrumentSensitivity`).

    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
    (StationXML's `InstrumentSensitivity/Value`).

    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.

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

    stages: list[ResponseStage] = field(factory=list)
    """Digital decimation stages, in signal order. Empty if the document has
    no digital stages.

    See [`StagedResponse.stages`][pysmo.StagedResponse.stages] for more details.
    """

    network: str = field(validator=validators.instance_of(str))
    """Network code parsed from the StationXML document."""

    station: str = field(validator=validators.instance_of(str))
    """Station code parsed from the StationXML document."""

    location: str = field(validator=validators.instance_of(str))
    """Location code parsed from the StationXML document."""

    channel: str = field(validator=validators.instance_of(str))
    """Channel code parsed from the StationXML document."""

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

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

    @classmethod
    def from_bytes(
        cls,
        xml: bytes,
        *,
        time: pd.Timestamp | 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
        station-level query (or one saved for later, offline use) commonly
        returns every location/channel combination on record, each with its
        own epoch history. `location`/`channel` narrow to one before `time`
        is applied; without them, a multi-channel document raises the same
        "more than one epoch" error as an ambiguous *time*.

        Args:
            xml: Raw StationXML document bytes (as returned by the FDSN
                station web service with `level=response`).
            time: Timestamp used to select the response epoch. If `None`,
                the currently-open epoch (no end date) is selected.
            location: Location code to narrow to, if `xml` covers more than
                one location. If `None`, location is not filtered.
            channel: Channel code to narrow to, if `xml` covers more than
                one channel. If `None`, channel is not filtered.

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

        Raises:
            ValueError: If, after narrowing by `location`/`channel`, zero or
                more than one response 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.
        """
        epochs = parse_stationxml(xml)
        matches = _matching_epochs(epochs, time, location=location, channel=channel)
        if len(matches) != 1:
            raise ValueError(
                f"Expected exactly one response epoch in the given "
                f"StationXML at "
                f"{'the currently open epoch' if time is None else time}"
                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 fetch(cls, *, station: Station, time: pd.Timestamp | None = None) -> Self:
        """Fetch and parse an instrument response from the EarthScope FDSN station web service, selecting one epoch.

        A channel's instrument response usually has several epochs (e.g.
        after a sensor swap), so the request is narrowed to a single one:
        the epoch active at *time* if given, otherwise the one currently
        open (no `endDate`). Fetches the full response history in one
        request and narrows client-side (like
        [`from_bytes`][pysmo.classes.StationXML.from_bytes]); to fetch once
        and interpret later (e.g. offline, or without repeating the network
        request), use [`pysmo.tools.web.fetch_stationxml`][] and
        [`from_bytes`][pysmo.classes.StationXML.from_bytes] /
        [`all_from_bytes`][pysmo.classes.StationXML.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.
            time: Timestamp used to select the response epoch. If `None`,
                the currently-open epoch (no end date) is selected.

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

        Raises:
            ValueError: If zero or more than one response epoch matches
                *time* (or "currently open", if `time` is `None`).
            urllib3.exceptions.ResponseError: If the station web service
                returns an HTTP error.

        Examples:
            ```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,
            ... )
            >>> response = StationXML.fetch(station=station)  # doctest: +SKIP
            >>>
            ```
        """
        xml = fetch_stationxml(station=station)
        return cls.from_bytes(xml, time=time)

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

        Unlike [`from_bytes`][pysmo.classes.StationXML.from_bytes], this
        does not narrow to a single epoch — a document covering a channel's
        full instrument history returns several, each with its own
        `network`/`station`/`location`/`channel`/`start_date`/`end_date`
        provenance, which callers can filter themselves.

        Args:
            xml: Raw StationXML document bytes.

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

    @classmethod
    def _from_raw(cls, raw: _RawResponse) -> Self:
        return cls(
            poles=raw.poles,
            zeros=raw.zeros,
            overall_sensitivity=raw.normalization_factor * raw.sensitivity_value,
            reference_sensitivity=raw.sensitivity_value,
            input_units=raw.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.digital_stages
            ],
            network=raw.network,
            station=raw.station,
            location=raw.location,
            channel=raw.channel,
            start_date=raw.start_date,
            end_date=raw.end_date,
        )

channel class-attribute instance-attribute

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

Channel code parsed from the StationXML document.

end_date class-attribute instance-attribute

end_date: Timestamp | None = field(default=None)

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.

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 StationXML document.

network class-attribute instance-attribute

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

Network code parsed from the StationXML document.

overall_sensitivity class-attribute instance-attribute

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

Scale factor combined with poles/zeros to reconstruct H(f) (NormalizationFactor * InstrumentSensitivity).

See Response.overall_sensitivity for more details.

poles class-attribute instance-attribute

poles: list[complex] = field()

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 (StationXML's InstrumentSensitivity/Value).

See Response.reference_sensitivity for more details.

stages class-attribute instance-attribute

stages: list[ResponseStage] = field(factory=list)

Digital decimation stages, in signal order. Empty if the document has no digital stages.

See StagedResponse.stages for more details.

start_date class-attribute instance-attribute

start_date: Timestamp = field()

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 StationXML document.

zeros class-attribute instance-attribute

zeros: list[complex] = field()

Response zeros.

See Response.zeros for more details.

all_from_bytes classmethod

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

Create one instance per response epoch in a StationXML document.

Unlike from_bytes, this does not narrow to a single epoch — a document covering a channel's full instrument history returns several, each with its own network/station/location/channel/start_date/end_date provenance, which callers can filter themselves.

Parameters:

Name Type Description Default
xml bytes

Raw StationXML document bytes.

required

Returns:

Type Description
list[Self]

One StationXML instance per response epoch found, in document

list[Self]

order.

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

    Unlike [`from_bytes`][pysmo.classes.StationXML.from_bytes], this
    does not narrow to a single epoch — a document covering a channel's
    full instrument history returns several, each with its own
    `network`/`station`/`location`/`channel`/`start_date`/`end_date`
    provenance, which callers can filter themselves.

    Args:
        xml: Raw StationXML document bytes.

    Returns:
        One StationXML instance per response 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 and parse an instrument response from the EarthScope FDSN station web service, selecting one epoch.

A channel's instrument response usually has several epochs (e.g. after a sensor swap), so the request is narrowed to a single one: the epoch active at time if given, otherwise the one currently open (no endDate). Fetches the full response history in one request and narrows client-side (like from_bytes); to fetch once and interpret later (e.g. offline, or without repeating the network request), use pysmo.tools.web.fetch_stationxml 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
time Timestamp | None

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

None

Returns:

Type Description
Self

A new StationXML instance for the response epoch active at

Self

time (or currently open, if time is None).

Raises:

Type Description
ValueError

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

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,
... )
>>> response = StationXML.fetch(station=station)  # doctest: +SKIP
>>>
Source code in src/pysmo/classes/_stationxml.py
@classmethod
def fetch(cls, *, station: Station, time: pd.Timestamp | None = None) -> Self:
    """Fetch and parse an instrument response from the EarthScope FDSN station web service, selecting one epoch.

    A channel's instrument response usually has several epochs (e.g.
    after a sensor swap), so the request is narrowed to a single one:
    the epoch active at *time* if given, otherwise the one currently
    open (no `endDate`). Fetches the full response history in one
    request and narrows client-side (like
    [`from_bytes`][pysmo.classes.StationXML.from_bytes]); to fetch once
    and interpret later (e.g. offline, or without repeating the network
    request), use [`pysmo.tools.web.fetch_stationxml`][] and
    [`from_bytes`][pysmo.classes.StationXML.from_bytes] /
    [`all_from_bytes`][pysmo.classes.StationXML.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.
        time: Timestamp used to select the response epoch. If `None`,
            the currently-open epoch (no end date) is selected.

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

    Raises:
        ValueError: If zero or more than one response epoch matches
            *time* (or "currently open", if `time` is `None`).
        urllib3.exceptions.ResponseError: If the station web service
            returns an HTTP error.

    Examples:
        ```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,
        ... )
        >>> response = StationXML.fetch(station=station)  # doctest: +SKIP
        >>>
        ```
    """
    xml = fetch_stationxml(station=station)
    return cls.from_bytes(xml, time=time)

from_bytes classmethod

from_bytes(
    xml: bytes,
    *,
    time: Timestamp | 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 station-level query (or one saved for later, offline use) commonly returns every location/channel combination on record, each with its own epoch history. location/channel narrow to one before time is applied; without them, a multi-channel document raises the same "more than one epoch" error as an ambiguous time.

Parameters:

Name Type Description Default
xml bytes

Raw StationXML document bytes (as returned by the FDSN station web service with level=response).

required
time Timestamp | None

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

None
location str | None

Location code to narrow to, if xml covers more than one location. If None, location is not filtered.

None
channel str | None

Channel code to narrow to, if xml covers more than one channel. If None, channel is not filtered.

None

Returns:

Type Description
Self

A new StationXML instance for the response epoch active at

Self

time (or currently open, if time is None).

Raises:

Type Description
ValueError

If, after narrowing by location/channel, zero or more than one response 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,
    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
    station-level query (or one saved for later, offline use) commonly
    returns every location/channel combination on record, each with its
    own epoch history. `location`/`channel` narrow to one before `time`
    is applied; without them, a multi-channel document raises the same
    "more than one epoch" error as an ambiguous *time*.

    Args:
        xml: Raw StationXML document bytes (as returned by the FDSN
            station web service with `level=response`).
        time: Timestamp used to select the response epoch. If `None`,
            the currently-open epoch (no end date) is selected.
        location: Location code to narrow to, if `xml` covers more than
            one location. If `None`, location is not filtered.
        channel: Channel code to narrow to, if `xml` covers more than
            one channel. If `None`, channel is not filtered.

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

    Raises:
        ValueError: If, after narrowing by `location`/`channel`, zero or
            more than one response 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.
    """
    epochs = parse_stationxml(xml)
    matches = _matching_epochs(epochs, time, location=location, channel=channel)
    if len(matches) != 1:
        raise ValueError(
            f"Expected exactly one response epoch in the given "
            f"StationXML at "
            f"{'the currently open epoch' if time is None else time}"
            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])