Skip to content

pysmo.lib

Internal utilities, validators, defaults, and I/O used by pysmo.

Modules:

Name Description
decorators

Decorators for pysmo.

defaults

Defaults for pysmo functions/classes.

io

Low-level I/O classes for reading and writing seismological file formats.

mini_utils

Mini utils.

validators

Validators for pysmo classes using attrs.

decorators

Decorators for pysmo.

Functions:

Name Description
add_doc

Decorator to add a docstring to a function via decorator.

value_not_none

Decorator to ensure the value in Class properties is not None.

add_doc

add_doc(docstring: str) -> Callable

Decorator to add a docstring to a function via decorator.

Useful to use e.g. f-strings in the docstring.

Parameters:

Name Type Description Default
docstring str

The docstring to add.

required

Returns:

Type Description
Callable

Function with docstring applied.

Source code in src/pysmo/lib/decorators.py
def add_doc(docstring: str) -> Callable:
    """Decorator to add a docstring to a function via decorator.

    Useful to use e.g. f-strings in the docstring.

    Args:
        docstring: The docstring to add.

    Returns:
        Function with docstring applied.
    """

    def decorator(function: Callable) -> Callable:
        function.__doc__ = docstring
        return function

    return decorator

value_not_none

value_not_none(
    function: Callable[..., Any],
) -> Callable[..., Any]

Decorator to ensure the value in Class properties is not None.

Parameters:

Name Type Description Default
function Callable[..., Any]

The function to decorate.

required

Returns:

Type Description
Callable[..., Any]

Function with value not None check applied.

Source code in src/pysmo/lib/decorators.py
def value_not_none(function: Callable[..., Any]) -> Callable[..., Any]:
    """Decorator to ensure the value in Class properties is not None.

    Args:
        function: The function to decorate.

    Returns:
        Function with value not None check applied.
    """

    @wraps(function)
    def decorator(*args: Any, **kwargs: Any) -> Any:
        instance, value, *_ = args
        if value is None:
            raise TypeError(
                f"{instance.__class__.__name__}.{function.__name__} may not be of None type."
            )
        return function(*args, **kwargs)

    return decorator

defaults

Defaults for pysmo functions/classes.

Classes:

Name Description
SEISMOGRAM_DEFAULTS

Defaults for classes related to Seismogram.

SEISMOGRAM_DEFAULTS

Bases: Enum

Defaults for classes related to Seismogram.

Attributes:

Name Type Description
begin_time

Seismogram begin time.

delta

Sampling interval.

Source code in src/pysmo/lib/defaults.py
class SEISMOGRAM_DEFAULTS(Enum):
    """Defaults for classes related to [`Seismogram`][pysmo.Seismogram]."""

    begin_time = Timestamp.fromtimestamp(0, tz=timezone.utc)
    "Seismogram begin time."
    delta = Timedelta(seconds=1)
    "Sampling interval."

begin_time class-attribute instance-attribute

begin_time = fromtimestamp(0, tz=utc)

Seismogram begin time.

delta class-attribute instance-attribute

delta = Timedelta(seconds=1)

Sampling interval.

io

Low-level I/O classes for reading and writing seismological file formats.

Classes in this module handle file format details but do not implement pysmo protocol types directly. They serve as the foundation for the higher-level classes in pysmo.classes and should generally not be used directly.

Classes:

Name Description
SacIO

Access SAC files in Python.

SacIO

Bases: SacIOBase

Access SAC files in Python.

The SacIO class reads and writes data and header values to and from a SAC file. Instances of SacIO provide attributes named identially to header names in the SAC file format. Additonal attributes may be set, but are not written to a SAC file (because there is no space reserved for them there). Class attributes with corresponding header fields in a SAC file (for example the begin time b) are checked for a valid format before being saved in the SacIO instance.

Warning

This class should typically never be used directly. Instead please use the SAC class, which inherits all attributes and methods from here.

Examples:

Create a new instance from a file and print seismogram data:

>>> from pysmo.lib.io import SacIO
>>> sac = SacIO.from_file("example.sac")
>>> data = sac.data
>>> data
array([2302., 2313., 2345., ..., 2836., 2772., 2723.], shape=(180000,))
>>>

Read the sampling rate:

>>> delta = sac.delta
>>> delta
0.019999999552965164
>>>

Change the sampling rate:

>>> newdelta = 0.05
>>> sac.delta = newdelta
>>> sac.delta
0.05
>>>

Create a new instance from IRIS services:

>>> from pysmo.lib.io import SacIO
>>> sac = SacIO.from_iris(net="C1",
... sta="VA01",
... cha="BHZ",
... loc="--",
... start="2021-03-22T13:00:00",
... duration=1 * 60 * 60,
... scale="AUTO",
... demean="true",
... force_single_result=True)
>>> sac.npts
144001
>>>

For each SAC(file) header field there is a corresponding attribute in this class. There are a lot of header fields in a SAC file, which are all called the same way when using SacIO.

Methods:

Name Description
change_all_times

Change all time headers by the same amount.

from_buffer

Create a new SAC instance from a SAC data buffer.

from_file

Create a new SAC instance from a SAC file.

from_iris

Create a list of SAC instances from a single IRIS

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

Writes data and header values to a SAC file.

Attributes:

Name Type Description
a float | None

First arrival time (seconds relative to reference time).

az float

Event to station azimuth (degrees).

b float

Beginning value of the independent variable.

baz float

Station to event azimuth (degrees).

cmpaz float | None

Component azimuth (degrees clockwise from north).

cmpinc float | None

Component incident angle (degrees from upward vertical; SEED/MINISEED uses dip: degrees from horizontal down).

data ndarray

Seismogram data.

delta float

Increment between evenly spaced samples (nominal value).

depmax float | None

Maximum value of dependent variable.

depmen float | None

Mean value of dependent variable.

depmin float | None

Minimum value of dependent variable.

dist float

Station to event distance (km).

e float

Ending value of the independent variable.

evdp float | None

Event depth below surface (kilometers -- previously meters).

evel float | None

Event elevation (meters).

evla float | None

Event latitude (degrees, north positive).

evlo float | None

Event longitude (degrees, east positive).

f float | None

Fini or end of event time (seconds relative to reference time).

gcarc float

Station to event great circle arc length (degrees).

ibody str | None

Body / Spheroid definition used in Distance Calculations.

idep str

Type of dependent variable.

ievreg str | None

Event geographic region.

ievtyp str

Type of event.

iftype str

Type of file.

iinst str | None

Type of recording instrument.

imagsrc str | None

Source of magnitude information.

imagtyp str | None

Magnitude type.

iqual str | None

Quality of data.

istreg str | None

Station geographic region.

isynth str | None

Synthetic data flag.

iztype str

Reference time equivalence.

ka str | None

First arrival time identification.

kcmpnm str | None

Channel name. SEED volumes use three character names, and the third is the component/orientation. For horizontals, the current trend is to use 1 and 2 instead of N and E.

kdatrd str | None

Date data was read onto computer.

kevnm str | None

Event name.

kf str | None

Fini identification.

khole str | None

Nuclear: hole identifier; Other: location identifier (LOCID).

kinst str | None

Generic name of recording instrument.

knetwk str | None

Name of seismic network.

ko str | None

Event origin time identification.

kstnm str | None

Station name.

kt0 str | None

User defined time pick identification.

kt1 str | None

User defined time pick identification.

kt2 str | None

User defined time pick identification.

kt3 str | None

User defined time pick identification.

kt4 str | None

User defined time pick identification.

kt5 str | None

User defined time pick identification.

kt6 str | None

User defined time pick identification.

kt7 str | None

User defined time pick identification.

kt8 str | None

User defined time pick identification.

kt9 str | None

User defined time pick identification.

kuser0 str | None

User defined variable storage area.

kuser1 str | None

User defined variable storage area.

kuser2 str | None

User defined variable storage area.

kzdate str | None

ISO 8601 format of GMT reference date.

kztime str | None

Alphanumeric form of GMT reference time.

lcalda Literal[True]

TRUE if DIST, AZ, BAZ, and GCARC are to be calculated from station and event coordinates.

leven bool

TRUE if data is evenly spaced.

lovrok bool | None

TRUE if it is okay to overwrite this file on disk.

lpspol bool | None

TRUE if station components have a positive polarity (left-hand rule).

mag float | None

Event magnitude.

nevid int | None

Event ID (CSS 3.0).

norid int | None

Origin ID (CSS 3.0).

npts int

Number of points per data component.

nvhdr int

Header version number.

nwfid int | None

Waveform ID (CSS 3.0).

nxsize int | None

Spectral Length (Spectral files only).

nysize int | None

Spectral Width (Spectral files only).

nzhour int | None

GMT hour.

nzjday int | None

GMT julian day.

nzmin int | None

GMT minute.

nzmsec int | None

GMT millisecond.

nzsec int | None

GMT second.

nzyear int | None

GMT year corresponding to reference (zero) time in file.

o float | None

Event origin time (seconds relative to reference time).

odelta float | None

Observed increment if different from nominal value.

ref_datetime datetime | None

Return Python datetime object of GMT reference time and date.

resp0 float | None

Instrument response parameter 0 (not currently used).

resp1 float | None

Instrument response parameter 1 (not currently used).

resp2 float | None

Instrument response parameter 2 (not currently used).

resp3 float | None

Instrument response parameter 3 (not currently used).

resp4 float | None

Instrument response parameter 4 (not currently used).

resp5 float | None

Instrument response parameter 5 (not currently used).

resp6 float | None

Instrument response parameter 6 (not currently used).

resp7 float | None

Instrument response parameter 7 (not currently used).

resp8 float | None

Instrument response parameter 8 (not currently used).

resp9 float | None

Instrument response parameter 9 (not currently used).

stdp float | None

Station depth below surface (meters).

stel float | None

Station elevation above sea level (meters).

stla float | None

Station latitude (degrees, north positive).

stlo float | None

Station longitude (degrees, east positive).

t0 float | None

User defined time pick or marker 0 (seconds relative to reference time).

t1 float | None

User defined time pick or marker 1 (seconds relative to reference time).

t2 float | None

User defined time pick or marker 2 (seconds relative to reference time).

t3 float | None

User defined time pick or marker 3 (seconds relative to reference time).

t4 float | None

User defined time pick or marker 4 (seconds relative to reference time).

t5 float | None

User defined time pick or marker 5 (seconds relative to reference time).

t6 float | None

User defined time pick or marker 6 (seconds relative to reference time).

t7 float | None

User defined time pick or marker 7 (seconds relative to reference time).

t8 float | None

User defined time pick or marker 8 (seconds relative to reference time).

t9 float | None

User defined time pick or marker 9 (seconds relative to reference time).

user0 float | None

User defined variable storage area.

user1 float | None

User defined variable storage area.

user2 float | None

User defined variable storage area.

user3 float | None

User defined variable storage area.

user4 float | None

User defined variable storage area.

user5 float | None

User defined variable storage area.

user6 float | None

User defined variable storage area.

user7 float | None

User defined variable storage area.

user8 float | None

User defined variable storage area.

user9 float | None

User defined variable storage area.

xmaximum float | None

Maximum value of X (Spectral files only).

xminimum float | None

Minimum value of X (Spectral files only).

ymaximum float | None

Maximum value of Y (Spectral files only).

yminimum float | None

Minimum value of Y (Spectral files only).

Source code in src/pysmo/lib/io/_sacio/sacio.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
@define(kw_only=True)
class SacIO(SacIOBase):
    """
    Access SAC files in Python.

    The `SacIO` class reads and writes data and header values to and from a
    SAC file. Instances of `SacIO` provide attributes named identially to
    header names in the SAC file format. Additonal attributes may be set, but
    are not written to a SAC file (because there is no space reserved for them
    there). Class attributes with corresponding header fields in a SAC file
    (for example the begin time [`b`][pysmo.lib.io.SacIO.b]) are checked for a
    valid format before being saved in the `SacIO` instance.

    Warning:
        This class should typically never be used directly. Instead please
        use the [`SAC`][pysmo.classes.SAC] class, which inherits all attributes
        and methods from here.

    Examples:
        Create a new instance from a file and print seismogram data:

        ```python
        >>> from pysmo.lib.io import SacIO
        >>> sac = SacIO.from_file("example.sac")
        >>> data = sac.data
        >>> data
        array([2302., 2313., 2345., ..., 2836., 2772., 2723.], shape=(180000,))
        >>>
        ```

        Read the sampling rate:

        ```python
        >>> delta = sac.delta
        >>> delta
        0.019999999552965164
        >>>
        ```

        Change the sampling rate:

        ```python
        >>> newdelta = 0.05
        >>> sac.delta = newdelta
        >>> sac.delta
        0.05
        >>>
        ```

        Create a new instance from IRIS services:

        ```python
        >>> from pysmo.lib.io import SacIO
        >>> sac = SacIO.from_iris(net="C1",
        ... sta="VA01",
        ... cha="BHZ",
        ... loc="--",
        ... start="2021-03-22T13:00:00",
        ... duration=1 * 60 * 60,
        ... scale="AUTO",
        ... demean="true",
        ... force_single_result=True)
        >>> sac.npts
        144001
        >>>
        ```

    For each SAC(file) header field there is a corresponding attribute in this
    class. There are a lot of header fields in a SAC file, which are all called
    the same way when using `SacIO`.
    """

    @property
    def depmin(self) -> float | None:
        """Minimum value of dependent variable."""
        if self.npts == 0:
            return None
        return np.min(self.data).item()

    @property
    def depmax(self) -> float | None:
        """Maximum value of dependent variable."""
        if self.npts == 0:
            return None
        return np.max(self.data).item()

    @property
    def depmen(self) -> float | None:
        """Mean value of dependent variable."""
        if self.npts == 0:
            return None
        return np.mean(self.data).item()

    @property
    def e(self) -> float:
        """Ending value of the independent variable."""
        if self.npts == 0:
            return self.b
        return self.b + (self.npts - 1) * self.delta

    @property
    def dist(self) -> float:
        """Station to event distance (km)."""
        if self.stla and self.stlo and self.evla and self.evlo:
            station_location = MiniLocation(latitude=self.stla, longitude=self.stlo)
            event_location = MiniLocation(latitude=self.evla, longitude=self.evlo)
            return (
                distance(location_1=station_location, location_2=event_location) / 1000
            )
        raise TypeError("One or more coordinates are None.")

    @property
    def az(self) -> float:
        """Event to station azimuth (degrees)."""
        if self.stla and self.stlo and self.evla and self.evlo:
            station_location = MiniLocation(latitude=self.stla, longitude=self.stlo)
            event_location = MiniLocation(latitude=self.evla, longitude=self.evlo)
            return azimuth(location_1=station_location, location_2=event_location)
        raise TypeError("One or more coordinates are None.")

    @property
    def baz(self) -> float:
        """Station to event azimuth (degrees)."""
        if self.stla and self.stlo and self.evla and self.evlo:
            station_location = MiniLocation(latitude=self.stla, longitude=self.stlo)
            event_location = MiniLocation(latitude=self.evla, longitude=self.evlo)
            return backazimuth(location_1=station_location, location_2=event_location)
        raise TypeError("One or more coordinates are None.")

    @property
    def gcarc(self) -> float:
        """Station to event great circle arc length (degrees)."""
        if self.stla and self.stlo and self.evla and self.evlo:
            lat1, lon1 = np.deg2rad(self.stla), np.deg2rad(self.stlo)
            lat2, lon2 = np.deg2rad(self.evla), np.deg2rad(self.evlo)
            return np.rad2deg(
                np.arccos(
                    np.sin(lat1) * np.sin(lat2)
                    + np.cos(lat1) * np.cos(lat2) * np.cos(np.abs(lon1 - lon2))
                )
            )
        raise TypeError("One or more coordinates are None.")

    @property
    def xminimum(self) -> float | None:
        """Minimum value of X (Spectral files only)."""
        if self.nxsize == 0 or not self.nxsize:
            return None
        return np.min(self.x).item()

    @property
    def xmaximum(self) -> float | None:
        """Maximum value of X (Spectral files only)."""
        if self.nxsize == 0 or not self.nxsize:
            return None
        return np.max(self.x).item()

    @property
    def yminimum(self) -> float | None:
        """Minimum value of Y (Spectral files only)."""
        if self.nysize == 0 or not self.nysize:
            return None
        return np.min(self.y).item()

    @property
    def ymaximum(self) -> float | None:
        """Maximum value of Y (Spectral files only)."""
        if self.nysize == 0 or not self.nysize:
            return None
        return np.max(self.y).item()

    @property
    def npts(self) -> int:
        """Number of points per data component."""
        return np.size(self.data)

    @property
    def nxsize(self) -> int | None:
        """Spectral Length (Spectral files only)."""
        if np.size(self.x) == 0:
            return None
        return np.size(self.x)

    @property
    def nysize(self) -> int | None:
        """Spectral Width (Spectral files only)."""
        if np.size(self.y) == 0:
            return None
        return np.size(self.y)

    @property
    def lcalda(self) -> Literal[True]:
        """TRUE if DIST, AZ, BAZ, and GCARC are to be calculated from station and event coordinates.

        Note:
            Above fields are all read only properties in this class, so
            they are always calculated.
        """
        return True

    @property
    def ref_datetime(self) -> datetime | None:
        """Return Python datetime object of GMT reference time and date."""
        if (
            self.nzyear is None
            or self.nzjday is None
            or self.nzhour is None
            or self.nzmin is None
            or self.nzsec is None
            or self.nzmsec is None
        ):
            return None
        return datetime(
            year=self.nzyear,
            month=1,
            day=1,
            hour=self.nzhour,
            minute=self.nzmin,
            second=self.nzsec,
            microsecond=self.nzmsec * 1000,
            tzinfo=timezone.utc,
        ) + timedelta(days=self.nzjday - 1)

    @ref_datetime.setter
    def ref_datetime(self, value: datetime) -> None:
        timedelta_for_rounding = timedelta(microseconds=500)
        value += timedelta_for_rounding
        self.nzyear = value.year
        self.nzjday = value.timetuple().tm_yday
        self.nzhour = value.hour
        self.nzmin = value.minute
        self.nzsec = value.second
        self.nzmsec = int(value.microsecond / 1000)

    @property
    def kzdate(self) -> str | None:
        """ISO 8601 format of GMT reference date."""
        if self.ref_datetime is None:
            return None
        return self.ref_datetime.date().isoformat()

    @property
    def kztime(self) -> str | None:
        """Alphanumeric form of GMT reference time."""
        if self.ref_datetime is None:
            return None
        return self.ref_datetime.time().isoformat(timespec="milliseconds")

    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.
        """

        filename = Path(filename).resolve()

        self.read_buffer(filename.read_bytes())

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

        Args:
            filename: Name of the sacfile to write to.
        """
        with open(filename, "wb") as file_handle:
            # loop over all valid header fields and write them to the file
            for header, header_metadata in SAC_HEADERS.items():
                header_type = header_metadata.type
                header_format = header_metadata.format
                start = header_metadata.start
                header_undefined = HEADER_TYPES[header_type].undefined

                value = None
                try:
                    if hasattr(self, header):
                        value = getattr(self, header)
                except TypeError:
                    value = None

                # convert enumerated header to integer if it is not None
                if header_type == "i" and value is not None:
                    value = SAC_ENUMS_DICT[header][value]

                # set None to -12345
                if value is None:
                    value = header_undefined

                # Encode strings to bytes
                if isinstance(value, str):
                    value = value.encode()

                # write to file
                file_handle.seek(start)
                file_handle.write(struct.pack(header_format, value))

            # write data (if npts > 0)
            data_1_start = 632
            data_1_end = data_1_start + self.npts * 4
            file_handle.truncate(data_1_start)
            if self.npts > 0:
                file_handle.seek(data_1_start)
                for x in self.data:
                    file_handle.write(struct.pack("f", x))

            if self.nvhdr == 7:
                for footer, footer_metadata in SAC_FOOTERS.items():
                    undefined = -12345.0
                    start = footer_metadata.start + data_1_end
                    value = None
                    try:
                        if hasattr(self, footer):
                            value = getattr(self, footer)
                    except AttributeError:
                        value = None

                    # set None to -12345
                    if value is None:
                        value = undefined

                    # write to file
                    file_handle.seek(start)
                    file_handle.write(struct.pack("d", value))

    @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 SacIO instance.
        """
        newinstance = cls()
        newinstance.read(filename)
        return newinstance

    @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 SacIO instance.
        """
        newinstance = cls()
        newinstance.read_buffer(buffer)
        return newinstance

    @classmethod
    def from_iris(
        cls,
        net: str,
        sta: str,
        cha: str,
        loc: str,
        force_single_result: bool = False,
        **kwargs: Any,
    ) -> Self | dict[str, Self] | None:
        """Create a list of SAC instances from a single IRIS
        request using the output format as "sac.zip".

        Args:
            net: Network code (e.g. "US")
            sta: Station code (e.g. "BSS")
            cha: Channel code (e.g. "BHZ")
            loc: Location code (e.g. "00")
            force_single_result: If true, the function will return a single SAC
                                object or None if the requests returns nothing.

        Returns:
            A new SacIO instance.
        """
        kwargs["net"] = net
        kwargs["sta"] = sta
        kwargs["cha"] = cha
        kwargs["loc"] = loc
        kwargs["output"] = "sac.zip"

        if isinstance(kwargs["start"], datetime):
            kwargs["start"] = kwargs["start"].isoformat()

        end = kwargs.get("end", None)
        if end is not None and isinstance(end, datetime):
            kwargs["end"] = end.isoformat()

        transport = httpx.HTTPTransport(retries=3)
        client = httpx.Client(transport=transport)
        for attempt in range(SACIO_DEFAULTS.iris_request_retries):
            response = client.get(
                SACIO_DEFAULTS.iris_base_url,
                params=kwargs,
                follow_redirects=False,
                timeout=SACIO_DEFAULTS.iris_timeout_seconds,
            )
            if (
                response.status_code == 500
                and attempt < SACIO_DEFAULTS.iris_request_retries - 1
            ):
                _time.sleep(SACIO_DEFAULTS.iris_retry_delay_seconds)
                continue
            response.raise_for_status()
            break

        zip = ZipFile(BytesIO(response.content))

        result = {}
        for name in zip.namelist():
            buffer = zip.read(name)
            sac = cls.from_buffer(buffer)
            if force_single_result:
                return sac
            result[name] = sac
        return None if force_single_result else result

    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.
        """

        if len(buffer) < 632:
            raise EOFError()

        # Guess the file endianness first using the unused12 header field.
        # It is located at position 276 and its value should be -12345.0.
        # Try reading with little endianness
        if struct.unpack("<f", buffer[276:280])[-1] == -12345.0:
            file_byteorder = "<"
        # otherwise assume big endianness.
        else:
            file_byteorder = ">"

        # Loop over all header fields and store them in the SAC object under their
        # respective private names.
        npts = 0
        for header, header_metadata in SAC_HEADERS.items():
            header_type = header_metadata.type
            header_required = header_metadata.required
            header_undefined = HEADER_TYPES[header_type].undefined
            start = header_metadata.start
            length = header_metadata.length
            end = start + length
            if end >= len(buffer):
                continue
            content = buffer[start:end]
            value = struct.unpack(file_byteorder + header_metadata.format, content)[0]
            if isinstance(value, bytes):
                # strip spaces and "\x00" chars
                value = value.decode().rstrip(" \x00")

            # npts is read only property in this class, but is needed for reading data
            if header == "npts":
                npts = int(value)

            # raise error if header is undefined AND required
            if value == header_undefined and header_required:
                raise RuntimeError(
                    f"Required {header=} is undefined - invalid SAC file!"
                )

            # skip if undefined (value == -12345...) and not required
            if value == header_undefined and not header_required:
                continue

            # convert enumerated header to string and format others
            if header_type == "i":
                value = SAC_ENUMS_DICT[header](value).name

            # SAC file has headers fields which are read only attributes in this
            # class. We skip them with this try/except.
            # TODO: This is a bit crude, should maybe be a bit more specific.
            try:
                setattr(self, header, value)
            except AttributeError as e:
                if "object has no setter" in str(e):
                    pass

        # Only accept IFTYPE = ITIME SAC files. Other IFTYPE use two data blocks,
        # which is something we don't support for now.
        if self.iftype.lower() != "time":
            raise NotImplementedError(
                f"Reading SAC files with IFTYPE=(I){self.iftype.upper()} is not supported."  # noqa: E501
            )

        # Read first data block
        start = 632
        length = npts * 4
        data_end = start + length
        self.data = np.array([])
        if length > 0:
            data_end = start + length
            data_format = file_byteorder + str(npts) + "f"
            if data_end > len(buffer):
                raise EOFError()
            content = buffer[start:data_end]
            data = struct.unpack(data_format, content)
            self.data = np.array(data)

        if self.nvhdr == 7:
            for footer, footer_metadata in SAC_FOOTERS.items():
                undefined = -12345.0
                length = 8
                start = footer_metadata.start + data_end
                end = start + length

                if end > len(buffer):
                    raise EOFError()
                content = buffer[start:end]

                value = struct.unpack(file_byteorder + "d", content)[0]

                # skip if undefined (value == -12345...)
                if value == undefined:
                    continue

                # SAC file has headers fields which are read only attributes in this
                # class. We skip them with this try/except.
                # TODO: This is a bit crude, should maybe be a bit more specific.
                try:
                    setattr(self, footer, value)
                except AttributeError as e:
                    if "object has no setter" in str(e):
                        pass

    def change_all_times(self, dtime: float) -> None:
        """Change all time headers by the same amount.

        Args:
            dtime: Time offset to apply.

        Warning:
            This method also changes the value for the current zero time header.
            Typically it should only be used when changing
            [`SacIO.iztype`][pysmo.lib.io.SacIO.iztype].
        """
        try:
            self._zero_time_can_be_none_zero = True
            for time_header in SAC_TIME_HEADERS:
                try:
                    setattr(self, time_header, getattr(self, time_header) + dtime)
                except AttributeError as e:
                    if "object has no setter" in str(e):
                        continue
                except TypeError as e:
                    if "unsupported operand type(s) for" in str(e):
                        continue

        finally:
            self._zero_time_can_be_none_zero = False

a class-attribute instance-attribute

a: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

First arrival time (seconds relative to reference time).

az property

az: float

Event to station azimuth (degrees).

b class-attribute instance-attribute

b: float = field(
    default=b,
    converter=float,
    validator=[type_validator(), validate_with_iztype],
)

Beginning value of the independent variable.

baz property

baz: float

Station to event azimuth (degrees).

cmpaz class-attribute instance-attribute

cmpaz: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Component azimuth (degrees clockwise from north).

cmpinc class-attribute instance-attribute

cmpinc: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Component incident angle (degrees from upward vertical; SEED/MINISEED uses dip: degrees from horizontal down).

data class-attribute instance-attribute

data: ndarray = field(
    factory=lambda: array([]), validator=type_validator()
)

Seismogram data.

delta class-attribute instance-attribute

delta: float = field(
    default=delta,
    converter=float,
    validator=type_validator(),
)

Increment between evenly spaced samples (nominal value).

depmax property

depmax: float | None

Maximum value of dependent variable.

depmen property

depmen: float | None

Mean value of dependent variable.

depmin property

depmin: float | None

Minimum value of dependent variable.

dist property

dist: float

Station to event distance (km).

e property

e: float

Ending value of the independent variable.

evdp class-attribute instance-attribute

evdp: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Event depth below surface (kilometers -- previously meters).

evel class-attribute instance-attribute

evel: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Event elevation (meters).

evla class-attribute instance-attribute

evla: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional([type_validator(), ge(-90), le(90)]),
)

Event latitude (degrees, north positive).

evlo class-attribute instance-attribute

evlo: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), ge(-180), le(180)]
    ),
)

Event longitude (degrees, east positive).

f class-attribute instance-attribute

f: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

Fini or end of event time (seconds relative to reference time).

gcarc property

gcarc: float

Station to event great circle arc length (degrees).

ibody class-attribute instance-attribute

ibody: str | None = field(
    default=None, validator=optional(validate_sacenum)
)

Body / Spheroid definition used in Distance Calculations.

idep class-attribute instance-attribute

idep: str = field(default=idep, validator=validate_sacenum)

Type of dependent variable.

ievreg class-attribute instance-attribute

ievreg: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(4)]),
)

Event geographic region.

ievtyp class-attribute instance-attribute

ievtyp: str = field(
    default=ievtyp, validator=validate_sacenum
)

Type of event.

iftype class-attribute instance-attribute

iftype: str = field(
    default=iftype, validator=validate_sacenum
)

Type of file.

iinst class-attribute instance-attribute

iinst: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(4)]),
)

Type of recording instrument.

imagsrc class-attribute instance-attribute

imagsrc: str | None = field(
    default=None, validator=optional(validate_sacenum)
)

Source of magnitude information.

imagtyp class-attribute instance-attribute

imagtyp: str | None = field(
    default=None, validator=optional(validate_sacenum)
)

Magnitude type.

iqual class-attribute instance-attribute

iqual: str | None = field(
    default=None, validator=optional(validate_sacenum)
)

Quality of data.

istreg class-attribute instance-attribute

istreg: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(4)]),
)

Station geographic region.

isynth class-attribute instance-attribute

isynth: str | None = field(
    default=None, validator=optional(validate_sacenum)
)

Synthetic data flag.

iztype class-attribute instance-attribute

iztype: str = field(
    default=iztype, validator=validate_sacenum
)

Reference time equivalence.

ka class-attribute instance-attribute

ka: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

First arrival time identification.

kcmpnm class-attribute instance-attribute

kcmpnm: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

Channel name. SEED volumes use three character names, and the third is the component/orientation. For horizontals, the current trend is to use 1 and 2 instead of N and E.

kdatrd class-attribute instance-attribute

kdatrd: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

Date data was read onto computer.

kevnm class-attribute instance-attribute

kevnm: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(16)]),
)

Event name.

kf class-attribute instance-attribute

kf: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

Fini identification.

khole class-attribute instance-attribute

khole: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

Nuclear: hole identifier; Other: location identifier (LOCID).

kinst class-attribute instance-attribute

kinst: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

Generic name of recording instrument.

knetwk class-attribute instance-attribute

knetwk: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

Name of seismic network.

ko class-attribute instance-attribute

ko: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

Event origin time identification.

kstnm class-attribute instance-attribute

kstnm: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

Station name.

kt0 class-attribute instance-attribute

kt0: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kt1 class-attribute instance-attribute

kt1: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kt2 class-attribute instance-attribute

kt2: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kt3 class-attribute instance-attribute

kt3: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kt4 class-attribute instance-attribute

kt4: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kt5 class-attribute instance-attribute

kt5: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kt6 class-attribute instance-attribute

kt6: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kt7 class-attribute instance-attribute

kt7: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kt8 class-attribute instance-attribute

kt8: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kt9 class-attribute instance-attribute

kt9: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined time pick identification.

kuser0 class-attribute instance-attribute

kuser0: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined variable storage area.

kuser1 class-attribute instance-attribute

kuser1: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined variable storage area.

kuser2 class-attribute instance-attribute

kuser2: str | None = field(
    default=None,
    validator=optional([type_validator(), max_len(8)]),
)

User defined variable storage area.

kzdate property

kzdate: str | None

ISO 8601 format of GMT reference date.

kztime property

kztime: str | None

Alphanumeric form of GMT reference time.

lcalda property

lcalda: Literal[True]

TRUE if DIST, AZ, BAZ, and GCARC are to be calculated from station and event coordinates.

Note

Above fields are all read only properties in this class, so they are always calculated.

leven class-attribute instance-attribute

leven: bool = field(
    default=leven, validator=type_validator()
)

TRUE if data is evenly spaced.

lovrok class-attribute instance-attribute

lovrok: bool | None = field(
    default=None, validator=optional(type_validator())
)

TRUE if it is okay to overwrite this file on disk.

lpspol class-attribute instance-attribute

lpspol: bool | None = field(
    default=None, validator=optional(type_validator())
)

TRUE if station components have a positive polarity (left-hand rule).

mag class-attribute instance-attribute

mag: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Event magnitude.

nevid class-attribute instance-attribute

nevid: int | None = field(
    default=None, validator=optional(type_validator())
)

Event ID (CSS 3.0).

norid class-attribute instance-attribute

norid: int | None = field(
    default=None, validator=optional(type_validator())
)

Origin ID (CSS 3.0).

npts property

npts: int

Number of points per data component.

nvhdr class-attribute instance-attribute

nvhdr: int = field(
    default=nvhdr, validator=type_validator()
)

Header version number.

nwfid class-attribute instance-attribute

nwfid: int | None = field(
    default=None, validator=optional(type_validator())
)

Waveform ID (CSS 3.0).

nxsize property

nxsize: int | None

Spectral Length (Spectral files only).

nysize property

nysize: int | None

Spectral Width (Spectral files only).

nzhour class-attribute instance-attribute

nzhour: int | None = field(
    default=None, validator=optional(type_validator())
)

GMT hour.

nzjday class-attribute instance-attribute

nzjday: int | None = field(
    default=None, validator=optional(type_validator())
)

GMT julian day.

nzmin class-attribute instance-attribute

nzmin: int | None = field(
    default=None, validator=optional(type_validator())
)

GMT minute.

nzmsec class-attribute instance-attribute

nzmsec: int | None = field(
    default=None, validator=optional(type_validator())
)

GMT millisecond.

nzsec class-attribute instance-attribute

nzsec: int | None = field(
    default=None, validator=optional(type_validator())
)

GMT second.

nzyear class-attribute instance-attribute

nzyear: int | None = field(
    default=None, validator=optional(type_validator())
)

GMT year corresponding to reference (zero) time in file.

o class-attribute instance-attribute

o: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

Event origin time (seconds relative to reference time).

odelta class-attribute instance-attribute

odelta: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Observed increment if different from nominal value.

ref_datetime property writable

ref_datetime: datetime | None

Return Python datetime object of GMT reference time and date.

resp0 class-attribute instance-attribute

resp0: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 0 (not currently used).

resp1 class-attribute instance-attribute

resp1: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 1 (not currently used).

resp2 class-attribute instance-attribute

resp2: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 2 (not currently used).

resp3 class-attribute instance-attribute

resp3: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 3 (not currently used).

resp4 class-attribute instance-attribute

resp4: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 4 (not currently used).

resp5 class-attribute instance-attribute

resp5: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 5 (not currently used).

resp6 class-attribute instance-attribute

resp6: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 6 (not currently used).

resp7 class-attribute instance-attribute

resp7: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 7 (not currently used).

resp8 class-attribute instance-attribute

resp8: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 8 (not currently used).

resp9 class-attribute instance-attribute

resp9: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Instrument response parameter 9 (not currently used).

stdp class-attribute instance-attribute

stdp: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Station depth below surface (meters).

stel class-attribute instance-attribute

stel: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

Station elevation above sea level (meters).

stla class-attribute instance-attribute

stla: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional([type_validator(), ge(-90), le(90)]),
)

Station latitude (degrees, north positive).

stlo class-attribute instance-attribute

stlo: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), ge(-180), le(180)]
    ),
)

Station longitude (degrees, east positive).

t0 class-attribute instance-attribute

t0: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 0 (seconds relative to reference time).

t1 class-attribute instance-attribute

t1: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 1 (seconds relative to reference time).

t2 class-attribute instance-attribute

t2: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 2 (seconds relative to reference time).

t3 class-attribute instance-attribute

t3: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 3 (seconds relative to reference time).

t4 class-attribute instance-attribute

t4: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 4 (seconds relative to reference time).

t5 class-attribute instance-attribute

t5: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 5 (seconds relative to reference time).

t6 class-attribute instance-attribute

t6: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 6 (seconds relative to reference time).

t7 class-attribute instance-attribute

t7: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 7 (seconds relative to reference time).

t8 class-attribute instance-attribute

t8: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 8 (seconds relative to reference time).

t9 class-attribute instance-attribute

t9: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(
        [type_validator(), validate_with_iztype]
    ),
)

User defined time pick or marker 9 (seconds relative to reference time).

user0 class-attribute instance-attribute

user0: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

user1 class-attribute instance-attribute

user1: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

user2 class-attribute instance-attribute

user2: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

user3 class-attribute instance-attribute

user3: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

user4 class-attribute instance-attribute

user4: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

user5 class-attribute instance-attribute

user5: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

user6 class-attribute instance-attribute

user6: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

user7 class-attribute instance-attribute

user7: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

user8 class-attribute instance-attribute

user8: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

user9 class-attribute instance-attribute

user9: float | None = field(
    default=None,
    converter=optional(float),
    validator=optional(type_validator()),
)

User defined variable storage area.

xmaximum property

xmaximum: float | None

Maximum value of X (Spectral files only).

xminimum property

xminimum: float | None

Minimum value of X (Spectral files only).

ymaximum property

ymaximum: float | None

Maximum value of Y (Spectral files only).

yminimum property

yminimum: float | None

Minimum value of Y (Spectral files only).

change_all_times

change_all_times(dtime: float) -> None

Change all time headers by the same amount.

Parameters:

Name Type Description Default
dtime float

Time offset to apply.

required
Warning

This method also changes the value for the current zero time header. Typically it should only be used when changing SacIO.iztype.

Source code in src/pysmo/lib/io/_sacio/sacio.py
def change_all_times(self, dtime: float) -> None:
    """Change all time headers by the same amount.

    Args:
        dtime: Time offset to apply.

    Warning:
        This method also changes the value for the current zero time header.
        Typically it should only be used when changing
        [`SacIO.iztype`][pysmo.lib.io.SacIO.iztype].
    """
    try:
        self._zero_time_can_be_none_zero = True
        for time_header in SAC_TIME_HEADERS:
            try:
                setattr(self, time_header, getattr(self, time_header) + dtime)
            except AttributeError as e:
                if "object has no setter" in str(e):
                    continue
            except TypeError as e:
                if "unsupported operand type(s) for" in str(e):
                    continue

    finally:
        self._zero_time_can_be_none_zero = False

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 SacIO instance.

Source code in src/pysmo/lib/io/_sacio/sacio.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 SacIO instance.
    """
    newinstance = cls()
    newinstance.read_buffer(buffer)
    return newinstance

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 SacIO instance.

Source code in src/pysmo/lib/io/_sacio/sacio.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 SacIO instance.
    """
    newinstance = cls()
    newinstance.read(filename)
    return newinstance

from_iris classmethod

from_iris(
    net: str,
    sta: str,
    cha: str,
    loc: str,
    force_single_result: bool = False,
    **kwargs: Any
) -> Self | dict[str, Self] | None

Create a list of SAC instances from a single IRIS request using the output format as "sac.zip".

Parameters:

Name Type Description Default
net str

Network code (e.g. "US")

required
sta str

Station code (e.g. "BSS")

required
cha str

Channel code (e.g. "BHZ")

required
loc str

Location code (e.g. "00")

required
force_single_result bool

If true, the function will return a single SAC object or None if the requests returns nothing.

False

Returns:

Type Description
Self | dict[str, Self] | None

A new SacIO instance.

Source code in src/pysmo/lib/io/_sacio/sacio.py
@classmethod
def from_iris(
    cls,
    net: str,
    sta: str,
    cha: str,
    loc: str,
    force_single_result: bool = False,
    **kwargs: Any,
) -> Self | dict[str, Self] | None:
    """Create a list of SAC instances from a single IRIS
    request using the output format as "sac.zip".

    Args:
        net: Network code (e.g. "US")
        sta: Station code (e.g. "BSS")
        cha: Channel code (e.g. "BHZ")
        loc: Location code (e.g. "00")
        force_single_result: If true, the function will return a single SAC
                            object or None if the requests returns nothing.

    Returns:
        A new SacIO instance.
    """
    kwargs["net"] = net
    kwargs["sta"] = sta
    kwargs["cha"] = cha
    kwargs["loc"] = loc
    kwargs["output"] = "sac.zip"

    if isinstance(kwargs["start"], datetime):
        kwargs["start"] = kwargs["start"].isoformat()

    end = kwargs.get("end", None)
    if end is not None and isinstance(end, datetime):
        kwargs["end"] = end.isoformat()

    transport = httpx.HTTPTransport(retries=3)
    client = httpx.Client(transport=transport)
    for attempt in range(SACIO_DEFAULTS.iris_request_retries):
        response = client.get(
            SACIO_DEFAULTS.iris_base_url,
            params=kwargs,
            follow_redirects=False,
            timeout=SACIO_DEFAULTS.iris_timeout_seconds,
        )
        if (
            response.status_code == 500
            and attempt < SACIO_DEFAULTS.iris_request_retries - 1
        ):
            _time.sleep(SACIO_DEFAULTS.iris_retry_delay_seconds)
            continue
        response.raise_for_status()
        break

    zip = ZipFile(BytesIO(response.content))

    result = {}
    for name in zip.namelist():
        buffer = zip.read(name)
        sac = cls.from_buffer(buffer)
        if force_single_result:
            return sac
        result[name] = sac
    return None if force_single_result else result

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/lib/io/_sacio/sacio.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.
    """

    filename = Path(filename).resolve()

    self.read_buffer(filename.read_bytes())

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/lib/io/_sacio/sacio.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.
    """

    if len(buffer) < 632:
        raise EOFError()

    # Guess the file endianness first using the unused12 header field.
    # It is located at position 276 and its value should be -12345.0.
    # Try reading with little endianness
    if struct.unpack("<f", buffer[276:280])[-1] == -12345.0:
        file_byteorder = "<"
    # otherwise assume big endianness.
    else:
        file_byteorder = ">"

    # Loop over all header fields and store them in the SAC object under their
    # respective private names.
    npts = 0
    for header, header_metadata in SAC_HEADERS.items():
        header_type = header_metadata.type
        header_required = header_metadata.required
        header_undefined = HEADER_TYPES[header_type].undefined
        start = header_metadata.start
        length = header_metadata.length
        end = start + length
        if end >= len(buffer):
            continue
        content = buffer[start:end]
        value = struct.unpack(file_byteorder + header_metadata.format, content)[0]
        if isinstance(value, bytes):
            # strip spaces and "\x00" chars
            value = value.decode().rstrip(" \x00")

        # npts is read only property in this class, but is needed for reading data
        if header == "npts":
            npts = int(value)

        # raise error if header is undefined AND required
        if value == header_undefined and header_required:
            raise RuntimeError(
                f"Required {header=} is undefined - invalid SAC file!"
            )

        # skip if undefined (value == -12345...) and not required
        if value == header_undefined and not header_required:
            continue

        # convert enumerated header to string and format others
        if header_type == "i":
            value = SAC_ENUMS_DICT[header](value).name

        # SAC file has headers fields which are read only attributes in this
        # class. We skip them with this try/except.
        # TODO: This is a bit crude, should maybe be a bit more specific.
        try:
            setattr(self, header, value)
        except AttributeError as e:
            if "object has no setter" in str(e):
                pass

    # Only accept IFTYPE = ITIME SAC files. Other IFTYPE use two data blocks,
    # which is something we don't support for now.
    if self.iftype.lower() != "time":
        raise NotImplementedError(
            f"Reading SAC files with IFTYPE=(I){self.iftype.upper()} is not supported."  # noqa: E501
        )

    # Read first data block
    start = 632
    length = npts * 4
    data_end = start + length
    self.data = np.array([])
    if length > 0:
        data_end = start + length
        data_format = file_byteorder + str(npts) + "f"
        if data_end > len(buffer):
            raise EOFError()
        content = buffer[start:data_end]
        data = struct.unpack(data_format, content)
        self.data = np.array(data)

    if self.nvhdr == 7:
        for footer, footer_metadata in SAC_FOOTERS.items():
            undefined = -12345.0
            length = 8
            start = footer_metadata.start + data_end
            end = start + length

            if end > len(buffer):
                raise EOFError()
            content = buffer[start:end]

            value = struct.unpack(file_byteorder + "d", content)[0]

            # skip if undefined (value == -12345...)
            if value == undefined:
                continue

            # SAC file has headers fields which are read only attributes in this
            # class. We skip them with this try/except.
            # TODO: This is a bit crude, should maybe be a bit more specific.
            try:
                setattr(self, footer, value)
            except AttributeError as e:
                if "object has no setter" in str(e):
                    pass

write

write(filename: str | PathLike) -> None

Writes data and header values to a SAC file.

Parameters:

Name Type Description Default
filename str | PathLike

Name of the sacfile to write to.

required
Source code in src/pysmo/lib/io/_sacio/sacio.py
def write(self, filename: str | PathLike) -> None:
    """Writes data and header values to a SAC file.

    Args:
        filename: Name of the sacfile to write to.
    """
    with open(filename, "wb") as file_handle:
        # loop over all valid header fields and write them to the file
        for header, header_metadata in SAC_HEADERS.items():
            header_type = header_metadata.type
            header_format = header_metadata.format
            start = header_metadata.start
            header_undefined = HEADER_TYPES[header_type].undefined

            value = None
            try:
                if hasattr(self, header):
                    value = getattr(self, header)
            except TypeError:
                value = None

            # convert enumerated header to integer if it is not None
            if header_type == "i" and value is not None:
                value = SAC_ENUMS_DICT[header][value]

            # set None to -12345
            if value is None:
                value = header_undefined

            # Encode strings to bytes
            if isinstance(value, str):
                value = value.encode()

            # write to file
            file_handle.seek(start)
            file_handle.write(struct.pack(header_format, value))

        # write data (if npts > 0)
        data_1_start = 632
        data_1_end = data_1_start + self.npts * 4
        file_handle.truncate(data_1_start)
        if self.npts > 0:
            file_handle.seek(data_1_start)
            for x in self.data:
                file_handle.write(struct.pack("f", x))

        if self.nvhdr == 7:
            for footer, footer_metadata in SAC_FOOTERS.items():
                undefined = -12345.0
                start = footer_metadata.start + data_1_end
                value = None
                try:
                    if hasattr(self, footer):
                        value = getattr(self, footer)
                except AttributeError:
                    value = None

                # set None to -12345
                if value is None:
                    value = undefined

                # write to file
                file_handle.seek(start)
                file_handle.write(struct.pack("d", value))

mini_utils

Mini utils.

Functions:

Name Description
matching_pysmo_types

Returns pysmo types that objects may be an instance of.

proto2mini

Returns valid Mini classes that implement the given pysmo Protocol.

_AnyMini

_AnyMini = _BaseMini | _ToolsMini

Type alias for any pysmo Mini class.

_AnyProto

_AnyProto = _BaseProto | _ToolsProto

Type alias for any pysmo Protocol class.

matching_pysmo_types

matching_pysmo_types(
    obj: object,
) -> tuple[type[_AnyProto], ...]

Returns pysmo types that objects may be an instance of.

Parameters:

Name Type Description Default
obj object

Name of the object to check.

required

Returns:

Type Description
tuple[type[_AnyProto], ...]

Pysmo types for which obj is an instance of.

Examples:

Pysmo types matching instances of MiniLocationWithDepth or the class itself:

>>> from pysmo.lib.mini_utils import matching_pysmo_types
>>> from pysmo import MiniLocationWithDepth
>>>
>>> mini = MiniLocationWithDepth(latitude=12, longitude=34, depth=56)
>>> matching_pysmo_types(mini)
(<class 'pysmo.Location'>, <class 'pysmo.LocationWithDepth'>)
>>>
>>> matching_pysmo_types(MiniLocationWithDepth)
(<class 'pysmo.Location'>, <class 'pysmo.LocationWithDepth'>)
>>>
Source code in src/pysmo/lib/mini_utils.py
def matching_pysmo_types(obj: object) -> tuple[type[_AnyProto], ...]:
    """Returns pysmo types that objects may be an instance of.

    Args:
        obj: Name of the object to check.

    Returns:
        Pysmo types for which `obj` is an instance of.

    Examples:
        Pysmo types matching instances of
        [`MiniLocationWithDepth`][pysmo.MiniLocationWithDepth] or the class
        itself:

        ```python
        >>> from pysmo.lib.mini_utils import matching_pysmo_types
        >>> from pysmo import MiniLocationWithDepth
        >>>
        >>> mini = MiniLocationWithDepth(latitude=12, longitude=34, depth=56)
        >>> matching_pysmo_types(mini)
        (<class 'pysmo.Location'>, <class 'pysmo.LocationWithDepth'>)
        >>>
        >>> matching_pysmo_types(MiniLocationWithDepth)
        (<class 'pysmo.Location'>, <class 'pysmo.LocationWithDepth'>)
        >>>
        ```
    """

    matches: list[type[_AnyProto]] = []

    possible_protos = _get_flattened_types(_AnyProto)

    for proto in possible_protos:
        if _safe_check(obj, proto):
            matches.append(cast(type[_AnyProto], proto))

    return tuple(matches)

proto2mini

proto2mini(
    proto: type[_AnyProto],
) -> tuple[type[_AnyMini], ...]

Returns valid Mini classes that implement the given pysmo Protocol.

This function resolves the input protocol (handling modern type aliases and unions) and filters the available 'Mini' classes to find those that structurally implement it.

Parameters:

Name Type Description Default
proto type[_AnyProto]

A pysmo type (e.g., Location, Event) or a type alias pointing to one.

required

Returns:

Type Description
tuple[type[_AnyMini], ...]

A tuple of concrete Mini classes (e.g., MiniLocation, MiniEvent) that satisfy the interface defined by proto.

Examples:

Get all Mini classes that implement the Location protocol:

>>> from pysmo.lib.mini_utils import proto2mini
>>> from pysmo import Location, Event
>>> proto2mini(Location)
(<class 'pysmo.MiniStation'>, <class 'pysmo.MiniEvent'>, <class 'pysmo.MiniLocation'>, <class 'pysmo.MiniLocationWithDepth'>)
>>>

Works with Type Aliases and Unions (if the input is a union, it returns Minis matching any of the protocols in that union):

>>> type MyProto = Location | Event
>>> proto2mini(MyProto)
(<class 'pysmo.MiniStation'>, <class 'pysmo.MiniEvent'>, <class 'pysmo.MiniLocation'>, <class 'pysmo.MiniLocationWithDepth'>)
>>>
Source code in src/pysmo/lib/mini_utils.py
def proto2mini(proto: type[_AnyProto]) -> tuple[type[_AnyMini], ...]:
    """Returns valid Mini classes that implement the given pysmo Protocol.

    This function resolves the input protocol (handling modern type aliases and
    unions) and filters the available 'Mini' classes to find those that
    structurally implement it.

    Args:
        proto: A pysmo type (e.g., `Location`, `Event`) or a type alias
            pointing to one.

    Returns:
        A tuple of concrete Mini classes (e.g., `MiniLocation`, `MiniEvent`)
            that satisfy the interface defined by `proto`.

    Examples:
        Get all Mini classes that implement the `Location` protocol:

        ```python
        >>> from pysmo.lib.mini_utils import proto2mini
        >>> from pysmo import Location, Event
        >>> proto2mini(Location)
        (<class 'pysmo.MiniStation'>, <class 'pysmo.MiniEvent'>, <class 'pysmo.MiniLocation'>, <class 'pysmo.MiniLocationWithDepth'>)
        >>>
        ```

        Works with Type Aliases and Unions (if the input is a union, it returns
        Minis matching *any* of the protocols in that union):

        ```python
        >>> type MyProto = Location | Event
        >>> proto2mini(MyProto)
        (<class 'pysmo.MiniStation'>, <class 'pysmo.MiniEvent'>, <class 'pysmo.MiniLocation'>, <class 'pysmo.MiniLocationWithDepth'>)
        >>>
        ```
    """

    target_proto = _get_flattened_types(proto)[0]
    possible_minis = _get_flattened_types(_AnyMini)

    return tuple(
        mini for mini in possible_minis if target_proto in matching_pysmo_types(mini)
    )

validators

Validators for pysmo classes using attrs.

Functions:

Name Description
datetime_is_utc

Ensure pandas.Timestamp objects have tzdata=timezone.utc set.

datetime_is_utc

datetime_is_utc(
    _: Any, attribute: Attribute, value: Timestamp | None
) -> None

Ensure pandas.Timestamp objects have tzdata=timezone.utc set.

Source code in src/pysmo/lib/validators.py
def datetime_is_utc(_: Any, attribute: Attribute, value: Timestamp | None) -> None:
    """Ensure [`pandas.Timestamp`][pandas.Timestamp] objects have `#!py tzdata=timezone.utc` set."""
    if value is None:
        return
    if value.tzinfo != timezone.utc:
        raise TypeError(
            f"Timestamp object {attribute} doesn't have tzdata=timezone.utc"
        )