├── .gitignore ├── setup.cfg ├── CHANGELOG.txt ├── docs ├── source │ ├── manual.pdf │ ├── metadata.yaml │ ├── license.md │ ├── obspy.md │ ├── package.md │ └── index.md ├── make_pdf.sh └── mkdocs.yml ├── pysac ├── __init__.py ├── util.py ├── header.py ├── arrayio.py └── sactrace.py ├── setup.py ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | docs/site/ 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.2.0: 2 | - Update to match ObsPy 1.0.3 3 | 0.0.1: 4 | - Released to match ObsPy 1.0.0 5 | -------------------------------------------------------------------------------- /docs/source/manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LANL-Seismoacoustics/pysac/HEAD/docs/source/manual.pdf -------------------------------------------------------------------------------- /docs/make_pdf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pandoc -f markdown -t latex --toc -o source/manual.pdf source/index.md source/package.md source/obspy.md source/license.md source/metadata.yaml 4 | -------------------------------------------------------------------------------- /docs/source/metadata.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'PySAC: Python Interface to the Seismic Analysis Code (SAC) File Format.' 3 | author: Jonathan MacCarthy, Los Alamos National Laboratory 4 | geometry: margin=1in 5 | numbersections: yes 6 | --- 7 | -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PySAC 2 | site_url: http://jkmacc-lanl.github.io/pysac/ 3 | repo_url: https://github.com/jkmacc-LANL/pysac 4 | site_description: Python interface to the Seismic Analysis Code (SAC) file format. 5 | theme: readthedocs 6 | use_directory_urls: false 7 | markdown_extensions: 8 | - toc: 9 | permalink: True 10 | docs_dir: 'source' 11 | site_dir: 'site' 12 | pages: 13 | - Introduction: 'index.md' 14 | - Package: 'package.md' 15 | - About: 16 | - 'License': 'license.md' 17 | - 'ObsPy': 'obspy.md' 18 | -------------------------------------------------------------------------------- /pysac/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python interface to the Seismic Analysis Code (SAC) file format. 4 | 5 | :copyright: 6 | The Los Alamos National Security, LLC, Yannik Behr, C. J. Ammon, 7 | C. Satriano, L. Krischer, and J. MacCarthy 8 | :license: 9 | GNU Lesser General Public License, Version 3 10 | (http://www.gnu.org/copyleft/lesser.html) 11 | """ 12 | from __future__ import (absolute_import, division, print_function, 13 | unicode_literals) 14 | from future.builtins import * # NOQA 15 | 16 | from .sactrace import SACTrace 17 | 18 | __version__ = '0.2.0' 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | import setuptool 3 | except: 4 | pass 5 | 6 | import os 7 | import os.path 8 | import sys 9 | import commands 10 | from glob import glob 11 | from distutils.core import setup, Extension 12 | 13 | setup(name = 'pysac', 14 | version = '0.2.0', 15 | description = 'Python interface to the Seismic Analsys Code file format.', 16 | author = 'J. MacCarthy', 17 | author_email = 'jkmacc@lanl.gov', 18 | long_description = ''' 19 | Python interface to the Seismic Analsys Code file format. 20 | ''', 21 | packages = ['pysac'], 22 | py_modules = ['pysac.header', 'pysac.util', 'pysac.arrayio', 23 | 'pysac.sactrace'], 24 | ) 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015. Los Alamos National Security, LLC for pysac LA-CC-15-051. This 2 | material was produced under U.S. Government contract DE-AC52-06NA25396 for Los 3 | Alamos National Laboratory (LANL), which is operated by Los Alamos National 4 | Security, LLC for the U.S. Department of Energy. The U.S. Government has rights 5 | to use, reproduce, and distribute this software. NEITHER THE GOVERNMENT NOR 6 | LOS ALAMOS NATIONAL SECURITY, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR 7 | ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is modified to 8 | produce derivative works, such modified software should be clearly marked, so 9 | as not to confuse it with the version available from LANL. 10 | 11 | Additionally, this library is free software; you can redistribute it and/or 12 | modify it under the terms of the GNU Lesser General Public License, v. 3., as 13 | published by the Free Software Foundation. Accordingly, this library is 14 | distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 15 | without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 16 | PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 17 | -------------------------------------------------------------------------------- /docs/source/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Copyright 2015. Los Alamos National Security, LLC for pysac LA-CC-15-051. This 4 | material was produced under U.S. Government contract DE-AC52-06NA25396 for Los 5 | Alamos National Laboratory (LANL), which is operated by Los Alamos National 6 | Security, LLC for the U.S. Department of Energy. The U.S. Government has rights 7 | to use, reproduce, and distribute this software. NEITHER THE GOVERNMENT NOR 8 | LOS ALAMOS NATIONAL SECURITY, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR 9 | ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is modified to 10 | produce derivative works, such modified software should be clearly marked, so 11 | as not to confuse it with the version available from LANL. 12 | 13 | Additionally, this library is free software; you can redistribute it and/or 14 | modify it under the terms of the GNU Lesser General Public License, v. 3., as 15 | published by the Free Software Foundation. Accordingly, this library is 16 | distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 17 | without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 18 | PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 19 | -------------------------------------------------------------------------------- /docs/source/obspy.md: -------------------------------------------------------------------------------- 1 | # Relationship to ObsPy 2 | 3 | PySAC is largely re-written from the 4 | [obspy.io.sac](https://github.com/obspy/obspy/tree/master/obspy/io/sac) module, 5 | with the intention of eventually replacing it. 6 | 7 | ## Why a re-write? 8 | 9 | The sacio module underlying ObsPy's SAC handling was sometimes hard to follow, 10 | as it has a long inheritance, which made it hard to track down issues or make 11 | fixes. This re-write attempts to make the SAC plugin easier to understand and 12 | maintain, as well as offer some potential improvements. 13 | 14 | ## Improve maintainability. 15 | 16 | I've split out the header specification (header.py), the low-level array-based 17 | SAC file I/O (arrayio.py), and the object-oriented interface (sactrace.py), 18 | whereas it was previously all within one sacio.py module. I hope that the flow 19 | of how each plugs into the other is clear, so that bug tracking is 20 | straight-forward, and that hacking on one aspect of SAC handling is not 21 | cluttered/distracted by another. 22 | 23 | ## Expand support for round-trip SAC file processing 24 | 25 | This rewrite attempts to improve support for a common work flow: read one or 26 | more SAC files into ObsPy, do some processing, then (over)write them back as 27 | SAC files that look mostly like the originals. Previously, ObsPy Traces written 28 | to SAC files wrote only files based on the first sample time (iztype 9/'ib'). 29 | In `util.py:obspy_to_sac_header` of this module, if an old tr.stats.sac SAC 30 | header is found, the iztype and reference "nz" times are used and kept, and the 31 | "b" and "e" times of the Trace being written are adjusted according to this 32 | reference time. This preserves the absolute reference of any relative time 33 | headers, like t0-t9, carried from the old SAC header into the new file. This 34 | can only be done if SAC to Trace conversion preserves these "nz" time headers, 35 | which is possible with the current `debug_headers=True` flag. 36 | -------------------------------------------------------------------------------- /docs/source/package.md: -------------------------------------------------------------------------------- 1 | # Package Organization 2 | 3 | ## pysac.header 4 | 5 | SAC header specification, including documentation. 6 | 7 | Header names, order, and types, nulls, as well as allowed enumerated values, are 8 | specified here. Header name strings, and their array order are contained in 9 | separate float, int, and string tuples. Enumerated values, and their allowed 10 | string and integer values, are in dictionaries. Header value documentation is 11 | in a dictionary, `DOC`, for reuse throughout the package. 12 | 13 | 14 | ## pysac.util 15 | 16 | PySAC helper functions and data. Contains functions to validate and convert 17 | enumerated values, byteorder consistency checking, and SAC reference time 18 | reading. 19 | 20 | Two of the most important functions in this module are `sac_to_obspy_header` 21 | and `obspy_to_sac_header`. These contain the conversion routines between SAC 22 | header dictionaries and ObsPy header dictionaries. **These functions control 23 | the way ObsPy reads and writes SAC files,** which was one of the main 24 | motivations for authoring this package. 25 | 26 | 27 | ## pysac.arrayio 28 | 29 | Low-level array interface to the SAC file format. 30 | 31 | Functions in this module work directly with numpy arrays that mirror the SAC 32 | format, and comprise much of the machinery that underlies the `SACTrace` class. 33 | The 'primitives' in this module are the float, int, and string header arrays, 34 | the float data array, and a header dictionary. Convenience functions are 35 | provided to convert between header arrays and more user-friendly dictionaries. 36 | 37 | These read/write routines are very literal; there is almost no value or type 38 | checking, except for byteorder and header/data array length. File- and array- 39 | based checking routines are provided for additional checks where desired. 40 | 41 | Reading and writing are done with `read_sac` and `write_sac` for binary SAC 42 | files, and `read_sac_ascii` and `write_sac_ascii` for alphanumeric files. 43 | Conversions between header dictionaries and the three SAC header arrays are done 44 | with the `header_arrays_to_dict` and `dict_to_header_arrays` functions. 45 | Validation of header values and data is managed by `validate_sac_content`, 46 | which can currently do six different tests. 47 | 48 | 49 | ## pysac.sactrace 50 | 51 | Contains the `SACTrace` class, which is the main user-facing interface to the 52 | SAC file format. 53 | 54 | The `SACTrace` object maintains consistency between SAC headers and manages 55 | header values in a user-friendly way. This includes some value-checking, native 56 | Python logicals and nulls instead of SAC's header-dependent logical/null 57 | representation. 58 | 59 | 60 | ### Reading and writing SAC files 61 | 62 | PySAC can read and write evenly-spaced time-series files. It supports big or 63 | little-endian binary files, or alphanumeric/ASCII files. 64 | 65 | ```python 66 | # read from a binary file 67 | sac = SACTrace.read(filename) 68 | 69 | # read header only 70 | sac = SACTrace.read(filename, headonly=True) 71 | 72 | # write header-only, file must exist 73 | sac.write(filename, headonly=True) 74 | 75 | # read from an ASCII file 76 | sac = SACTrace.read(filename, ascii=True) 77 | 78 | # write a binary SAC file for a Sun machine 79 | sac.write(filename, byteorder='big') 80 | ``` 81 | 82 | ### Headers 83 | 84 | In the `SACTrace` class, SAC headers are implemented as properties, with 85 | appropriate *getters* and *setters*. The getters/setters translate user-facing 86 | native Python values like `True`, `False`, and `None` to the appropriate SAC 87 | header values, like `1`, `0`, `-12345`, `'-12345 '`, etc. 88 | 89 | Header values that depend on the SAC `.data` vector are calculated on-the-fly, 90 | and fall back to the stored header value. 91 | 92 | A convenient read-only dictionary of non-null, raw SAC header values is 93 | available as `SACTrace._header`. Formatted non-null headers are viewable using 94 | `print(sac)` or the `.lh()` or `listhdr()` methods. Relative time headers and 95 | picks are viewable with `lh('picks')`. 96 | 97 | 98 | ### Reference time and relative time header handling 99 | 100 | The SAC reference time is built from "nz..." time fields in the header, and it 101 | is available as the attribute `.reftime`, an ObsPy `UTCDateTime` instance. 102 | `reftime` can be modified in two ways: by resetting it with a new absolute 103 | `UTCDateTime` instance, or by adding/subtracting seconds from it. **Modifying 104 | the `reftime` will also modify all relative time headers such that they are 105 | still correct in an absolute sense**. This includes 106 | `a`, `b`, `e`, `f`, `o`, and `t1`-`t9`. This means that adjusting the 107 | reference time does not invalidate the origin time, the first sample time, or 108 | any picks! 109 | 110 | Here, we build a 100-second `SACTrace` that starts at Y2K. 111 | 112 | ```python 113 | sac = SACTrace(nzyear=2000, nzjday=1, nzhour=0, nzmin=0, nzsec=0, nzmsec=0, 114 | t1=23.5, data=numpy.arange(101)) 115 | 116 | sac.reftime 117 | sac.b, sac.e, sac.t1 118 | ``` 119 | 120 | ``` 121 | 2000-01-01T00:00:00.000000Z 122 | (0.0, 100.0, 23.5) 123 | ``` 124 | 125 | Move reference time by relative seconds, relative time headers are preserved. 126 | ```python 127 | sac.reftime -= 2.5 128 | sac.b, sac.e, sac.t1 129 | ``` 130 | 131 | ``` 132 | (2.5, 102.5, 26.0) 133 | ``` 134 | 135 | Set reference time to new absolute time, two minutes later. Relative time 136 | headers are preserved. 137 | ```python 138 | sac.reftime = UTCDateTime(2000, 1, 1, 0, 2, 0, 0) 139 | sac.b, sac.e 140 | ``` 141 | 142 | ``` 143 | (-120.0, -20.0, -96.5) 144 | ``` 145 | 146 | 147 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | # PySAC 2 | 3 | Python interface to the [Seismic Analysis 4 | Code](http://ds.iris.edu/files/sac-manual/) (SAC) file format. 5 | 6 | File-type support: 7 | 8 | * little and big-endian binary format 9 | * alphanumeric format 10 | * evenly-sampled data 11 | * time-series, not spectra 12 | 13 | [Project page](https://lanl-seismoacoustics.github.io/pysac) 14 | [Repository](https://github.com/LANL-Seismoacoustics/pysac) 15 | [Docs PDF](manual.pdf) 16 | 17 | ## Goals 18 | 19 | 1. Expose the file format in a way that is intuitive to SAC users and to Python 20 | programmers 21 | 2. Maintaining header validity when converting between ObsPy Traces. 22 | 23 | ## Features 24 | 25 | 1. **Read and write SAC binary or ASCII** 26 | - autodetect or specify expected byteorder 27 | - optional file size checking and/or header consistency checks 28 | - header-only reading and writing 29 | - "overwrite OK" checking ('lovrok' header) 30 | 2. **Convenient access and manipulation of relative and absolute time headers** 31 | 3. **User-friendly header printing/viewing** 32 | 4. **Fast access to header values from attributes** 33 | - With type checking, null handling, and enumerated value checking 34 | 5. **Convert to/from ObsPy Traces** 35 | - Conversion from ObsPy Trace to SAC trace retains detected previous SAC header values. 36 | - Conversion to ObsPy Trace retains the *complete* SAC header. 37 | 38 | ## Usage examples 39 | 40 | ### Read/write SAC files 41 | ```python 42 | # read from a binary file 43 | sac = SACTrace.read(filename) 44 | 45 | # read header only 46 | sac = SACTrace.read(filename, headonly=True) 47 | 48 | # write header-only, file must exist 49 | sac.write(filename, headonly=True) 50 | 51 | # read from an ASCII file 52 | sac = SACTrace.read(filename, ascii=True) 53 | 54 | # write a binary SAC file for a Sun machine 55 | sac.write(filename, byteorder='big') 56 | ``` 57 | 58 | ### Reference-time and relative time headers 59 | ```python 60 | sac = SACTrace(nzyear=2000, nzjday=1, nzhour=0, nzmin=0, nzsec=0, nzmsec=0, 61 | t1=23.5, data=numpy.arange(100)) 62 | 63 | sac.reftime 64 | sac.b, sac.e, sac.t1 65 | ``` 66 | 67 | ``` 68 | 2000-01-01T00:00:00.000000Z 69 | (0.0, 99.0, 23.5) 70 | ``` 71 | 72 | Move reference time by relative seconds, relative time headers are preserved. 73 | ```python 74 | sac.reftime -= 2.5 75 | sac.b, sac.e, sac.t1 76 | ``` 77 | 78 | ``` 79 | (2.5, 101.5, 26.0) 80 | ``` 81 | 82 | Set reference time to new absolute time, relative time headers are preserved. 83 | ```python 84 | sac.reftime = UTCDateTime(2000, 1, 1, 0, 2, 0, 0) 85 | sac.b, sac.e 86 | ``` 87 | 88 | ``` 89 | (-120.0, -21.0, -96.5) 90 | ``` 91 | 92 | ### Quick header viewing 93 | 94 | Print non-null header values. 95 | ```python 96 | sac = SACTrace() 97 | print sac 98 | ``` 99 | 100 | ``` 101 | Reference Time = 01/01/2000 (001) 00:00:00.000000 102 | iztype IB: begin time 103 | b = 0.0 104 | cmpaz = 0.0 105 | cmpinc = 0.0 106 | delta = 1.0 107 | e = 99.0 108 | iftype = itime 109 | internal0 = 2.0 110 | iztype = ib 111 | kcmpnm = Z 112 | lcalda = False 113 | leven = True 114 | lovrok = True 115 | lpspol = True 116 | npts = 100 117 | nvhdr = 6 118 | nzhour = 0 119 | nzjday = 1 120 | nzmin = 0 121 | nzmsec = 0 122 | nzsec = 0 123 | nzyear = 2000 124 | ``` 125 | 126 | Print relative time header values. 127 | ```python 128 | sac.lh('picks') 129 | ``` 130 | 131 | ``` 132 | Reference Time = 01/01/1970 (001) 00:00:00.000000 133 | iztype IB: begin time 134 | a = None 135 | b = 0.0 136 | e = 0.0 137 | f = None 138 | o = None 139 | t0 = None 140 | t1 = None 141 | t2 = None 142 | t3 = None 143 | t4 = None 144 | t5 = None 145 | t6 = None 146 | t7 = None 147 | t8 = None 148 | t9 = None 149 | ``` 150 | 151 | ### Header values as attributes 152 | 153 | Great for interactive use, with (ipython) tab-completion... 154 | ```python 155 | sac. 156 | ``` 157 | 158 | ``` 159 | sac.a sac.kevnm sac.nzsec 160 | sac.az sac.kf sac.nzyear 161 | sac.b sac.khole sac.o 162 | sac.baz sac.kinst sac.odelta 163 | sac.byteorder sac.knetwk sac.read 164 | sac.cmpaz sac.ko sac.reftime 165 | sac.cmpinc sac.kstnm sac.scale 166 | sac.copy sac.kt0 sac.stdp 167 | sac.data sac.kt1 sac.stel 168 | sac.delta sac.kt2 sac.stla 169 | sac.depmax sac.kt3 sac.stlo 170 | sac.depmen sac.kt4 sac.t0 171 | sac.depmin sac.kt5 sac.t1 172 | sac.dist sac.kt6 sac.t2 173 | sac.e sac.kt7 sac.t3 174 | sac.evdp sac.kt8 sac.t4 175 | sac.evla sac.kt9 sac.t5 176 | sac.evlo sac.kuser0 sac.t6 177 | sac.f sac.kuser1 sac.t7 178 | sac.from_obspy_trace sac.kuser2 sac.t8 179 | sac.gcarc sac.lcalda sac.t9 180 | sac.idep sac.leven sac.to_obspy_trace 181 | sac.ievreg sac.lh sac.unused23 182 | sac.ievtyp sac.listhdr sac.user0 183 | sac.iftype sac.lovrok sac.user1 184 | sac.iinst sac.lpspol sac.user2 185 | sac.imagsrc sac.mag sac.user3 186 | sac.imagtyp sac.nevid sac.user4 187 | sac.internal0 sac.norid sac.user5 188 | sac.iqual sac.npts sac.user6 189 | sac.istreg sac.nvhdr sac.user7 190 | sac.isynth sac.nwfid sac.user8 191 | sac.iztype sac.nzhour sac.user9 192 | sac.ka sac.nzjday sac.validate 193 | sac.kcmpnm sac.nzmin sac.write 194 | sac.kdatrd sac.nzmsec 195 | ``` 196 | 197 | ...and documentation! 198 | ```python 199 | sac.iztype? 200 | ``` 201 | 202 | ``` 203 | Type: property 204 | String form: 205 | Docstring: 206 | I Reference time equivalence: 207 | * IUNKN (5): Unknown 208 | * IB (9): Begin time 209 | * IDAY (10): Midnight of reference GMT day 210 | * IO (11): Event origin time 211 | * IA (12): First arrival time 212 | * ITn (13-22): User defined time pick n, n=0,9 213 | ``` 214 | 215 | ### Convert to/from ObsPy Traces 216 | 217 | ```python 218 | from obspy import read 219 | tr = read()[0] 220 | print tr.stats 221 | ``` 222 | ``` 223 | network: BW 224 | station: RJOB 225 | location: 226 | channel: EHZ 227 | starttime: 2009-08-24T00:20:03.000000Z 228 | endtime: 2009-08-24T00:20:32.990000Z 229 | sampling_rate: 100.0 230 | delta: 0.01 231 | npts: 3000 232 | calib: 1.0 233 | back_azimuth: 100.0 234 | inclination: 30.0 235 | ``` 236 | 237 | ```python 238 | sac = SACTrace.from_obspy_trace(tr) 239 | print sac 240 | ``` 241 | 242 | ``` 243 | Reference Time = 08/24/2009 (236) 00:20:03.000000 244 | iztype IB: begin time 245 | b = 0.0 246 | cmpaz = 0.0 247 | cmpinc = 0.0 248 | delta = 0.00999999977648 249 | depmax = 1293.77099609 250 | depmen = -4.49556303024 251 | depmin = -1515.81311035 252 | e = 29.9899993297 253 | iftype = itime 254 | internal0 = 2.0 255 | iztype = ib 256 | kcmpnm = EHZ 257 | knetwk = BW 258 | kstnm = RJOB 259 | lcalda = False 260 | leven = True 261 | lovrok = True 262 | lpspol = True 263 | npts = 3000 264 | nvhdr = 6 265 | nzhour = 0 266 | nzjday = 236 267 | nzmin = 20 268 | nzmsec = 0 269 | nzsec = 3 270 | nzyear = 2009 271 | scale = 1.0 272 | ``` 273 | 274 | ```python 275 | tr2 = sac.to_obspy_trace() 276 | print tr2.stats 277 | ``` 278 | 279 | ``` 280 | network: BW 281 | station: RJOB 282 | location: 283 | channel: EHZ 284 | starttime: 2009-08-24T00:20:03.000000Z 285 | endtime: 2009-08-24T00:20:32.990000Z 286 | sampling_rate: 100.0 287 | delta: 0.01 288 | npts: 3000 289 | calib: 1.0 290 | sac: AttribDict({'cmpaz': 0.0, 'nzyear': 2009, 'nzjday': 236, 291 | 'iztype': 9, 'evla': 0.0, 'nzhour': 0, 'lcalda': 0, 'evlo': 0.0, 292 | 'scale': 1.0, 'nvhdr': 6, 'depmin': -1515.8131, 'kcmpnm': 'EHZ', 293 | 'nzsec': 3, 'internal0': 2.0, 'depmen': -4.495563, 'cmpinc': 0.0, 294 | 'depmax': 1293.771, 'iftype': 1, 'delta': 0.0099999998, 'nzmsec': 295 | 0, 'lpspol': 1, 'b': 0.0, 'e': 29.99, 'leven': 1, 'kstnm': 'RJOB', 296 | 'nzmin': 20, 'lovrok': 1, 'npts': 3000, 'knetwk': 'BW'}) 297 | ``` 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE**: this repository has been archived. PySAC was incorporated into [ObsPy's SAC plugin](https://github.com/obspy/obspy/tree/master/obspy/io/sac), which is where development and maintenance is happening. 2 | 3 | # PySAC 4 | 5 | Python interface to the [Seismic Analysis 6 | Code](http://ds.iris.edu/files/sac-manual/) (SAC) file format. 7 | 8 | File-type support: 9 | 10 | * little and big-endian binary format 11 | * alphanumeric format 12 | * evenly-sampled data 13 | * time-series, not spectra 14 | 15 | [Project page](https://lanl-seismoacoustics.github.io/pysac) 16 | [Repository](https://github.com/lanl-seismoacoustics/pysac) 17 | [LANL Seismoacoustics](https://lanl-seismoacoustics.github.io/) 18 | 19 | ## Goals 20 | 21 | 1. Expose the file format in a way that is intuitive to SAC users and to Python 22 | programmers 23 | 2. Maintaining header validity when converting between ObsPy Traces. 24 | 25 | ## Features 26 | 27 | 1. **Read and write SAC binary or ASCII** 28 | - autodetect or specify expected byteorder 29 | - optional file size checking and/or header consistency checks 30 | - header-only reading and writing 31 | - "overwrite OK" checking ('lovrok' header) 32 | 2. **Convenient access and manipulation of relative and absolute time headers** 33 | 3. **User-friendly header printing/viewing** 34 | 4. **Fast access to header values from attributes** 35 | - With type checking, null handling, and enumerated value checking 36 | 5. **Convert to/from ObsPy Traces** 37 | - Conversion from ObsPy Trace to SAC trace retains detected previous SAC header values. 38 | - Conversion to ObsPy Trace retains the *complete* SAC header. 39 | 40 | ## Usage examples 41 | 42 | ### Read/write SAC files 43 | ```python 44 | # read from a binary file 45 | sac = SACTrace.read(filename) 46 | 47 | # read header only 48 | sac = SACTrace.read(filename, headonly=True) 49 | 50 | # write header-only, file must exist 51 | sac.write(filename, headonly=True) 52 | 53 | # read from an ASCII file 54 | sac = SACTrace.read(filename, ascii=True) 55 | 56 | # write a binary SAC file for a Sun machine 57 | sac.write(filename, byteorder='big') 58 | ``` 59 | 60 | ### Reference-time and relative time headers 61 | ```python 62 | sac = SACTrace(nzyear=2000, nzjday=1, nzhour=0, nzmin=0, nzsec=0, nzmsec=0, 63 | t1=23.5, data=numpy.arange(100)) 64 | 65 | sac.reftime 66 | sac.b, sac.e, sac.t1 67 | ``` 68 | 69 | ``` 70 | 2000-01-01T00:00:00.000000Z 71 | (0.0, 99.0, 23.5) 72 | ``` 73 | 74 | Move reference time by relative seconds, relative time headers are preserved. 75 | ```python 76 | sac.reftime -= 2.5 77 | sac.b, sac.e, sac.t1 78 | ``` 79 | 80 | ``` 81 | (2.5, 101.5, 26.0) 82 | ``` 83 | 84 | Set reference time to new absolute time, relative time headers are preserved. 85 | ```python 86 | sac.reftime = UTCDateTime(2000, 1, 1, 0, 2, 0, 0) 87 | sac.b, sac.e 88 | ``` 89 | 90 | ``` 91 | (-120.0, -21.0, -96.5) 92 | ``` 93 | 94 | ### Quick header viewing 95 | 96 | Print non-null header values. 97 | ```python 98 | sac = SACTrace() 99 | print sac 100 | ``` 101 | 102 | ``` 103 | Reference Time = 01/01/2000 (001) 00:00:00.000000 104 | iztype IB: begin time 105 | b = 0.0 106 | cmpaz = 0.0 107 | cmpinc = 0.0 108 | delta = 1.0 109 | e = 99.0 110 | iftype = itime 111 | internal0 = 2.0 112 | iztype = ib 113 | kcmpnm = Z 114 | lcalda = False 115 | leven = True 116 | lovrok = True 117 | lpspol = True 118 | npts = 100 119 | nvhdr = 6 120 | nzhour = 0 121 | nzjday = 1 122 | nzmin = 0 123 | nzmsec = 0 124 | nzsec = 0 125 | nzyear = 2000 126 | ``` 127 | 128 | Print relative time header values. 129 | ```python 130 | sac.lh('picks') 131 | ``` 132 | 133 | ``` 134 | Reference Time = 01/01/1970 (001) 00:00:00.000000 135 | iztype IB: begin time 136 | a = None 137 | b = 0.0 138 | e = 0.0 139 | f = None 140 | o = None 141 | t0 = None 142 | t1 = None 143 | t2 = None 144 | t3 = None 145 | t4 = None 146 | t5 = None 147 | t6 = None 148 | t7 = None 149 | t8 = None 150 | t9 = None 151 | ``` 152 | 153 | ### Header values as attributes 154 | 155 | Great for interactive use, with (ipython) tab-completion... 156 | ```python 157 | sac. 158 | ``` 159 | 160 | ``` 161 | sac.a sac.kevnm sac.nzsec 162 | sac.az sac.kf sac.nzyear 163 | sac.b sac.khole sac.o 164 | sac.baz sac.kinst sac.odelta 165 | sac.byteorder sac.knetwk sac.read 166 | sac.cmpaz sac.ko sac.reftime 167 | sac.cmpinc sac.kstnm sac.scale 168 | sac.copy sac.kt0 sac.stdp 169 | sac.data sac.kt1 sac.stel 170 | sac.delta sac.kt2 sac.stla 171 | sac.depmax sac.kt3 sac.stlo 172 | sac.depmen sac.kt4 sac.t0 173 | sac.depmin sac.kt5 sac.t1 174 | sac.dist sac.kt6 sac.t2 175 | sac.e sac.kt7 sac.t3 176 | sac.evdp sac.kt8 sac.t4 177 | sac.evla sac.kt9 sac.t5 178 | sac.evlo sac.kuser0 sac.t6 179 | sac.f sac.kuser1 sac.t7 180 | sac.from_obspy_trace sac.kuser2 sac.t8 181 | sac.gcarc sac.lcalda sac.t9 182 | sac.idep sac.leven sac.to_obspy_trace 183 | sac.ievreg sac.lh sac.unused23 184 | sac.ievtyp sac.listhdr sac.user0 185 | sac.iftype sac.lovrok sac.user1 186 | sac.iinst sac.lpspol sac.user2 187 | sac.imagsrc sac.mag sac.user3 188 | sac.imagtyp sac.nevid sac.user4 189 | sac.internal0 sac.norid sac.user5 190 | sac.iqual sac.npts sac.user6 191 | sac.istreg sac.nvhdr sac.user7 192 | sac.isynth sac.nwfid sac.user8 193 | sac.iztype sac.nzhour sac.user9 194 | sac.ka sac.nzjday sac.validate 195 | sac.kcmpnm sac.nzmin sac.write 196 | sac.kdatrd sac.nzmsec 197 | ``` 198 | 199 | ...and documentation! 200 | ```python 201 | sac.iztype? 202 | ``` 203 | 204 | ``` 205 | Type: property 206 | String form: 207 | Docstring: 208 | I Reference time equivalence: 209 | * IUNKN (5): Unknown 210 | * IB (9): Begin time 211 | * IDAY (10): Midnight of reference GMT day 212 | * IO (11): Event origin time 213 | * IA (12): First arrival time 214 | * ITn (13-22): User defined time pick n, n=0,9 215 | ``` 216 | 217 | ### Convert to/from ObsPy Traces 218 | 219 | ```python 220 | from obspy import read 221 | tr = read()[0] 222 | print tr.stats 223 | ``` 224 | ``` 225 | network: BW 226 | station: RJOB 227 | location: 228 | channel: EHZ 229 | starttime: 2009-08-24T00:20:03.000000Z 230 | endtime: 2009-08-24T00:20:32.990000Z 231 | sampling_rate: 100.0 232 | delta: 0.01 233 | npts: 3000 234 | calib: 1.0 235 | back_azimuth: 100.0 236 | inclination: 30.0 237 | ``` 238 | 239 | ```python 240 | sac = SACTrace.from_obspy_trace(tr) 241 | print sac 242 | ``` 243 | 244 | ``` 245 | Reference Time = 08/24/2009 (236) 00:20:03.000000 246 | iztype IB: begin time 247 | b = 0.0 248 | cmpaz = 0.0 249 | cmpinc = 0.0 250 | delta = 0.00999999977648 251 | depmax = 1293.77099609 252 | depmen = -4.49556303024 253 | depmin = -1515.81311035 254 | e = 29.9899993297 255 | iftype = itime 256 | internal0 = 2.0 257 | iztype = ib 258 | kcmpnm = EHZ 259 | knetwk = BW 260 | kstnm = RJOB 261 | lcalda = False 262 | leven = True 263 | lovrok = True 264 | lpspol = True 265 | npts = 3000 266 | nvhdr = 6 267 | nzhour = 0 268 | nzjday = 236 269 | nzmin = 20 270 | nzmsec = 0 271 | nzsec = 3 272 | nzyear = 2009 273 | scale = 1.0 274 | ``` 275 | 276 | ```python 277 | tr2 = sac.to_obspy_trace() 278 | print tr2.stats 279 | ``` 280 | 281 | ``` 282 | network: BW 283 | station: RJOB 284 | location: 285 | channel: EHZ 286 | starttime: 2009-08-24T00:20:03.000000Z 287 | endtime: 2009-08-24T00:20:32.990000Z 288 | sampling_rate: 100.0 289 | delta: 0.01 290 | npts: 3000 291 | calib: 1.0 292 | sac: AttribDict({'cmpaz': 0.0, 'nzyear': 2009, 'nzjday': 236, 293 | 'iztype': 9, 'evla': 0.0, 'nzhour': 0, 'lcalda': 0, 'evlo': 0.0, 294 | 'scale': 1.0, 'nvhdr': 6, 'depmin': -1515.8131, 'kcmpnm': 'EHZ', 295 | 'nzsec': 3, 'internal0': 2.0, 'depmen': -4.495563, 'cmpinc': 0.0, 296 | 'depmax': 1293.771, 'iftype': 1, 'delta': 0.0099999998, 'nzmsec': 297 | 0, 'lpspol': 1, 'b': 0.0, 'e': 29.99, 'leven': 1, 'kstnm': 'RJOB', 298 | 'nzmin': 20, 'lovrok': 1, 'npts': 3000, 'knetwk': 'BW'}) 299 | ``` 300 | -------------------------------------------------------------------------------- /pysac/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | SAC module helper functions and data. 4 | 5 | """ 6 | from __future__ import (absolute_import, division, print_function, 7 | unicode_literals) 8 | from future.builtins import * # NOQA 9 | 10 | import sys 11 | import warnings 12 | 13 | import numpy as np 14 | 15 | from obspy import UTCDateTime 16 | from obspy.core import Stats 17 | 18 | from . import header as HD # noqa 19 | 20 | # ------------- DATA ---------------------------------------------------------- 21 | TWO_DIGIT_YEAR_MSG = ("SAC file with 2-digit year header field encountered. " 22 | "This is not supported by the SAC file format standard. " 23 | "Prepending '19'.") 24 | 25 | 26 | # ------------- SAC-SPECIFIC EXCEPTIONS --------------------------------------- 27 | class SacError(Exception): 28 | """ 29 | Raised if the SAC file is corrupt or if necessary information 30 | in the SAC file is missing. 31 | """ 32 | pass 33 | 34 | 35 | class SacIOError(SacError, IOError): 36 | """ 37 | Raised if the given SAC file can't be read. 38 | """ 39 | pass 40 | 41 | 42 | class SacInvalidContentError(SacError): 43 | """ 44 | Raised if headers and/or data are not valid. 45 | """ 46 | pass 47 | 48 | 49 | class SacHeaderError(SacError): 50 | """ 51 | Raised if header has issues. 52 | """ 53 | pass 54 | 55 | 56 | class SacHeaderTimeError(SacHeaderError, ValueError): 57 | """ 58 | Raised if header has invalid "nz" times. 59 | """ 60 | pass 61 | 62 | 63 | # ------------- VALIDITY CHECKS ----------------------------------------------- 64 | def is_valid_enum_str(hdr, name): 65 | # is this a valid string name for this hdr 66 | # assume that, if a value isn't in HD.ACCEPTED_VALS, it's not valid 67 | if hdr in HD.ACCEPTED_VALS: 68 | tf = name in HD.ACCEPTED_VALS[hdr] 69 | else: 70 | tf = False 71 | return tf 72 | 73 | 74 | def is_valid_enum_int(hdr, val, allow_null=True): 75 | # is this a valid integer for this hdr. 76 | if hdr in HD.ACCEPTED_VALS: 77 | accep = [HD.ENUM_VALS[nm] for nm in HD.ACCEPTED_VALS[hdr]] 78 | if allow_null: 79 | accep += [HD.INULL] 80 | tf = val in accep 81 | else: 82 | tf = False 83 | return tf 84 | 85 | 86 | # ------------- GENERAL ------------------------------------------------------- 87 | def _convert_enum(header, converter, accep): 88 | # header : dict, SAC header 89 | # converter : dict, {source value: target value} 90 | # accep : dict, {header name: acceptable value list} 91 | 92 | # TODO: use functools.partial/wraps? 93 | for hdr, val in header.items(): 94 | if hdr in HD.ACCEPTED_VALS: 95 | if val in accep[hdr]: 96 | header[hdr] = converter[val] 97 | else: 98 | msg = 'Unrecognized enumerated value "{}" for header "{}"' 99 | raise ValueError(msg.format(val, hdr)) 100 | 101 | return header 102 | 103 | 104 | def enum_string_to_int(header): 105 | """Convert enumerated string values in header dictionary to int values.""" 106 | out = _convert_enum(header, converter=HD.ENUM_VALS, accep=HD.ACCEPTED_VALS) 107 | return out 108 | 109 | 110 | def enum_int_to_string(header): 111 | """Convert enumerated int values in header dictionary to string values.""" 112 | out = _convert_enum(header, converter=HD.ENUM_NAMES, accep=HD.ACCEPTED_INT) 113 | return out 114 | 115 | 116 | def byteswap(*arrays): 117 | """ 118 | Swapping of bytes for provided arrays. 119 | 120 | Notes 121 | ----- 122 | arr.newbyteorder('S') swaps dtype interpretation, but not bytes in memory 123 | arr.byteswap() swaps bytes in memory, but not dtype interpretation 124 | arr.byteswap(True).newbyteorder('S') completely swaps both 125 | 126 | References 127 | ---------- 128 | https://docs.scipy.org/doc/numpy/user/basics.byteswapping.html 129 | 130 | """ 131 | return [arr.newbyteorder('S') for arr in arrays] 132 | 133 | 134 | def is_same_byteorder(bo1, bo2): 135 | """ 136 | Deal with all the ways to compare byte order string representations. 137 | 138 | :param bo1: Byte order string. Can be one of {'l', 'little', 'L', '<', 139 | 'b', 'big', 'B', '>', 'n', 'native','N', '='} 140 | :type bo1: str 141 | :param bo2: Byte order string. Can be one of {'l', 'little', 'L', '<', 142 | 'b', 'big', 'B', '>', 'n', 'native','N', '='} 143 | :type bo1: str 144 | 145 | :rtype: bool 146 | :return: True of same byte order. 147 | 148 | """ 149 | # TODO: extend this as is_same_byteorder(*byteorders) using itertools 150 | be = ('b', 'big', '>') 151 | le = ('l', 'little', '<') 152 | ne = ('n', 'native', '=') 153 | ok = be + le + ne 154 | 155 | if (bo1.lower() not in ok) or (bo2.lower() not in ok): 156 | raise ValueError("Unrecognized byte order string.") 157 | 158 | # make native decide what it is 159 | bo1 = sys.byteorder if bo1.lower() in ne else bo1 160 | bo2 = sys.byteorder if bo2.lower() in ne else bo2 161 | 162 | return (bo1.lower() in le) == (bo2.lower() in le) 163 | 164 | 165 | def _clean_str(value, strip_whitespace=True): 166 | """ 167 | Remove null values and whitespace, return a str 168 | 169 | This fn is used in two places: in SACTrace.read, to sanitize strings for 170 | SACTrace, and in sac_to_obspy_header, to sanitize strings for making a 171 | Trace that the user may have manually added. 172 | """ 173 | try: 174 | value = value.decode('ASCII', 'replace') 175 | except AttributeError: 176 | pass 177 | 178 | null_term = value.find('\x00') 179 | if null_term >= 0: 180 | value = value[:null_term] + " " * len(value[null_term:]) 181 | 182 | if strip_whitespace: 183 | value = value.strip() 184 | 185 | return value 186 | 187 | 188 | # TODO: do this in SACTrace? 189 | def sac_to_obspy_header(sacheader): 190 | """ 191 | Make an ObsPy Stats header dictionary from a SAC header dictionary. 192 | 193 | :param sacheader: SAC header dictionary. 194 | :type sacheader: dict 195 | 196 | :rtype: :class:`~obspy.core.Stats` 197 | :return: Filled ObsPy Stats header. 198 | 199 | """ 200 | 201 | # 1. get required sac header values 202 | try: 203 | npts = sacheader['npts'] 204 | delta = sacheader['delta'] 205 | except KeyError: 206 | msg = "Incomplete SAC header information to build an ObsPy header." 207 | raise KeyError(msg) 208 | 209 | assert npts != HD.INULL 210 | assert delta != HD.FNULL 211 | # 212 | # 2. get time 213 | try: 214 | reftime = get_sac_reftime(sacheader) 215 | except (SacError, ValueError, TypeError): 216 | # ObsPy doesn't require a valid reftime 217 | reftime = UTCDateTime(0.0) 218 | 219 | b = sacheader.get('b', HD.FNULL) 220 | # 221 | # 3. get optional sac header values 222 | calib = sacheader.get('scale', HD.FNULL) 223 | kcmpnm = sacheader.get('kcmpnm', HD.SNULL) 224 | kstnm = sacheader.get('kstnm', HD.SNULL) 225 | knetwk = sacheader.get('knetwk', HD.SNULL) 226 | khole = sacheader.get('khole', HD.SNULL) 227 | # 228 | # 4. deal with null values 229 | b = b if (b != HD.FNULL) else 0.0 230 | calib = calib if (calib != HD.FNULL) else 1.0 231 | kcmpnm = kcmpnm if (kcmpnm != HD.SNULL) else '' 232 | kstnm = kstnm if (kstnm != HD.SNULL) else '' 233 | knetwk = knetwk if (knetwk != HD.SNULL) else '' 234 | khole = khole if (khole != HD.SNULL) else '' 235 | # 236 | # 5. transform to obspy values 237 | # nothing is null 238 | stats = {} 239 | stats['npts'] = npts 240 | stats['sampling_rate'] = np.float32(1.) / np.float32(delta) 241 | stats['network'] = _clean_str(knetwk) 242 | stats['station'] = _clean_str(kstnm) 243 | stats['channel'] = _clean_str(kcmpnm) 244 | stats['location'] = _clean_str(khole) 245 | stats['calib'] = calib 246 | 247 | # store _all_ provided SAC header values 248 | stats['sac'] = sacheader.copy() 249 | 250 | # get first sample absolute time as UTCDateTime 251 | # always add the begin time (if it's defined) to get the given 252 | # SAC reference time, no matter which iztype is given 253 | # b may be non-zero, even for iztype 'ib', especially if it was used to 254 | # store microseconds from obspy_to_sac_header 255 | stats['starttime'] = UTCDateTime(reftime) + b 256 | 257 | return Stats(stats) 258 | 259 | 260 | def split_microseconds(microseconds): 261 | # Returns milliseconds and remainder microseconds 262 | milliseconds = microseconds // 1000 263 | microseconds = (microseconds - milliseconds * 1000) 264 | 265 | return milliseconds, microseconds 266 | 267 | 268 | def utcdatetime_to_sac_nztimes(utcdt): 269 | # Returns a dict of integer nz-times and remainder microseconds 270 | nztimes = {} 271 | nztimes['nzyear'] = utcdt.year 272 | nztimes['nzjday'] = utcdt.julday 273 | nztimes['nzhour'] = utcdt.hour 274 | nztimes['nzmin'] = utcdt.minute 275 | nztimes['nzsec'] = utcdt.second 276 | # nz times don't have enough precision, so push microseconds into b, 277 | # using integer arithmetic 278 | millisecond, microsecond = split_microseconds(utcdt.microsecond) 279 | nztimes['nzmsec'] = millisecond 280 | 281 | return nztimes, microsecond 282 | 283 | 284 | def obspy_to_sac_header(stats, keep_sac_header=True): 285 | """ 286 | Merge a primary with a secondary header, reconciling some differences. 287 | 288 | :param stats: Filled ObsPy Stats header 289 | :type stats: dict or :class:`~obspy.core.Stats` 290 | :param keep_sac_header: If keep_sac_header is True, old stats.sac 291 | header values are kept, and a minimal set of values are updated from 292 | the stats dictionary according to these guidelines: 293 | * npts, delta always come from stats 294 | * If a valid old reftime is found, the new b and e will be made 295 | and properly referenced to it. All other old SAC headers are simply 296 | carried along. 297 | * If the old SAC reftime is invalid and relative time headers are set, 298 | a SacHeaderError exception will be raised. 299 | * If the old SAC reftime is invalid, no relative time headers are set, 300 | and "b" is set, "e" is updated from stats and other old SAC headers 301 | are carried along. 302 | * If the old SAC reftime is invalid, no relative time headers are set, 303 | and "b" is not set, the reftime will be set from stats.starttime 304 | (with micro/milliseconds precision adjustments) and "b" and "e" are 305 | set accordingly. 306 | * If 'kstnm', 'knetwk', 'kcmpnm', or 'khole' are not set or differ 307 | from Stats values 'station', 'network', 'channel', or 'location', 308 | they are taken from the Stats values. 309 | If keep_sac_header is False, a new SAC header is constructed from only 310 | information found in the Stats dictionary, with some other default 311 | values introduced. It will be an iztype 9 ("ib") file, with small 312 | reference time adjustments for micro/milliseconds precision issues. 313 | SAC headers nvhdr, level, lovrok, and iftype are always produced. 314 | :type keep_sac_header: bool 315 | :rtype merged: dict 316 | :return: SAC header 317 | 318 | """ 319 | header = {} 320 | oldsac = stats.get('sac', {}) 321 | 322 | if keep_sac_header and oldsac: 323 | header.update(oldsac) 324 | 325 | try: 326 | reftime = get_sac_reftime(header) 327 | except SacHeaderTimeError: 328 | reftime = None 329 | 330 | relhdrs = [hdr for hdr in HD.RELHDRS 331 | if header.get(hdr) not in (None, HD.SNULL)] 332 | 333 | if reftime: 334 | # Set current 'b' relative to the old reftime. 335 | b = stats['starttime'] - reftime 336 | else: 337 | # Invalid reference time. Relative times like 'b' cannot be 338 | # unambiguously referenced to stats.starttime. 339 | if 'b' in relhdrs: 340 | # Assume no trimming/expanding of the Trace occurred relative 341 | # to the old 'b', and just use the old 'b' value. 342 | b = header['b'] 343 | else: 344 | # Assume it's an iztype=ib (9) type file. Also set iztype? 345 | b = 0 346 | 347 | # Set the stats.starttime as the reftime and set 'b' and 'e'. 348 | # ObsPy issue 1204 349 | reftime = stats['starttime'] - b 350 | nztimes, microsecond = utcdatetime_to_sac_nztimes(reftime) 351 | header.update(nztimes) 352 | b += (microsecond * 1e-6) 353 | 354 | header['b'] = b 355 | header['e'] = b + (stats['endtime'] - stats['starttime']) 356 | 357 | # Merge some values from stats if they're missing in the SAC header 358 | # ObsPy issues 1204, 1457 359 | # XXX: If Stats values are empty/"" and SAC header values are real, 360 | # this will replace the real SAC values with SAC null values. 361 | for sachdr, statshdr in [('kstnm', 'station'), ('knetwk', 'network'), 362 | ('kcmpnm', 'channel'), ('khole', 'location')]: 363 | if (header.get(sachdr) in (None, HD.SNULL)) or \ 364 | (header.get(sachdr).strip() != stats[statshdr]): 365 | header[sachdr] = stats[statshdr] or HD.SNULL 366 | else: 367 | # SAC header from Stats only. 368 | 369 | # Here, set headers from Stats that would otherwise depend on the old 370 | # SAC header 371 | header['iztype'] = 9 372 | starttime = stats['starttime'] 373 | # nz times don't have enough precision, so push microseconds into b, 374 | # using integer arithmetic 375 | nztimes, microsecond = utcdatetime_to_sac_nztimes(starttime) 376 | header.update(nztimes) 377 | 378 | header['b'] = microsecond * 1e-6 379 | 380 | # we now have correct b, npts, delta, and nz times 381 | header['e'] = header['b'] + (stats['npts'] - 1) * stats['delta'] 382 | 383 | header['scale'] = stats.get('calib', HD.FNULL) 384 | 385 | # NOTE: overwrites existing SAC headers 386 | # nulls for these are '', which stats.get(hdr, HD.SNULL) won't catch 387 | header['kcmpnm'] = stats['channel'] if stats['channel'] else HD.SNULL 388 | header['kstnm'] = stats['station'] if stats['station'] else HD.SNULL 389 | header['knetwk'] = stats['network'] if stats['network'] else HD.SNULL 390 | header['khole'] = stats['location'] if stats['location'] else HD.SNULL 391 | 392 | header['lpspol'] = True 393 | header['lcalda'] = False 394 | 395 | # ObsPy issue 1204 396 | header['nvhdr'] = 6 397 | header['leven'] = 1 398 | header['lovrok'] = 1 399 | header['iftype'] = 1 400 | 401 | # ObsPy issue #1317 402 | header['npts'] = stats['npts'] 403 | header['delta'] = stats['delta'] 404 | 405 | return header 406 | 407 | 408 | def get_sac_reftime(header): 409 | """ 410 | Get SAC header reference time as a UTCDateTime instance from a SAC header 411 | dictionary. 412 | 413 | Builds the reference time from SAC "nz" time fields. Raises 414 | :class:`SacHeaderTimeError` if any time fields are null. 415 | 416 | :param header: SAC header 417 | :type header: dict 418 | 419 | :rtype: :class:`~obspy.core.UTCDateTime` 420 | :returns: SAC reference time. 421 | 422 | """ 423 | # NOTE: epoch seconds can be got by: 424 | # (reftime - datetime.datetime(1970,1,1)).total_seconds() 425 | try: 426 | yr = header['nzyear'] 427 | nzjday = header['nzjday'] 428 | nzhour = header['nzhour'] 429 | nzmin = header['nzmin'] 430 | nzsec = header['nzsec'] 431 | nzmsec = header['nzmsec'] 432 | except KeyError as e: 433 | # header doesn't have all the keys 434 | msg = "Not enough time information: {}".format(e) 435 | raise SacHeaderTimeError(msg) 436 | 437 | if 0 <= yr <= 99: 438 | warnings.warn(TWO_DIGIT_YEAR_MSG) 439 | yr += 1900 440 | 441 | try: 442 | reftime = UTCDateTime(year=yr, julday=nzjday, hour=nzhour, 443 | minute=nzmin, second=nzsec, 444 | microsecond=nzmsec * 1000) 445 | except (ValueError, TypeError): 446 | msg = "Invalid time headers. May contain null values." 447 | raise SacHeaderTimeError(msg) 448 | 449 | return reftime 450 | -------------------------------------------------------------------------------- /pysac/header.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from future.builtins import * # NOQA 5 | 6 | MODULE_DOCSTRING = """ 7 | SAC header specification, including documentation. 8 | 9 | Header names, order, types, and nulls, as well as allowed enumerated values, 10 | are specified here. Header name strings, and their array order are contained 11 | in separate float, int, and string tuples. Enumerated values, and their 12 | allowed string and integer values, are in dictionaries. Header value 13 | documentation is in a dictionary, for reuse throughout the package. 14 | 15 | """ 16 | 17 | # header documentation is large and used in several places, so we just write 18 | # it once here and distributed it as needed. 19 | DOC = { 20 | 'npts': 'N Number of points per data component.', 21 | 'nvhdr': '''N Header version number. Current value is the integer 6. 22 | Older version data (NVHDR < 6) are automatically updated 23 | when read into sac.''', 24 | 'b': 'F Beginning value of the independent variable.', 25 | 'e': 'F Ending value of the independent variable.', 26 | 'iftype': '''I Type of file: 27 | 28 | * ITIME {Time series file} 29 | * IRLIM {Spectral file---real and imaginary} 30 | * IAMPH {Spectral file---amplitude and phase} 31 | * IXY {General x versus y data} 32 | * IXYZ {General XYZ (3-D) file}''', 33 | 'leven': 'L TRUE if data is evenly spaced.', 34 | 'delta': 'F Increment between evenly spaced samples (nominal value).', 35 | 'odelta': 'F Observed increment if different from nominal value.', 36 | 'idep': '''I Type of dependent variable: 37 | 38 | * IUNKN (Unknown) 39 | * IDISP (Displacement in nm) 40 | * IVEL (Velocity in nm/sec) 41 | * IVOLTS (Velocity in volts) 42 | * IACC (Acceleration in nm/sec/sec)''', 43 | 'scale': 'F Multiplying scale factor for dependent variable ', 44 | 'depmin': 'F Minimum value of dependent variable.', 45 | 'depmax': 'F Maximum value of dependent variable.', 46 | 'depmen': 'F Mean value of dependent variable.', 47 | 'nzyear': 'N GMT year corresponding to reference time in file.', 48 | 'nzjday': 'N GMT julian day.', 49 | 'nzhour': 'N GMT hour.', 50 | 'nzmin': 'N GMT minute.', 51 | 'nzsec': 'N GMT second.', 52 | 'nzmsec': 'N GMT millisecond.', 53 | 'iztype': '''I Reference time equivalence: 54 | 55 | * IUNKN (5): Unknown 56 | * IB (9): Begin time 57 | * IDAY (10): Midnight of reference GMT day 58 | * IO (11): Event origin time 59 | * IA (12): First arrival time 60 | * ITn (13-22): User defined time pick n, n=0,9''', 61 | 'o': 'F Event origin time (seconds relative to reference time.)', 62 | 'ko': 'K Event origin time identification.', 63 | 'a': 'F First arrival time (seconds relative to reference time.)', 64 | 'ka': 'K First arrival time identification.', 65 | 'f': 'F Fini/end of event time (seconds relative to reference time.)', 66 | 'kf': 'F Fini or end of event time identification.', 67 | 't0': 'F User defined time (seconds picks or markers relative to ' 68 | 'reference time).', 69 | 't1': 'F User defined time (seconds picks or markers relative to ' 70 | 'reference time).', 71 | 't2': 'F User defined time (seconds picks or markers relative to ' 72 | 'reference time).', 73 | 't3': 'F User defined time (seconds picks or markers relative to ' 74 | 'reference time).', 75 | 't4': 'F User defined time (seconds picks or markers relative to ' 76 | 'reference time).', 77 | 't5': 'F User defined time (seconds picks or markers relative to ' 78 | 'reference time).', 79 | 't6': 'F User defined time (seconds picks or markers relative to ' 80 | 'reference time).', 81 | 't7': 'F User defined time (seconds picks or markers relative to ' 82 | 'reference time).', 83 | 't8': 'F User defined time (seconds picks or markers relative to ' 84 | 'reference time).', 85 | 't9': 'F User defined time (seconds picks or markers relative to ' 86 | 'reference time).', 87 | 'kt0': 'F User defined time pick identification.', 88 | 'kt1': 'F User defined time pick identification.', 89 | 'kt2': 'F User defined time pick identification.', 90 | 'kt3': 'F User defined time pick identification.', 91 | 'kt4': 'F User defined time pick identification.', 92 | 'kt5': 'F User defined time pick identification.', 93 | 'kt6': 'F User defined time pick identification.', 94 | 'kt7': 'F User defined time pick identification.', 95 | 'kt8': 'F User defined time pick identification.', 96 | 'kt9': 'F User defined time pick identification.', 97 | 'kinst': 'K Generic name of recording instrument', 98 | 'iinst': 'I Type of recording instrument.', 99 | 'knetwk': 'K Name of seismic network.', 100 | 'kstnm': 'K Station name.', 101 | 'istreg': 'I Station geographic region.', 102 | 'stla': 'F Station latitude (degrees, north positive)', 103 | 'stlo': 'F Station longitude (degrees, east positive).', 104 | 'stel': 'F Station elevation (meters).', 105 | 'stdp': 'F Station depth below surface (meters).', 106 | 'cmpaz': 'F Component azimuth (degrees, clockwise from north).', 107 | 'cmpinc': 'F Component incident angle (degrees, from vertical).', 108 | 'kcmpnm': 'K Component name.', 109 | 'lpspol': 'L TRUE if station components have a positive polarity ' 110 | '(left-hand rule).', 111 | 'kevnm': 'K Event name.', 112 | 'ievreg': 'I Event geographic region.', 113 | 'evla': 'F Event latitude (degrees north positive).', 114 | 'evlo': 'F Event longitude (degrees east positive).', 115 | 'evel': 'F Event elevation (meters).', 116 | 'evdp': 'F Event depth below surface (meters).', 117 | 'mag': 'F Event magnitude.', 118 | 'imagtyp': '''I Magnitude type: 119 | 120 | * IMB (52): Bodywave Magnitude 121 | * IMS (53): Surfacewave Magnitude 122 | * IML (54): Local Magnitude 123 | * IMW (55): Moment Magnitude 124 | * IMD (56): Duration Magnitude 125 | * IMX (57): User Defined Magnitude''', 126 | 'imagsrc': '''I Source of magnitude information: 127 | 128 | * INEIC (National Earthquake Information Center) 129 | * IPDE (Preliminary Determination of Epicenter) 130 | * IISC (International Seismological Centre) 131 | * IREB (Reviewed Event Bulletin) 132 | * IUSGS (US Geological Survey) 133 | * IBRK (UC Berkeley) 134 | * ICALTECH (California Institute of Technology) 135 | * ILLNL (Lawrence Livermore National Laboratory) 136 | * IEVLOC (Event Location (computer program) ) 137 | * IJSOP (Joint Seismic Observation Program) 138 | * IUSER (The individual using SAC2000) 139 | * IUNKNOWN (unknown)''', 140 | 'ievtyp': '''I Type of event: 141 | 142 | * IUNKN (Unknown) 143 | * INUCL (Nuclear event) 144 | * IPREN (Nuclear pre-shot event) 145 | * IPOSTN (Nuclear post-shot event) 146 | * IQUAKE (Earthquake) 147 | * IPREQ (Foreshock) 148 | * IPOSTQ (Aftershock) 149 | * ICHEM (Chemical explosion) 150 | * IQB (Quarry or mine blast confirmed by quarry) 151 | * IQB1 (Quarry/mine blast with designed shot 152 | info-ripple fired) 153 | * IQB2 (Quarry/mine blast with observed shot 154 | info-ripple fired) 155 | * IQMT (Quarry/mining-induced events: 156 | tremors and rockbursts) 157 | * IEQ (Earthquake) 158 | * IEQ1 (Earthquakes in a swarm or aftershock 159 | sequence) 160 | * IEQ2 (Felt earthquake) 161 | * IME (Marine explosion) 162 | * IEX (Other explosion) 163 | * INU (Nuclear explosion) 164 | * INC (Nuclear cavity collapse) 165 | * IO_ (Other source of known origin) 166 | * IR (Regional event of unknown origin) 167 | * IT (Teleseismic event of unknown origin) 168 | * IU (Undetermined or conflicting information) 169 | * IOTHER (Other)''', 170 | 'nevid': 'N Event ID (CSS 3.0)', 171 | 'norid': 'N Origin ID (CSS 3.0)', 172 | 'nwfid': 'N Waveform ID (CSS 3.0)', 173 | 'khole': 'k Hole identification if nuclear event.', 174 | 'dist': 'F Station to event distance (km).', 175 | 'az': 'F Event to station azimuth (degrees).', 176 | 'baz': 'F Station to event azimuth (degrees).', 177 | 'gcarc': 'F Station to event great circle arc length (degrees).', 178 | 'lcalda': 'L TRUE if DIST AZ BAZ and GCARC are to be calculated ' 179 | 'from st event coordinates.', 180 | 'iqual': '''N Quality of data, as integers. Enum values listed: 181 | 182 | * IGOOD (45) (Good data) 183 | * IGLCH (46) (Glitches) 184 | * IDROP (47) (Dropouts) 185 | * ILOWSN (48) (Low signal to noise ratio) 186 | * IOTHER (44) (Other)''', 187 | 'isynth': '''I Synthetic data flag: 188 | 189 | * IRLDTA (Real data) 190 | * ????? (Flags for various synthetic seismogram codes)''', 191 | 'user0': 'F User defined variable storage area 0.', 192 | 'user1': 'F User defined variable storage area 1.', 193 | 'user2': 'F User defined variable storage area 2.', 194 | 'user3': 'F User defined variable storage area 3.', 195 | 'user4': 'F User defined variable storage area 4.', 196 | 'user5': 'F User defined variable storage area 5.', 197 | 'user6': 'F User defined variable storage area 6.', 198 | 'user7': 'F User defined variable storage area 7.', 199 | 'user8': 'F User defined variable storage area 8.', 200 | 'user9': 'F User defined variable storage area 9.', 201 | 'kuser0': 'K User defined variable storage area 0.', 202 | 'kuser1': 'K User defined variable storage area 1.', 203 | 'kuser2': 'K User defined variable storage area 2.', 204 | 'lovrok': 'L TRUE if it is okay to overwrite this file on disk.' 205 | } 206 | 207 | # because readable 208 | _hdritems = sorted(DOC.items()) 209 | _hdrfmt = "{:10.10s} = {}" 210 | HDR_DESC = '\n'.join([_hdrfmt.format(_hdr, _doc) for _hdr, _doc in _hdritems]) 211 | 212 | HEADER_DOCSTRING = """ 213 | ============ ==== =============================================== 214 | Field Name Type Description 215 | ============ ==== =============================================== 216 | """ + HDR_DESC + \ 217 | "\n============ ==== ===============================================" 218 | 219 | # Module documentation string 220 | __doc__ = MODULE_DOCSTRING + HEADER_DOCSTRING 221 | 222 | # ------------ NULL VALUES ---------------------------------------------------- 223 | FNULL = -12345.0 224 | INULL = -12345 225 | SNULL = '-12345 ' 226 | 227 | # ------------ HEADER NAMES, TYPES, ARRAY POSITIONS --------------------------- 228 | # these are useful b/c they can be used forwards or backwards, like: 229 | # FLOADHDRS.index('az') is 40, and FLOATHDRS[40] is 'az'. 230 | FLOATHDRS = ('delta', 'depmin', 'depmax', 'scale', 'odelta', 'b', 'e', 'o', 231 | 'a', 'internal0', 't0', 't1', 't2', 't3', 't4', 't5', 't6', 't7', 232 | 't8', 't9', 'f', 'resp0', 'resp1', 'resp2', 'resp3', 'resp4', 233 | 'resp5', 'resp6', 'resp7', 'resp8', 'resp9', 'stla', 'stlo', 234 | 'stel', 'stdp', 'evla', 'evlo', 'evel', 'evdp', 'mag', 'user0', 235 | 'user1', 'user2', 'user3', 'user4', 'user5', 'user6', 'user7', 236 | 'user8', 'user9', 'dist', 'az', 'baz', 'gcarc', 'internal1', 237 | 'internal2', 'depmen', 'cmpaz', 'cmpinc', 'xminimum', 'xmaximum', 238 | 'yminimum', 'ymaximum', 'unused6', 'unused7', 'unused8', 239 | 'unused9', 'unused10', 'unused11', 'unused12') 240 | 241 | INTHDRS = ('nzyear', 'nzjday', 'nzhour', 'nzmin', 'nzsec', 'nzmsec', 'nvhdr', 242 | 'norid', 'nevid', 'npts', 'internal3', 'nwfid', 'nxsize', 'nysize', 243 | 'unused13', 'iftype', 'idep', 'iztype', 'unused14', 'iinst', 244 | 'istreg', 'ievreg', 'ievtyp', 'iqual', 'isynth', 'imagtyp', 245 | 'imagsrc', 'unused15', 'unused16', 'unused17', 'unused18', 246 | 'unused19', 'unused20', 'unused21', 'unused22', 'leven', 'lpspol', 247 | 'lovrok', 'lcalda', 'unused23') 248 | 249 | STRHDRS = ('kstnm', 'kevnm', 'kevnm2', 'khole', 'ko', 'ka', 'kt0', 'kt1', 250 | 'kt2', 'kt3', 'kt4', 'kt5', 'kt6', 'kt7', 'kt8', 'kt9', 'kf', 251 | 'kuser0', 'kuser1', 'kuser2', 'kcmpnm', 'knetwk', 'kdatrd', 'kinst') 252 | 253 | # Headers that are in seconds relative to the reference "nz" times 254 | RELHDRS = ('b', 'e', 'a', 'o', 'f', 't0', 't1', 't2', 't3', 't4', 't5', 't6', 255 | 't7', 't8', 't9') 256 | 257 | """ 258 | NOTE: 259 | 260 | kevnm also has a kevnm2 b/c it takes two array spaces. 261 | 'kevnm' lookups must be caught and handled differently. This happens in the 262 | SACTrace string property getters/setters, .io.dict_to_header_arrays 263 | and .arrayio.header_arrays_to_dict. 264 | """ 265 | # NOTE: using namedtuples for header arrays sounds great, but they're immutable 266 | 267 | # TODO: make a dict converter between {'<', '>', '='} and {'little', 'big'} 268 | 269 | # ------------ ENUMERATED VALUES ---------------------------------------------- 270 | # These are stored in the header as integers. 271 | # Their names and values are given in the mapping below. 272 | # Some (many) are not used. 273 | # TODO: this is ugly; rename things a bit 274 | ENUM_VALS = {'itime': 1, 'irlim': 2, 'iamph': 3, 'ixy': 4, 'iunkn': 5, 275 | 'idisp': 6, 'ivel': 7, 'iacc': 8, 'ib': 9, 'iday': 10, 'io': 11, 276 | 'ia': 12, 'it0': 13, 'it1': 14, 'it2': 15, 'it3': 16, 'it4': 17, 277 | 'it5': 18, 'it6': 19, 'it7': 20, 'it8': 21, 'it9': 22, 278 | 'iradnv': 23, 'itannv': 24, 'iradev': 25, 'itanev': 26, 279 | 'inorth': 27, 'ieast': 28, 'ihorza': 29, 'idown': 30, 'iup': 31, 280 | 'illlbb': 32, 'iwwsn1': 33, 'iwwsn2': 34, 'ihglp': 35, 'isro': 36, 281 | 'inucl': 37, 'ipren': 38, 'ipostn': 39, 'iquake': 40, 'ipreq': 41, 282 | 'ipostq': 42, 'ichem': 43, 'iother': 44, 'igood': 45, 'iglch': 46, 283 | 'idrop': 47, 'ilowsn': 48, 'irldta': 49, 'ivolts': 50, 'imb': 52, 284 | 'ims': 53, 'iml': 54, 'imw': 55, 'imd': 56, 'imx': 57, 285 | 'ineic': 58, 'ipdeq': 59, 'ipdew': 60, 'ipde': 61, 'iisc': 62, 286 | 'ireb': 63, 'iusgs': 64, 'ibrk': 65, 'icaltech': 66, 'illnl': 67, 287 | 'ievloc': 68, 'ijsop': 69, 'iuser': 70, 'iunknown': 71, 'iqb': 72, 288 | 'iqb1': 73, 'iqb2': 74, 'iqbx': 75, 'iqmt': 76, 'ieq': 77, 289 | 'ieq1': 78, 'ieq2': 79, 'ime': 80, 'iex': 81, 'inu': 82, 290 | 'inc': 83, 'io_': 84, 'il': 85, 'ir': 86, 'it': 87, 'iu': 88, 291 | 'ieq3': 89, 'ieq0': 90, 'iex0': 91, 'iqc': 92, 'iqb0': 93, 292 | 'igey': 94, 'ilit': 95, 'imet': 96, 'iodor': 97, 'ios': 103} 293 | 294 | # reverse look-up: you have the number, want the string 295 | ENUM_NAMES = dict((v, k) for k, v in ENUM_VALS.items()) 296 | 297 | 298 | # accepted values, by header 299 | ACCEPTED_VALS = {'iftype': ['itime', 'irlim', 'iamph', 'ixy'], 300 | 'idep': ['iunkn', 'idisp', 'ivel', 'ivolts', 'iacc'], 301 | 'iztype': ['iunkn', 'ib', 'iday', 'io', 'ia', 'it0', 'it1', 302 | 'it2', 'it3', 'it4', 'it5', 'it6', 'it7', 'it8', 303 | 'it9'], 304 | 'imagtyp': ['imb', 'ims', 'iml', 'imw', 'imd', 'imx'], 305 | 'imagsrc': ['ineic', 'ipde', 'iisc', 'ireb', 'iusgs', 'ipdeq', 306 | 'ibrk', 'icaltech', 'illnl', 'ievloc', 'ijsop', 307 | 'iuser', 'iunknown'], 308 | 'ievtyp': ['iunkn', 'inucl', 'ipren', 'ipostn', 'iquake', 309 | 'ipreq', 'ipostq', 'ichem', 'iqb', 'iqb1', 'iqb2', 310 | 'iqbx', 'iqmt', 'ieq', 'ieq1', 'ieq2', 'ime', 311 | 'iex', 'inu', 'inc', 'io_', 'il', 'ir', 'it', 'iu', 312 | 'iother'], 313 | 'iqual': ['igood', 'iglch', 'idrop', 'ilowsn', 'iother'], 314 | 'isynth': ['irldta']} 315 | 316 | ACCEPTED_INT = ACCEPTED_VALS.copy() 317 | for _hdr in ACCEPTED_INT: 318 | ACCEPTED_INT[_hdr] = [ENUM_VALS[_ival] for _ival in ACCEPTED_VALS[_hdr]] 319 | -------------------------------------------------------------------------------- /pysac/arrayio.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Low-level array interface to the SAC file format. 4 | 5 | Functions in this module work directly with NumPy arrays that mirror the SAC 6 | format. The 'primitives' in this module are the float, int, and string header 7 | arrays, the float data array, and a header dictionary. Convenience functions 8 | are provided to convert between header arrays and more user-friendly 9 | dictionaries. 10 | 11 | These read/write routines are very literal; there is almost no value or type 12 | checking, except for byteorder and header/data array length. File- and array- 13 | based checking routines are provided for additional checks where desired. 14 | 15 | """ 16 | from __future__ import (absolute_import, division, print_function, 17 | unicode_literals) 18 | from future.utils import native_str 19 | from future.builtins import * # NOQA 20 | 21 | import os 22 | import sys 23 | import warnings 24 | 25 | import numpy as np 26 | 27 | from obspy.core.compatibility import from_buffer 28 | from obspy import UTCDateTime 29 | 30 | from . import header as HD # noqa 31 | from .util import SacIOError, SacInvalidContentError 32 | from .util import is_valid_enum_int 33 | 34 | 35 | def init_header_arrays(arrays=('float', 'int', 'str'), byteorder='='): 36 | """ 37 | Initialize arbitrary header arrays. 38 | 39 | :param arrays: Specify which arrays to initialize and the desired order. 40 | If omitted, returned arrays are ('float', 'int', 'str'), in that order. 41 | :type arrays: Tuple of strings {'float', 'int', 'str'} 42 | :param byteorder: Desired byte order of initialized arrays 43 | (little, native, big). 44 | :type byteorder: str {'<', '=', '>'} 45 | 46 | :rtype: list of :class:`~numpy.ndarray` instances 47 | :returns: The desired SAC header arrays. 48 | 49 | """ 50 | out = [] 51 | for itype in arrays: 52 | if itype == 'float': 53 | # null float header array 54 | hf = np.empty(70, dtype=native_str(byteorder + 'f4')) 55 | hf.fill(HD.FNULL) 56 | out.append(hf) 57 | elif itype == 'int': 58 | # null integer header array 59 | hi = np.empty(40, dtype=native_str(byteorder + 'i4')) 60 | hi.fill(HD.INULL) 61 | # set logicals to 0, not -1234whatever 62 | for i, hdr in enumerate(HD.INTHDRS): 63 | if hdr.startswith('l'): 64 | hi[i] = 0 65 | # TODO: make an init_header_array_values function that sets sane 66 | # initial values, including lcalda, nvhdr, leven, etc.. 67 | # calculate distances by default 68 | hi[HD.INTHDRS.index('lcalda')] = 1 69 | out.append(hi) 70 | elif itype == 'str': 71 | # null string header array 72 | hs = np.empty(24, dtype=native_str('|S8')) 73 | hs.fill(HD.SNULL) 74 | out.append(hs) 75 | else: 76 | raise ValueError("Unrecognized header array type {}".format(itype)) 77 | 78 | return out 79 | 80 | 81 | def read_sac(source, headonly=False, byteorder=None, checksize=False): 82 | """ 83 | Read a SAC binary file. 84 | 85 | :param source: Full path string for File-like object from a SAC binary file 86 | on disk. If it is an open File object, open 'rb'. 87 | :type source: str or file 88 | :param headonly: If headonly is True, only read the header arrays not the 89 | data array. 90 | :type headonly: bool 91 | :param byteorder: If omitted or None, automatic byte-order checking is 92 | done, starting with native order. If byteorder is specified and 93 | incorrect, a SacIOError is raised. 94 | :type byteorder: str {'little', 'big'}, optional 95 | :param checksize: If True, check that the theoretical file size from the 96 | header matches the size on disk. 97 | :type checksize: bool 98 | 99 | :return: The float, integer, and string header arrays, and data array, 100 | in that order. Data array will be None if headonly is True. 101 | :rtype: tuple of :class:`numpy.ndarray` 102 | 103 | :raises: :class:`ValueError` if unrecognized byte order. :class:`IOError` 104 | if file not found, incorrect specified byteorder, theoretical file size 105 | doesn't match header, or header arrays are incorrect length. 106 | 107 | """ 108 | # TODO: rewrite using "with" statement instead of open/close management. 109 | # check byte order, header array length, file size, npts == data length 110 | try: 111 | f = open(source, 'rb') 112 | is_file_name = True 113 | except TypeError: 114 | # source is already a file-like object 115 | f = source 116 | is_file_name = False 117 | 118 | is_byteorder_specified = byteorder is not None 119 | if not is_byteorder_specified: 120 | byteorder = sys.byteorder 121 | 122 | if byteorder == 'little': 123 | endian_str = '<' 124 | elif byteorder == 'big': 125 | endian_str = '>' 126 | else: 127 | raise ValueError("Unrecognized byteorder. Use {'little', 'big'}") 128 | 129 | # -------------------------------------------------------------- 130 | # READ HEADER 131 | # The sac header has 70 floats, 40 integers, then 192 bytes 132 | # in strings. Store them in array (and convert the char to a 133 | # list). That's a total of 632 bytes. 134 | # -------------------------------------------------------------- 135 | hf = from_buffer(f.read(4 * 70), dtype=native_str(endian_str + 'f4')) 136 | hi = from_buffer(f.read(4 * 40), dtype=native_str(endian_str + 'i4')) 137 | hs = from_buffer(f.read(24 * 8), dtype=native_str('|S8')) 138 | 139 | if not is_valid_byteorder(hi): 140 | if is_byteorder_specified: 141 | # specified but not valid. you dun messed up. 142 | raise SacIOError("Incorrect byteorder {}".format(byteorder)) 143 | else: 144 | # not valid, but not specified. 145 | # swap the dtype interpretation (dtype.byteorder), but keep the 146 | # bytes, so the arrays in memory reflect the bytes on disk 147 | hf = hf.newbyteorder('S') 148 | hi = hi.newbyteorder('S') 149 | 150 | # we now have correct headers, let's use their correct byte order. 151 | endian_str = hi.dtype.byteorder 152 | 153 | # check header lengths 154 | if len(hf) != 70 or len(hi) != 40 or len(hs) != 24: 155 | hf = hi = hs = None 156 | if not is_file_name: 157 | f.close() 158 | raise SacIOError("Cannot read all header values") 159 | 160 | npts = hi[HD.INTHDRS.index('npts')] 161 | 162 | # check file size 163 | if checksize: 164 | cur_pos = f.tell() 165 | f.seek(0, os.SEEK_END) 166 | length = f.tell() 167 | f.seek(cur_pos, os.SEEK_SET) 168 | th_length = (632 + 4 * int(npts)) 169 | if length != th_length: 170 | msg = "Actual and theoretical file size are inconsistent.\n" \ 171 | "Actual/Theoretical: {}/{}\n" \ 172 | "Check that headers are consistent with time series." 173 | raise SacIOError(msg.format(length, th_length)) 174 | 175 | # -------------------------------------------------------------- 176 | # READ DATA 177 | # -------------------------------------------------------------- 178 | if headonly: 179 | data = None 180 | else: 181 | data = from_buffer(f.read(int(npts) * 4), 182 | dtype=native_str(endian_str + 'f4')) 183 | 184 | if len(data) != npts: 185 | f.close() 186 | raise SacIOError("Cannot read all data points") 187 | 188 | if is_file_name: 189 | f.close() 190 | 191 | return hf, hi, hs, data 192 | 193 | 194 | def read_sac_ascii(source, headonly=False): 195 | """ 196 | Read a SAC ASCII/Alphanumeric file. 197 | 198 | :param source: Full path or File-like object from a SAC ASCII file on disk. 199 | :type source: str or file 200 | :param headonly: If headonly is True, return the header arrays not the 201 | data array. Note, the entire file is still read in if headonly=True. 202 | :type headonly: bool 203 | 204 | :return: The float, integer, and string header arrays, and data array, 205 | in that order. Data array will be None if headonly is True. 206 | :rtype: :class:`numpy.ndarray` 207 | 208 | """ 209 | # TODO: make headonly=True only read part of the file, not all of it. 210 | # checks: ASCII-ness, header array length, npts matches data length 211 | try: 212 | fh = open(source, 'rb') 213 | is_file_name = True 214 | except TypeError: 215 | fh = source 216 | is_file_name = False 217 | except IOError: 218 | raise SacIOError("No such file: " + source) 219 | finally: 220 | contents = fh.read() 221 | if is_file_name: 222 | fh.close() 223 | 224 | contents = [_i.rstrip(b"\n\r") for _i in contents.splitlines()] 225 | if len(contents) < 14 + 8 + 8: 226 | raise SacIOError("%s is not a valid SAC file:" % fh.name) 227 | 228 | # -------------------------------------------------------------- 229 | # parse the header 230 | # 231 | # The sac header has 70 floats, 40 integers, then 192 bytes 232 | # in strings. Store them in array (and convert the char to a 233 | # list). That's a total of 632 bytes. 234 | # -------------------------------------------------------------- 235 | # read in the float values 236 | # TODO: use native '=' dtype byteorder instead of forcing little endian? 237 | hf = np.array([i.split() for i in contents[:14]], 238 | dtype=native_str(''} 486 | 487 | :return: The float, integer, and string header arrays, in that order. 488 | :rtype: tuple of :class:`numpy.ndarray` 489 | 490 | """ 491 | hf, hi, hs = init_header_arrays(byteorder=byteorder) 492 | 493 | # have to split kevnm into two fields 494 | # TODO: add .lower() to hdr lookups, for safety 495 | if header is not None: 496 | for hdr, value in header.items(): 497 | if hdr in HD.FLOATHDRS: 498 | hf[HD.FLOATHDRS.index(hdr)] = value 499 | elif hdr in HD.INTHDRS: 500 | if not isinstance(value, (np.integer, int)): 501 | msg = "Non-integers may be truncated: {} = {}" 502 | warnings.warn(msg.format(hdr, value)) 503 | hi[HD.INTHDRS.index(hdr)] = value 504 | elif hdr in HD.STRHDRS: 505 | if hdr == 'kevnm': 506 | # assumes users will not include a 'kevnm2' key 507 | # XXX check for empty or null value? 508 | kevnm = '{:<8s}'.format(value[0:8]) 509 | kevnm2 = '{:<8s}'.format(value[8:16]) 510 | hs[1] = kevnm.encode('ascii', 'strict') 511 | hs[2] = kevnm2.encode('ascii', 'strict') 512 | else: 513 | # TODO: why was encoding done? 514 | # hs[HD.STRHDRS.index(hdr)] = value.encode('ascii', 515 | # 'strict') 516 | hs[HD.STRHDRS.index(hdr)] = value 517 | else: 518 | msg = "Unrecognized header name: {}. Ignored.".format(hdr) 519 | warnings.warn(msg) 520 | 521 | return hf, hi, hs 522 | 523 | 524 | def validate_sac_content(hf, hi, hs, data, *tests): 525 | """ 526 | Check validity of loaded SAC file content, such as header/data consistency. 527 | 528 | :param hf: SAC float header array 529 | :type hf: :class:`numpy.ndarray` of floats 530 | :param hi: SAC int header array 531 | :type hi: :class:`numpy.ndarray` of ints 532 | :param hs: SAC string header array 533 | :type hs: :class:`numpy.ndarray` of str 534 | :param data: SAC data array 535 | :type data: :class:`numpy.ndarray` of float32 536 | :param tests: One or more of the following validity tests: 537 | 'delta' : Time step "delta" is positive. 538 | 'logicals' : Logical values are 0, 1, or null 539 | 'data_hdrs' : Length, min, mean, max of data array match header values. 540 | 'enums' : Check validity of enumerated values. 541 | 'reftime' : Reference time values in header are all set. 542 | 'reltime' : Relative time values in header are absolutely referenced. 543 | 'all' : Do all tests. 544 | :type tests: str 545 | 546 | :raises: :class:`SacInvalidContentError` if any of the specified tests 547 | fail. :class:`ValueError` if 'data_hdrs' is specified and data is None, 548 | empty array, or no tests specified. 549 | 550 | """ 551 | # TODO: move this to util.py and write and use individual test functions, 552 | # so that all validity checks are in one place? 553 | _all = ('delta', 'logicals', 'data_hdrs', 'enums', 'reftime', 'reltime') 554 | 555 | if 'all' in tests: 556 | tests = _all 557 | 558 | if not tests: 559 | raise ValueError("No validation tests specified.") 560 | elif any([(itest not in _all) for itest in tests]): 561 | msg = "Unrecognized validataion test specified" 562 | raise ValueError(msg) 563 | 564 | if 'delta' in tests: 565 | dval = hf[HD.FLOATHDRS.index('delta')] 566 | if not (dval >= 0.0): 567 | msg = "Header 'delta' must be >= 0." 568 | raise SacInvalidContentError(msg) 569 | 570 | if 'logicals' in tests: 571 | for hdr in ('leven', 'lpspol', 'lovrok', 'lcalda'): 572 | lval = hi[HD.INTHDRS.index(hdr)] 573 | if lval not in (0, 1, HD.INULL): 574 | msg = "Header '{}' must be {{{}, {}, {}}}." 575 | raise SacInvalidContentError(msg.format(hdr, 0, 1, HD.INULL)) 576 | 577 | if 'data_hdrs' in tests: 578 | try: 579 | is_min = np.allclose(hf[HD.FLOATHDRS.index('depmin')], data.min()) 580 | is_max = np.allclose(hf[HD.FLOATHDRS.index('depmax')], data.max()) 581 | is_mean = np.allclose(hf[HD.FLOATHDRS.index('depmen')], 582 | data.mean()) 583 | if not all([is_min, is_max, is_mean]): 584 | msg = "Data headers don't match data array." 585 | raise SacInvalidContentError(msg) 586 | except (AttributeError, ValueError) as e: 587 | msg = "Data array is None, empty array, or non-array. " + \ 588 | "Cannot check data headers." 589 | raise SacInvalidContentError(msg) 590 | 591 | if 'enums' in tests: 592 | for hdr in HD.ACCEPTED_VALS: 593 | enval = hi[HD.INTHDRS.index(hdr)] 594 | if not is_valid_enum_int(hdr, enval, allow_null=True): 595 | msg = "Invalid enumerated value, '{}': {}".format(hdr, enval) 596 | raise SacInvalidContentError(msg) 597 | 598 | if 'reftime' in tests: 599 | nzyear = hi[HD.INTHDRS.index('nzyear')] 600 | nzjday = hi[HD.INTHDRS.index('nzjday')] 601 | nzhour = hi[HD.INTHDRS.index('nzhour')] 602 | nzmin = hi[HD.INTHDRS.index('nzmin')] 603 | nzsec = hi[HD.INTHDRS.index('nzsec')] 604 | nzmsec = hi[HD.INTHDRS.index('nzmsec')] 605 | 606 | # all header reference time fields are set 607 | if not all([val != HD.INULL for val in 608 | [nzyear, nzjday, nzhour, nzmin, nzsec, nzmsec]]): 609 | msg = "Null reference time values detected." 610 | raise SacInvalidContentError(msg) 611 | 612 | # reference time fields are reasonable values 613 | try: 614 | UTCDateTime(year=nzyear, julday=nzjday, hour=nzhour, minute=nzmin, 615 | second=nzsec, microsecond=nzmsec) 616 | except ValueError as e: 617 | raise SacInvalidContentError("Invalid reference time: %s" % str(e)) 618 | 619 | if 'reltime' in tests: 620 | # iztype is set and points to a non-null header value 621 | iztype_val = hi[HD.INTHDRS.index('iztype')] 622 | if is_valid_enum_int('iztype', iztype_val, allow_null=False): 623 | if iztype_val == 9: 624 | hdr = 'b' 625 | elif iztype_val == 11: 626 | hdr = 'o' 627 | elif iztype_val == 12: 628 | hdr = 'a' 629 | elif iztype_val in range(13, 23): 630 | hdr = 'it' + str(iztype_val - 13) 631 | 632 | if hi[HD.FLOATHDRS.index(hdr)] == HD.INULL: 633 | msg = "Reference header '{}' for iztype '{}' not set." 634 | raise SacInvalidContentError(msg.format(hdr, iztype_val)) 635 | 636 | else: 637 | msg = "Invalid iztype: {}".format(iztype_val) 638 | raise SacInvalidContentError(msg) 639 | 640 | return 641 | 642 | 643 | def is_valid_byteorder(hi): 644 | nvhdr = hi[HD.INTHDRS.index('nvhdr')] 645 | return (0 < nvhdr < 20) 646 | -------------------------------------------------------------------------------- /pysac/sactrace.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python interface to the Seismic Analysis Code (SAC) file format. 4 | 5 | :copyright: 6 | The Los Alamos National Security, LLC, Yannik Behr, C. J. Ammon, 7 | C. Satriano, L. Krischer, and J. MacCarthy 8 | :license: 9 | GNU Lesser General Public License, Version 3 10 | (https://www.gnu.org/copyleft/lesser.html) 11 | 12 | 13 | The SACTrace object maintains consistency between SAC headers and manages 14 | header values in a user-friendly way. This includes some value-checking, native 15 | Python logicals (True, False) and nulls (None) instead of SAC's 0, 1, or 16 | -12345... 17 | 18 | SAC headers are implemented as properties, with appropriate getters and 19 | setters. 20 | 21 | 22 | Features 23 | -------- 24 | 25 | 1. **Read and write SAC binary or ASCII** 26 | 27 | - autodetect or specify expected byteorder 28 | - optional file size checking and/or header consistency checks 29 | - header-only reading and writing 30 | - "overwrite OK" checking ('lovrok' header) 31 | 32 | 2. **Convenient access and manipulation of relative and absolute time 33 | headers** 34 | 3. **User-friendly header printing/viewing** 35 | 4. **Fast access to header values from attributes** 36 | 37 | - With type checking, null handling, and enumerated value checking 38 | 39 | 5. **Convert to/from ObsPy Traces** 40 | 41 | - Conversion from ObsPy Trace to SAC trace retains detected previous 42 | SAC header values. 43 | - Conversion to ObsPy Trace retains the *complete* SAC header. 44 | 45 | 46 | Usage examples 47 | -------------- 48 | 49 | Read/write SAC files 50 | ~~~~~~~~~~~~~~~~~~~~ 51 | 52 | .. code:: python 53 | 54 | # read from a binary file 55 | sac = SACTrace.read(filename) 56 | 57 | # read header only 58 | sac = SACTrace.read(filename, headonly=True) 59 | 60 | # write header-only, file must exist 61 | sac.write(filename, headonly=True) 62 | 63 | # read from an ASCII file 64 | sac = SACTrace.read(filename, ascii=True) 65 | 66 | # write a binary SAC file for a Sun machine 67 | sac.write(filename, byteorder='big') 68 | 69 | Build a SACTrace from a header dictionary and data array 70 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 71 | 72 | .. rubric:: Example 73 | 74 | >>> header = {'kstnm': 'ANMO', 'kcmpnm': 'BHZ', 'stla': 40.5, 'stlo': -108.23, 75 | ... 'evla': -15.123, 'evlo': 123, 'evdp': 50, 'nzyear': 2012, 76 | ... 'nzjday': 123, 'nzhour': 13, 'nzmin': 43, 'nzsec': 17, 77 | ... 'nzmsec': 100, 'delta': 1.0/40} 78 | >>> sac = SACTrace(data=np.random.random(100), **header) 79 | >>> print(sac) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS 80 | Reference Time = 05/02/2012 (123) 13:43:17.100000 81 | iztype IB: begin time 82 | b = 0.0 83 | delta = 0.0250000... 84 | e = 2.4750000... 85 | evdp = 50.0 86 | evla = -15.123000... 87 | evlo = 123.0 88 | iftype = itime 89 | internal0 = 2.0 90 | iztype = ib 91 | kcmpnm = BHZ 92 | kstnm = ANMO 93 | lcalda = False 94 | leven = True 95 | lovrok = True 96 | lpspol = True 97 | npts = 100 98 | nvhdr = 6 99 | nzhour = 13 100 | nzjday = 123 101 | nzmin = 43 102 | nzmsec = 100 103 | nzsec = 17 104 | nzyear = 2012 105 | stla = 40.5 106 | stlo = -108.23000... 107 | 108 | Reference-time and relative time headers 109 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 110 | 111 | .. rubric:: Example 112 | 113 | >>> sac = SACTrace(nzyear=2000, nzjday=1, nzhour=0, nzmin=0, nzsec=0, 114 | ... nzmsec=0, t1=23.5, data=np.arange(100)) 115 | >>> print(sac.reftime) 116 | 2000-01-01T00:00:00.000000Z 117 | >>> sac.b, sac.e, sac.t1 118 | (0.0, 99.0, 23.5) 119 | 120 | Move reference time by relative seconds, relative time headers are 121 | preserved. 122 | 123 | .. rubric:: Example 124 | 125 | >>> sac = SACTrace(nzyear=2000, nzjday=1, nzhour=0, nzmin=0, nzsec=0, 126 | ... nzmsec=0, t1=23.5, data=np.arange(100)) 127 | >>> sac.reftime -= 2.5 128 | >>> sac.b, sac.e, sac.t1 129 | (2.5, 101.5, 26.0) 130 | 131 | Set reference time to new absolute time, relative time headers are 132 | preserved. 133 | 134 | .. rubric:: Example 135 | 136 | >>> sac = SACTrace(nzyear=2000, nzjday=1, nzhour=0, nzmin=0, nzsec=0, 137 | ... nzmsec=0, t1=23.5, data=np.arange(100)) 138 | >>> # set the reftime two minutes later 139 | >>> sac.reftime = UTCDateTime(2000, 1, 1, 0, 2, 0, 0) 140 | >>> sac.b, sac.e, sac.t1 141 | (-120.0, -21.0, -96.5) 142 | 143 | Quick header viewing 144 | ~~~~~~~~~~~~~~~~~~~~ 145 | 146 | Print non-null header values. 147 | 148 | .. rubric:: Example 149 | 150 | >>> sac = SACTrace() 151 | >>> print(sac) # doctest: +NORMALIZE_WHITESPACE 152 | Reference Time = 01/01/1970 (001) 00:00:00.000000 153 | iztype IB: begin time 154 | b = 0.0 155 | delta = 1.0 156 | e = 0.0 157 | iftype = itime 158 | internal0 = 2.0 159 | iztype = ib 160 | lcalda = False 161 | leven = True 162 | lovrok = True 163 | lpspol = True 164 | npts = 0 165 | nvhdr = 6 166 | nzhour = 0 167 | nzjday = 1 168 | nzmin = 0 169 | nzmsec = 0 170 | nzsec = 0 171 | nzyear = 1970 172 | 173 | Print relative time header values. 174 | 175 | .. rubric:: Example 176 | 177 | >>> sac = SACTrace() 178 | >>> sac.lh('picks') # doctest: +NORMALIZE_WHITESPACE 179 | Reference Time = 01/01/1970 (001) 00:00:00.000000 180 | iztype IB: begin time 181 | a = None 182 | b = 0.0 183 | e = 0.0 184 | f = None 185 | o = None 186 | t0 = None 187 | t1 = None 188 | t2 = None 189 | t3 = None 190 | t4 = None 191 | t5 = None 192 | t6 = None 193 | t7 = None 194 | t8 = None 195 | t9 = None 196 | 197 | Header values as attributes 198 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 199 | 200 | Great for interactive use, with (ipython) tab-completion... 201 | 202 | .. code:: python 203 | 204 | sac. 205 | 206 | :: 207 | 208 | sac.a sac.kevnm sac.nzsec 209 | sac.az sac.kf sac.nzyear 210 | sac.b sac.khole sac.o 211 | sac.baz sac.kinst sac.odelta 212 | sac.byteorder sac.knetwk sac.read 213 | sac.cmpaz sac.ko sac.reftime 214 | sac.cmpinc sac.kstnm sac.scale 215 | sac.copy sac.kt0 sac.stdp 216 | sac.data sac.kt1 sac.stel 217 | sac.delta sac.kt2 sac.stla 218 | sac.depmax sac.kt3 sac.stlo 219 | sac.depmen sac.kt4 sac.t0 220 | sac.depmin sac.kt5 sac.t1 221 | sac.dist sac.kt6 sac.t2 222 | sac.e sac.kt7 sac.t3 223 | sac.evdp sac.kt8 sac.t4 224 | sac.evla sac.kt9 sac.t5 225 | sac.evlo sac.kuser0 sac.t6 226 | sac.f sac.kuser1 sac.t7 227 | sac.from_obspy_trace sac.kuser2 sac.t8 228 | sac.gcarc sac.lcalda sac.t9 229 | sac.idep sac.leven sac.to_obspy_trace 230 | sac.ievreg sac.lh sac.unused23 231 | sac.ievtyp sac.listhdr sac.user0 232 | sac.iftype sac.lovrok sac.user1 233 | sac.iinst sac.lpspol sac.user2 234 | sac.imagsrc sac.mag sac.user3 235 | sac.imagtyp sac.nevid sac.user4 236 | sac.internal0 sac.norid sac.user5 237 | sac.iqual sac.npts sac.user6 238 | sac.istreg sac.nvhdr sac.user7 239 | sac.isynth sac.nwfid sac.user8 240 | sac.iztype sac.nzhour sac.user9 241 | sac.ka sac.nzjday sac.validate 242 | sac.kcmpnm sac.nzmin sac.write 243 | sac.kdatrd sac.nzmsec 244 | 245 | ...and documentation (in IPython)! 246 | 247 | .. code:: python 248 | 249 | sac.iztype? 250 | 251 | :: 252 | 253 | Type: property 254 | String form: 255 | Docstring: 256 | I Reference time equivalence: 257 | 258 | * IUNKN (5): Unknown 259 | * IB (9): Begin time 260 | * IDAY (10): Midnight of reference GMT day 261 | * IO (11): Event origin time 262 | * IA (12): First arrival time 263 | * ITn (13-22): User defined time pick n, n=0,9 264 | 265 | Convert to/from ObsPy Traces 266 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 267 | 268 | .. rubric:: Example 269 | 270 | >>> from obspy import read 271 | >>> tr = read()[0] 272 | >>> print(tr.stats) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE 273 | network: BW 274 | station: RJOB 275 | location: 276 | channel: EHZ 277 | starttime: 2009-08-24T00:20:03.000000Z 278 | endtime: 2009-08-24T00:20:32.990000Z 279 | sampling_rate: 100.0 280 | delta: 0.01 281 | npts: 3000 282 | calib: 1.0 283 | back_azimuth: 100.0 284 | inclination: 30.0 285 | response: Channel Response 286 | ... 287 | 288 | 289 | >>> sac = SACTrace.from_obspy_trace(tr) 290 | >>> print(sac) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS 291 | Reference Time = 08/24/2009 (236) 00:20:03.000000 292 | iztype IB: begin time 293 | b = 0.0 294 | delta = 0.009999999... 295 | e = 29.989999... 296 | iftype = itime 297 | iztype = ib 298 | kcmpnm = EHZ 299 | knetwk = BW 300 | kstnm = RJOB 301 | lcalda = False 302 | leven = True 303 | lovrok = True 304 | lpspol = True 305 | npts = 3000 306 | nvhdr = 6 307 | nzhour = 0 308 | nzjday = 236 309 | nzmin = 20 310 | nzmsec = 0 311 | nzsec = 3 312 | nzyear = 2009 313 | scale = 1.0 314 | 315 | >>> tr2 = sac.to_obspy_trace() 316 | >>> print(tr2.stats) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS 317 | network: BW 318 | station: RJOB 319 | location: 320 | channel: EHZ 321 | starttime: 2009-08-24T00:20:03.000000Z 322 | endtime: 2009-08-24T00:20:32.990000Z 323 | sampling_rate: 100.0 324 | delta: 0.01 325 | npts: 3000 326 | calib: 1.0 327 | sac: AttribDict(...) 328 | 329 | """ 330 | from __future__ import (absolute_import, division, print_function, 331 | unicode_literals) 332 | from future.builtins import * # NOQA 333 | from future.utils import native_str 334 | 335 | import sys 336 | import warnings 337 | from copy import deepcopy 338 | from itertools import chain 339 | 340 | import numpy as np 341 | from obspy import Trace, UTCDateTime 342 | from obspy.geodetics import gps2dist_azimuth, kilometer2degrees 343 | 344 | from . import header as HD # noqa 345 | from .util import SacError, SacHeaderError 346 | from . import util as _ut 347 | from . import arrayio as _io 348 | 349 | 350 | # ------------- HEADER GETTER/SETTERS ----------------------------------------- 351 | # 352 | # These are functions used to set up header properties on the SACTrace class. 353 | # Properties are accessed like class attributes, but use getter and/or setter 354 | # functions. They're defined here, outside the class, b/c there are lots of 355 | # properties and we want to keep the class clean. 356 | # 357 | # getter/setter factories: 358 | # Property getters/setters must be defined for _each_ header, even if they work 359 | # the exact same way for similar headers, so we define function factories here 360 | # for groups of headers that will be gotten and set in a similar fashion. 361 | # 362 | # Usage: delta_getter = _floatgetter('delta') 363 | # delta_setter = _floatsetter('delta') 364 | # These factories produce functions that simply index into their array to get 365 | # or set. Use them for header values in hf, hi, and hs that need no special 366 | # handling. Values that depend on other values will need their own 367 | # getters/setters. 368 | # 369 | # See: 370 | # https://stackoverflow.com/q/2123585 371 | # 372 | # TODO: Replace all these factories and properties with Python Descriptors. 373 | # http://nbviewer.jupyter.org/urls/gist.github.com/ChrisBeaumont/ 374 | # 5758381/raw/descriptor_writeup.ipynb 375 | # Also, don't forget to worry about access to __doc__ on both the class and 376 | # the instances. 377 | # 378 | # floats 379 | def _floatgetter(hdr): 380 | def get_float(self): 381 | value = float(self._hf[HD.FLOATHDRS.index(hdr)]) 382 | if value == HD.FNULL: 383 | value = None 384 | return value 385 | return get_float 386 | 387 | 388 | def _floatsetter(hdr): 389 | def set_float(self, value): 390 | if value is None: 391 | value = HD.FNULL 392 | self._hf[HD.FLOATHDRS.index(hdr)] = value 393 | return set_float 394 | 395 | 396 | # ints 397 | def _intgetter(hdr): 398 | def get_int(self): 399 | value = int(self._hi[HD.INTHDRS.index(hdr)]) 400 | if value == HD.INULL: 401 | value = None 402 | return value 403 | return get_int 404 | 405 | 406 | def _intsetter(hdr): 407 | def set_int(self, value): 408 | if value % 1: 409 | warnings.warn("Non-integers may be truncated. ({}: {})".format( 410 | hdr, value)) 411 | if value is None: 412 | value = HD.INULL 413 | self._hi[HD.INTHDRS.index(hdr)] = value 414 | return set_int 415 | 416 | 417 | # logicals/bools (subtype of ints) 418 | def _boolgetter(hdr): 419 | def get_bool(self): 420 | value = self._hi[HD.INTHDRS.index(hdr)] 421 | return bool(value) 422 | return get_bool 423 | 424 | 425 | def _boolsetter(hdr): 426 | def set_bool(self, value): 427 | if value not in (True, False, 1, 0): 428 | msg = "Logical header values must be {True, False, 1, 0}" 429 | raise ValueError(msg) 430 | # booleans are subclasses of integers. They will be set (cast) 431 | # directly into an integer array as 0 or 1. 432 | self._hi[HD.INTHDRS.index(hdr)] = value 433 | return set_bool 434 | 435 | 436 | # enumerated values (stored as ints, represented by strings) 437 | def _enumgetter(hdr): 438 | def get_enum(self): 439 | value = self._hi[HD.INTHDRS.index(hdr)] 440 | if value == HD.INULL: 441 | name = None 442 | elif _ut.is_valid_enum_int(hdr, value): 443 | name = HD.ENUM_NAMES[value] 444 | else: 445 | msg = """Unrecognized enumerated value {} for header "{}". 446 | See .header for allowed values.""".format(value, hdr) 447 | warnings.warn(msg) 448 | name = None 449 | return name 450 | return get_enum 451 | 452 | 453 | def _enumsetter(hdr): 454 | def set_enum(self, value): 455 | if value is None: 456 | value = HD.INULL 457 | elif _ut.is_valid_enum_str(hdr, value): 458 | value = HD.ENUM_VALS[value] 459 | else: 460 | msg = 'Unrecognized enumerated value "{}" for header "{}"' 461 | raise ValueError(msg.format(value, hdr)) 462 | self._hi[HD.INTHDRS.index(hdr)] = value 463 | return set_enum 464 | 465 | 466 | # strings 467 | def _strgetter(hdr): 468 | def get_str(self): 469 | try: 470 | # value is a bytes 471 | value = native_str(self._hs[HD.STRHDRS.index(hdr)].decode()) 472 | except AttributeError: 473 | # value is a str 474 | value = native_str(self._hs[HD.STRHDRS.index(hdr)]) 475 | 476 | if value == HD.SNULL: 477 | value = None 478 | 479 | try: 480 | value = value.strip() 481 | except AttributeError: 482 | # it's None. no .strip method 483 | pass 484 | return value 485 | return get_str 486 | 487 | 488 | def _strsetter(hdr): 489 | def set_str(self, value): 490 | if value is None: 491 | value = HD.SNULL 492 | elif len(value) > 8: 493 | msg = "Alphanumeric headers longer than 8 characters are "\ 494 | "right-truncated." 495 | warnings.warn(msg) 496 | # they will truncate themselves, since _hs is dtype '|S8' 497 | try: 498 | self._hs[HD.STRHDRS.index(hdr)] = value.encode('ascii', 'strict') 499 | except AttributeError: 500 | self._hs[HD.STRHDRS.index(hdr)] = value 501 | 502 | return set_str 503 | 504 | 505 | # Factory for functions of .data (min, max, mean, len) 506 | def _make_data_func(func, hdr): 507 | # returns a method that returns the value of func(self.data), or the 508 | # corresponding array header value, if data is None 509 | def do_data_func(self): 510 | try: 511 | value = func(self.data) 512 | if not isinstance(value, int): 513 | value = float(value) 514 | except TypeError: 515 | # data is None, get the value from header 516 | try: 517 | value = float(self._hf[HD.FLOATHDRS.index(hdr)]) 518 | null = HD.FNULL 519 | except ValueError: 520 | # hdr is 'npts', the only integer 521 | # XXX: this also trip if a data-centric header is misspelled? 522 | value = int(self._hi[HD.INTHDRS.index(hdr)]) 523 | null = HD.INULL 524 | if value == null: 525 | value = None 526 | return value 527 | return do_data_func 528 | 529 | # TODO: a data setter the requires a float32 array 530 | 531 | 532 | # Factory function for setting relative time headers with either a relative 533 | # time float or an absolute UTCDateTime 534 | # used for: b, o, a, f, t0-t9 535 | def _reltime_setter(hdr): 536 | def set_reltime(self, value): 537 | if isinstance(value, UTCDateTime): 538 | # get the offset from reftime 539 | offset = value - self.reftime 540 | else: 541 | # value is already a reftime offset. 542 | offset = value 543 | # make and use a _floatsetter to actually set it 544 | floatsetter = _floatsetter(hdr) 545 | floatsetter(self, offset) 546 | return set_reltime 547 | 548 | 549 | # Factory function for setting geographic header values 550 | # (evlo, evla, stalo, stalat) 551 | # that will check lcalda and calculate and set dist, az, baz, gcarc 552 | def _geosetter(hdr): 553 | def set_geo(self, value): 554 | # make and use a _floatsetter 555 | set_geo_float = _floatsetter(hdr) 556 | set_geo_float(self, value) 557 | if self.lcalda: 558 | # check and maybe set lcalda 559 | try: 560 | self._set_distances() 561 | except SacHeaderError: 562 | pass 563 | return set_geo 564 | 565 | 566 | # OTHER GETTERS/SETTERS 567 | def _set_lcalda(self, value): 568 | # make and use a bool setter for lcalda 569 | lcalda_setter = _boolsetter('lcalda') 570 | lcalda_setter(self, value) 571 | # try to set set distances if "value" evaluates to True 572 | if value: 573 | try: 574 | self._set_distances() 575 | except SacHeaderError: 576 | pass 577 | 578 | 579 | def _get_e(self): 580 | try: 581 | if self.npts: 582 | e = self.b + (self.npts - 1) * self.delta 583 | else: 584 | e = self.b 585 | except TypeError: 586 | # b, npts, and/or delta are None/null 587 | # TODO: assume "b" is 0.0? 588 | e = None 589 | return e 590 | 591 | 592 | def _set_iztype(self, iztype): 593 | """ 594 | Set the iztype, which describes what the reftime is. 595 | 596 | Setting the iztype will shift the relative time headers, such that the 597 | header that iztype points to is (near) zero, and all others are shifted 598 | together by the difference. 599 | 600 | Affected headers: b, o, a, f, t0-t9 601 | 602 | :param iztype: One of the following strings: 603 | 'iunkn' 604 | 'ib', begin time 605 | 'iday', midnight of reference GMT day 606 | 'io', event origin time 607 | 'ia', first arrival time 608 | 'it0'-'it9', user defined pick t0-t9. 609 | :type iztype: str 610 | 611 | """ 612 | # The Plan: 613 | # 1. find the seconds needed to shift the old reftime to the new one. 614 | # 2. shift reference time onto the iztype header using that shift value. 615 | # 3. this triggers an _allt shift of all relative times by the same amount. 616 | # 4. If all goes well, actually set the iztype in the header. 617 | 618 | # 1. 619 | if iztype == 'iunkn': 620 | # no shift 621 | ref_val = 0.0 622 | elif iztype == 'iday': 623 | # seconds since midnight of reference day 624 | reftime = self.reftime 625 | ref_val = reftime - UTCDateTime(year=reftime.year, 626 | julday=reftime.julday) 627 | else: 628 | # a relative time header. 629 | # remove the 'i' (first character) in the iztype to get the header name 630 | ref_val = getattr(self, iztype[1:]) 631 | if ref_val is None: 632 | msg = "Reference header for iztype '{}' is not set".format(iztype) 633 | raise SacError(msg) 634 | 635 | # 2. set a new reference time, 636 | # 3. which also shifts all non-null relative times (self._allt). 637 | # remainder microseconds may be in the reference header value, because 638 | # nzmsec can't hold them. 639 | self.reftime = self.reftime + ref_val 640 | 641 | # 4. no exceptions yet. actually set the iztype 642 | # make an _enumsetter for iztype and use it, for its enum checking. 643 | izsetter = _enumsetter('iztype') 644 | izsetter(self, iztype) 645 | 646 | 647 | # kevnm is 16 characters, split into two 8-character fields 648 | # intercept and handle in while getting and setting 649 | def _get_kevnm(self): 650 | kevnm = self._hs[HD.STRHDRS.index('kevnm')] 651 | kevnm2 = self._hs[HD.STRHDRS.index('kevnm2')] 652 | try: 653 | kevnm = kevnm.decode() 654 | kevnm2 = kevnm2.decode() 655 | except AttributeError: 656 | # kevnm is a str 657 | pass 658 | 659 | if kevnm == HD.SNULL: 660 | kevnm = '' 661 | if kevnm2 == HD.SNULL: 662 | kevnm2 = '' 663 | 664 | value = (kevnm + kevnm2).strip() 665 | 666 | if not value: 667 | value = None 668 | 669 | return value 670 | 671 | 672 | def _set_kevnm(self, value): 673 | if value is None: 674 | value = HD.SNULL + HD.SNULL 675 | elif len(value) > 16: 676 | msg = "kevnm over 16 characters. Truncated to {}.".format(value[:16]) 677 | warnings.warn(msg) 678 | kevnm = '{:<8s}'.format(value[0:8]) 679 | kevnm2 = '{:<8s}'.format(value[8:16]) 680 | self._hs[HD.STRHDRS.index('kevnm')] = kevnm 681 | self._hs[HD.STRHDRS.index('kevnm2')] = kevnm2 682 | 683 | # TODO: move get/set reftime up here, make it a property 684 | 685 | 686 | # -------------------------- SAC OBJECT INTERFACE ----------------------------- 687 | class SACTrace(object): 688 | __doc__ = """ 689 | Convenient and consistent in-memory representation of Seismic Analysis Code 690 | (SAC) files. 691 | 692 | This is the human-facing interface for making a valid instance. For 693 | file-based or other constructors, see class methods .read and 694 | .from_obspy_trace. SACTrace instances preserve relationships between 695 | header values. 696 | 697 | :param data: Associated time-series data vector. Optional. If omitted, None 698 | is set as the instance data attribute. 699 | :type data: :class:`numpy.ndarray` of float32 700 | 701 | Any valid header key/value pair is also an optional input keyword argument. 702 | If not provided, minimum required headers are set to valid default values. 703 | The default instance is an evenly-space trace, with a sample rate of 1.0, 704 | and len(data) or 0 npts, starting at 1970-01-01T00:00:00.000000. 705 | 706 | :var reftime: Read-only reference time. Calculated from nzyear, nzjday, 707 | nzhour, nzmin, nzsec, nzmsec. 708 | :var byteorder: The byte order of the underlying header/data arrays. 709 | Raises :class:`SacError` if array byte orders are inconsistent, even in 710 | the case where '<' is your native order and byteorders look like '<', 711 | '=', '='. 712 | 713 | Any valid header name is also an attribute. See below, :mod:`header`, 714 | or individial attribution docstrings for more header information. 715 | 716 | THE SAC HEADER 717 | 718 | NOTE: All header names and string values are lowercase. Header value 719 | access should be through instance attributes. 720 | 721 | """ + HD.HEADER_DOCSTRING 722 | 723 | def __init__(self, leven=True, delta=1.0, b=0.0, e=0.0, iztype='ib', 724 | nvhdr=6, npts=0, iftype='itime', nzyear=1970, nzjday=1, 725 | nzhour=0, nzmin=0, nzsec=0, nzmsec=0, lcalda=False, 726 | lpspol=True, lovrok=True, internal0=2.0, data=None, **kwargs): 727 | """ 728 | Initialize a SACTrace object using header key-value pairs and a 729 | numpy.ndarray for the data, both optional. 730 | 731 | ..rubric:: Example 732 | 733 | >>> sac = SACTrace(nzyear=1995, nzmsec=50, data=np.arange(100)) 734 | >>> print(sac) # doctest: +NORMALIZE_WHITESPACE 735 | Reference Time = 01/01/1995 (001) 00:00:00.050000 736 | iztype IB: begin time 737 | b = 0.0 738 | delta = 1.0 739 | e = 99.0 740 | iftype = itime 741 | internal0 = 2.0 742 | iztype = ib 743 | lcalda = False 744 | leven = True 745 | lovrok = True 746 | lpspol = True 747 | npts = 100 748 | nvhdr = 6 749 | nzhour = 0 750 | nzjday = 1 751 | nzmin = 0 752 | nzmsec = 50 753 | nzsec = 0 754 | nzyear = 1995 755 | 756 | """ 757 | # The Plan: 758 | # 1. Build the default header dictionary and update with provided 759 | # values. 760 | # 2. Convert header dict to arrays (util.dict_to_header_arrays 761 | # initializes the arrays and fills in without checking. 762 | # 3. set the _h[fis] and data arrays on self. 763 | 764 | # 1. 765 | # build the required header from provided or default values 766 | header = {'leven': leven, 'npts': npts, 'delta': delta, 'b': b, 'e': e, 767 | 'iztype': iztype, 'nvhdr': nvhdr, 'iftype': iftype, 768 | 'nzyear': nzyear, 'nzjday': nzjday, 'nzhour': nzhour, 769 | 'nzmin': nzmin, 'nzsec': nzsec, 'nzmsec': nzmsec, 770 | 'lcalda': lcalda, 'lpspol': lpspol, 'lovrok': lovrok, 771 | 'internal0': internal0} 772 | 773 | # combine header with remaining non-required args. 774 | # user can put non-SAC key:value pairs into the header, but they're 775 | # ignored on write. 776 | header.update(kwargs) 777 | 778 | # -------------------------- DATA ARRAY ------------------------------- 779 | if data is None: 780 | # this is like "headonly=True" 781 | pass 782 | else: 783 | if not isinstance(data, np.ndarray): 784 | raise TypeError("data needs to be a numpy.ndarray") 785 | else: 786 | # Only copy the data if they are not of the required type 787 | # XXX: why require little endian instead of native byte order? 788 | # data = np.require(data, native_str('': 1027 | byteorder = 'big' 1028 | 1029 | return byteorder 1030 | 1031 | # TODO: make a byteorder setter? 1032 | def _byteswap(self): 1033 | """ 1034 | Change the underlying byte order and dtype interpretation of the float, 1035 | int, and (if present) data arrays. 1036 | 1037 | """ 1038 | try: 1039 | self._hf = self._hf.byteswap(True).newbyteorder('S') 1040 | self._hi = self._hi.byteswap(True).newbyteorder('S') 1041 | if self.data is not None: 1042 | self.data = self.data.byteswap(True).newbyteorder('S') 1043 | except Exception as e: 1044 | # if this fails, roll it back? 1045 | raise e 1046 | 1047 | @property 1048 | def reftime(self): 1049 | """ 1050 | Get or set the SAC header reference time as a UTCDateTime instance. 1051 | 1052 | reftime is not an attribute, but is constructed and dismantled each 1053 | time directly to/from the SAC "nz"-time fields. 1054 | 1055 | Setting a new reftime shifts all non-null relative time headers 1056 | accordingly. It accepts a UTCDateTime object, from which time shifts 1057 | are calculated. 1058 | 1059 | ..rubric:: notes 1060 | 1061 | The reftime you supply will be robbed of its remainder microseconds, 1062 | which are then pushed into the relative time header shifts. This means 1063 | that the reftime you observe after you set it here may not exactly 1064 | match the reftime you supplied; it may be `remainder microseconds` 1065 | earlier. Nor will the iztype reference header value be exactly zero; 1066 | it will be equal to `remainder microseconds` (as seconds). 1067 | 1068 | """ 1069 | return _ut.get_sac_reftime(self._header) 1070 | 1071 | @reftime.setter 1072 | def reftime(self, new_reftime): 1073 | try: 1074 | old_reftime = self.reftime 1075 | 1076 | # find the milliseconds and leftover microseconds for new reftime 1077 | _, rem_microsecs = _ut.split_microseconds(new_reftime.microsecond) 1078 | 1079 | # snap the new reftime to the most recent milliseconds 1080 | # (subtract the leftover microseconds) 1081 | new_reftime.microsecond -= rem_microsecs 1082 | 1083 | self.nzyear = new_reftime.year 1084 | self.nzjday = new_reftime.julday 1085 | self.nzhour = new_reftime.hour 1086 | self.nzmin = new_reftime.minute 1087 | self.nzsec = new_reftime.second 1088 | self.nzmsec = new_reftime.microsecond / 1000 1089 | 1090 | # get the float seconds between the old and new reftimes 1091 | shift = old_reftime - new_reftime 1092 | 1093 | # shift the relative time headers 1094 | self._allt(np.float32(shift)) 1095 | 1096 | except AttributeError: 1097 | msg = "New reference time must be an obspy.UTCDateTime instance." 1098 | raise TypeError(msg) 1099 | 1100 | # --------------------------- I/O METHODS --------------------------------- 1101 | @classmethod 1102 | def read(cls, source, headonly=False, ascii=False, byteorder=None, 1103 | checksize=False, debug_strings=False): 1104 | """ 1105 | Construct an instance from a binary or ASCII file on disk. 1106 | 1107 | :param source: Full path string for File-like object from a SAC binary 1108 | file on disk. If it is an open File object, open 'rb'. 1109 | :type source: str or file 1110 | :param headonly: If headonly is True, only read the header arrays not 1111 | the data array. 1112 | :type headonly: bool 1113 | :param ascii: If True, file is a SAC ASCII/Alphanumeric file. 1114 | :type ascii: bool 1115 | :param byteorder: If omitted or None, automatic byte-order checking is 1116 | done, starting with native order. If byteorder is specified and 1117 | incorrect, a :class:`SacIOError` is raised. Only valid for binary 1118 | files. 1119 | :type byteorder: str {'little', 'big'}, optional 1120 | :param checksize: If True, check that the theoretical file size from 1121 | the header matches the size on disk. Only valid for binary files. 1122 | :type checksize: bool 1123 | :param debug_strings: By default, non-ASCII and null-termination 1124 | characters are removed from character header fields, and those 1125 | beginning with '-12345' are considered unset. If True, they 1126 | are instead passed without modification. Good for debugging. 1127 | :type debug_strings: bool 1128 | 1129 | :raises: :class:`SacIOError` if checksize failed, byteorder was wrong, 1130 | or header arrays are wrong size. 1131 | 1132 | .. rubric:: Example 1133 | 1134 | >>> from obspy.core.util import get_example_file 1135 | >>> from obspy.io.sac.util import SacInvalidContentError 1136 | >>> file_ = get_example_file("test.sac") 1137 | >>> sac = SACTrace.read(file_, headonly=True) 1138 | >>> sac.data is None 1139 | True 1140 | >>> sac = SACTrace.read(file_, headonly=False) 1141 | >>> sac.data # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE 1142 | array([ -8.74227766e-08, -3.09016973e-01, -5.87785363e-01, 1143 | -8.09017122e-01, -9.51056600e-01, -1.00000000e+00, 1144 | -9.51056302e-01, -8.09016585e-01, -5.87784529e-01, 1145 | ... 1146 | 8.09022486e-01, 9.51059461e-01, 1.00000000e+00, 1147 | 9.51053500e-01, 8.09011161e-01, 5.87777138e-01, 1148 | 3.09007347e-01], dtype=float32) 1149 | 1150 | See also: :meth:`SACTrace.validate` 1151 | 1152 | """ 1153 | if ascii: 1154 | hf, hi, hs, data = _io.read_sac_ascii(source, headonly=headonly) 1155 | else: 1156 | hf, hi, hs, data = _io.read_sac(source, headonly=headonly, 1157 | byteorder=byteorder, 1158 | checksize=checksize) 1159 | if not debug_strings: 1160 | for i, val in enumerate(hs): 1161 | val = _ut._clean_str(val, strip_whitespace=False) 1162 | if val.startswith(native_str('-12345')): 1163 | val = HD.SNULL 1164 | hs[i] = val 1165 | 1166 | sac = cls._from_arrays(hf, hi, hs, data) 1167 | if sac.dist is None: 1168 | sac._set_distances() 1169 | 1170 | return sac 1171 | 1172 | def write(self, dest, headonly=False, ascii=False, byteorder=None, 1173 | flush_headers=True): 1174 | """ 1175 | Write the header and (optionally) data arrays to a SAC binary file. 1176 | 1177 | :param dest: Full path or File-like object to SAC binary file on disk. 1178 | :type dest: str or file 1179 | :param headonly: If headonly is True, only read the header arrays not 1180 | the data array. 1181 | :type headonly: bool 1182 | :param ascii: If True, file is a SAC ASCII/Alphanumeric file. 1183 | :type ascii: bool 1184 | :param byteorder: If omitted or None, automatic byte-order checking is 1185 | done, starting with native order. If byteorder is specified and 1186 | incorrect, a :class:`SacIOError` is raised. Only valid for binary 1187 | files. 1188 | :type byteorder: str {'little', 'big'}, optional 1189 | :param flush_headers: If True, update data headers like 'depmin' and 1190 | 'depmax' with values from the data array. 1191 | :type flush_headers: bool 1192 | 1193 | """ 1194 | if headonly: 1195 | data = None 1196 | else: 1197 | # do a check for float32 data here instead of arrayio.write_sac? 1198 | data = self.data 1199 | if flush_headers: 1200 | self._flush_headers() 1201 | 1202 | if ascii: 1203 | _io.write_sac_ascii(dest, self._hf, self._hi, self._hs, data) 1204 | else: 1205 | byteorder = byteorder or self.byteorder 1206 | _io.write_sac(dest, self._hf, self._hi, self._hs, data, 1207 | byteorder=byteorder) 1208 | 1209 | @classmethod 1210 | def _from_arrays(cls, hf=None, hi=None, hs=None, data=None): 1211 | """ 1212 | Low-level array-based constructor. 1213 | 1214 | This constructor is good for getting a "blank" SAC object, and is used 1215 | in other, perhaps more useful, alternate constructors ("See Also"). 1216 | No value checking is done and header values are completely overwritten 1217 | with the provided arrays, which is why this is a hidden constructor. 1218 | 1219 | :param hf: SAC float header array 1220 | :type hf: :class:`numpy.ndarray` of floats 1221 | :param hi: SAC int header array 1222 | :type hi: :class:`numpy.ndarray` of ints 1223 | :param hs: SAC string header array 1224 | :type hs: :class:`numpy.ndarray` of str 1225 | :param data: SAC data array, optional. 1226 | 1227 | If omitted or None, the header arrays are intialized according to 1228 | :func:`arrayio.init_header_arrays`. If data is omitted, it is 1229 | simply set to None on the corresponding :class:`SACTrace`. 1230 | 1231 | .. rubric:: Example 1232 | 1233 | >>> sac = SACTrace._from_arrays() 1234 | >>> print(sac) # doctest: +NORMALIZE_WHITESPACE 1235 | Reference Time = XX/XX/XX (XXX) XX:XX:XX.XXXXXX 1236 | iztype not set 1237 | lcalda = True 1238 | leven = False 1239 | lovrok = False 1240 | lpspol = False 1241 | 1242 | """ 1243 | # use the first byteorder we find, or system byteorder if we 1244 | # never find any 1245 | bo = '=' 1246 | for arr in (hf, hi, hs, data): 1247 | try: 1248 | bo = arr.dtype.byteorder 1249 | break 1250 | except AttributeError: 1251 | # arr is None (not supplied) 1252 | pass 1253 | hf0, hi0, hs0 = _io.init_header_arrays(byteorder=bo) 1254 | # TODO: hf0, hi0, hs0 = _io.init_header_array_values(hf0, hi0, hs0) 1255 | 1256 | if hf is None: 1257 | hf = hf0 1258 | if hi is None: 1259 | hi = hi0 1260 | if hs is None: 1261 | hs = hs0 1262 | 1263 | # get the default instance, but completely replace the arrays 1264 | # initializes arrays twice, but it beats converting empty arrays to a 1265 | # dict and then passing it to __init__, i think...maybe... 1266 | sac = cls() 1267 | sac._hf = hf 1268 | sac._hi = hi 1269 | sac._hs = hs 1270 | sac.data = data 1271 | 1272 | return sac 1273 | 1274 | # TO/FROM OBSPY TRACES 1275 | @classmethod 1276 | def from_obspy_trace(cls, trace, keep_sac_header=True): 1277 | """ 1278 | Construct an instance from an ObsPy Trace. 1279 | 1280 | :param trace: Source Trace object 1281 | :type trace: :class:`~obspy.core.Trace` instance 1282 | :param keep_sac_header: If True, any old stats.sac header values are 1283 | kept as is, and only a minimal set of values are updated from the 1284 | stats dictionary: npts, e, and data. If an old iztype and a valid 1285 | reftime are present, b and e will be properly referenced to it. If 1286 | False, a new SAC header is constructed from only information found 1287 | in the stats dictionary, with some other default values introduced. 1288 | :type keep_sac_header: bool 1289 | 1290 | """ 1291 | header = _ut.obspy_to_sac_header(trace.stats, keep_sac_header) 1292 | 1293 | # handle the data headers 1294 | data = trace.data 1295 | try: 1296 | if len(data) == 0: 1297 | # data is a empty numpy.array 1298 | data = None 1299 | except TypeError: 1300 | # data is None 1301 | data = None 1302 | 1303 | try: 1304 | byteorder = data.dtype.byteorder 1305 | except AttributeError: 1306 | # data is None 1307 | byteorder = '=' 1308 | 1309 | hf, hi, hs = _io.dict_to_header_arrays(header, byteorder=byteorder) 1310 | sac = cls._from_arrays(hf, hi, hs, data) 1311 | # sac._flush_headers() 1312 | 1313 | return sac 1314 | 1315 | def to_obspy_trace(self, debug_headers=False): 1316 | """ 1317 | Return an ObsPy Trace instance. 1318 | 1319 | Required headers: nz-time fields, npts, delta, calib, kcmpnm, kstnm, 1320 | ...? 1321 | 1322 | :param debug_headers: Include _all_ SAC headers into the 1323 | Trace.stats.sac dictionary. 1324 | :type debug_headers: bool 1325 | 1326 | .. rubric:: Example 1327 | 1328 | >>> from obspy.core.util import get_example_file 1329 | >>> file_ = get_example_file("test.sac") 1330 | >>> sac = SACTrace.read(file_, headonly=True) 1331 | >>> tr = sac.to_obspy_trace() 1332 | >>> print(tr) # doctest: +ELLIPSIS 1333 | .STA..Q | 1978-07-18T08:00:10.000000Z - ... | 1.0 Hz, 100 samples 1334 | 1335 | """ 1336 | # make the obspy test for tests/data/testxy.sac pass 1337 | # ObsPy does not require a valid reftime 1338 | # try: 1339 | # self.validate('reftime') 1340 | # except SacInvalidContentError: 1341 | # if not self.nzyear: 1342 | # self.nzyear = 1970 1343 | # if not self.nzjday: 1344 | # self.nzjday = 1 1345 | # for hdr in ['nzhour', 'nzmin', 'nzsec', 'nzmsec']: 1346 | # if not getattr(self, hdr): 1347 | # setattr(self, hdr, 0) 1348 | self.validate('delta') 1349 | if self.data is None: 1350 | # headonly is True 1351 | # Make it something palatable to ObsPy 1352 | data = np.array([], dtype=self._hf.dtype.byteorder + 'f4') 1353 | else: 1354 | data = self.data 1355 | 1356 | sachdr = _io.header_arrays_to_dict(self._hf, self._hi, self._hs, 1357 | nulls=debug_headers) 1358 | # TODO: logic to use debug_headers for real 1359 | 1360 | stats = _ut.sac_to_obspy_header(sachdr) 1361 | 1362 | return Trace(data=data, header=stats) 1363 | 1364 | # ---------------------- other properties/methods ------------------------- 1365 | def validate(self, *tests): 1366 | """ 1367 | Check validity of loaded SAC file content, such as header/data 1368 | consistency. 1369 | 1370 | :param tests: One or more of the following validity tests: 1371 | 'delta' : Time step "delta" is positive. 1372 | 'logicals' : Logical values are 0, 1, or null 1373 | 'data_hdrs' : Length, min, mean, max of data array match header 1374 | values. 1375 | 'enums' : Check validity of enumerated values. 1376 | 'reftime' : Reference time values in header are all set. 1377 | 'reltime' : Relative time values in header are absolutely 1378 | referenced. 1379 | 'all' : Do all tests. 1380 | :type tests: str 1381 | 1382 | :raises: :class:`SacInvalidContentError` if any of the specified tests 1383 | fail. :class:`ValueError` if 'data_hdrs' is specified and data is 1384 | None, empty array, or no tests specified. 1385 | 1386 | .. rubric:: Example 1387 | 1388 | >>> from obspy.core.util import get_example_file 1389 | >>> from obspy.io.sac.util import SacInvalidContentError 1390 | >>> file_ = get_example_file("LMOW.BHE.SAC") 1391 | >>> sac = SACTrace.read(file_) 1392 | >>> # make the time step invalid, catch it, and fix it 1393 | >>> sac.delta *= -1.0 1394 | >>> try: 1395 | ... sac.validate('delta') 1396 | ... except SacInvalidContentError as e: 1397 | ... sac.delta *= -1.0 1398 | ... sac.validate('delta') 1399 | >>> # make the data and depmin/men/max not match, catch the validation 1400 | >>> # error, then fix (flush) the headers so that they validate 1401 | >>> sac.data += 5.0 1402 | >>> try: 1403 | ... sac.validate('data_hdrs') 1404 | ... except SacInvalidContentError: 1405 | ... sac._flush_headers() 1406 | ... sac.validate('data_hdrs') 1407 | 1408 | """ 1409 | _io.validate_sac_content(self._hf, self._hi, self._hs, self.data, 1410 | *tests) 1411 | 1412 | def _format_header_str(self, hdrlist='all'): 1413 | """ 1414 | Produce a print-friendly string of header values for __repr__ , 1415 | .listhdr(), and .lh() 1416 | 1417 | """ 1418 | # interpret hdrlist 1419 | if hdrlist == 'all': 1420 | hdrlist = sorted(self._header.keys()) 1421 | elif hdrlist == 'picks': 1422 | hdrlist = ('a', 'b', 'e', 'f', 'o', 't0', 't1', 't2', 't3', 't4', 1423 | 't5', 't6', 't7', 't8', 't9') 1424 | else: 1425 | msg = "Unrecognized hdrlist '{}'".format(hdrlist) 1426 | raise ValueError(msg) 1427 | 1428 | # start building header string 1429 | # 1430 | # reference time 1431 | header_str = [] 1432 | try: 1433 | timefmt = "Reference Time = %m/%d/%Y (%j) %H:%M:%S.%f" 1434 | header_str.append(self.reftime.strftime(timefmt)) 1435 | except (ValueError, SacError): 1436 | msg = "Reference time information incomplete." 1437 | warnings.warn(msg) 1438 | notime_str = "Reference Time = XX/XX/XX (XXX) XX:XX:XX.XXXXXX" 1439 | header_str.append(notime_str) 1440 | # 1441 | # reftime type 1442 | # TODO: use enumerated value dict here? 1443 | iztype = self.iztype 1444 | if iztype is None: 1445 | header_str.append("\tiztype not set") 1446 | elif iztype == 'ib': 1447 | header_str.append("\tiztype IB: begin time") 1448 | elif iztype == 'io': 1449 | header_str.append("\tiztype IO: origin time") 1450 | elif iztype == 'ia': 1451 | header_str.append("\tiztype IA: first arrival time") 1452 | elif iztype[1] == 't': 1453 | vals = (iztype.upper(), iztype[1:]) 1454 | izfmt = "\tiztype {}: user-defined time {}" 1455 | header_str.append(izfmt.format(*vals)) 1456 | elif iztype == 'iunkn': 1457 | header_str.append("\tiztype IUNKN (Unknown)") 1458 | else: 1459 | header_str.append("\tunrecognized iztype: {}".format(iztype)) 1460 | # 1461 | # non-null headers 1462 | hdrfmt = "{:10.10s} = {}" 1463 | for hdr in hdrlist: 1464 | # XXX: non-null header values might have no property for getattr 1465 | try: 1466 | header_str.append(hdrfmt.format(hdr, getattr(self, hdr))) 1467 | except AttributeError: 1468 | header_str.append(hdrfmt.format(hdr, self._header[hdr])) 1469 | 1470 | return '\n'.join(header_str) 1471 | 1472 | def listhdr(self, hdrlist='all'): 1473 | """ 1474 | Print header values. 1475 | 1476 | Default is all non-null values. 1477 | 1478 | :param hdrlist: Which header fields to you want to list. Choose one of 1479 | {'all', 'picks'} or iterable of header fields. An iterable of 1480 | header fields can look like 'bea' or ('b', 'e', 'a'). 1481 | 1482 | 'all' (default) prints all non-null values. 1483 | 'picks' prints fields which are used to define time picks. 1484 | 1485 | An iterable of header fields can look like 'bea' or ('b', 'e', 'a'). 1486 | 1487 | .. rubric:: Example 1488 | 1489 | >>> from obspy.core.util import get_example_file 1490 | >>> file_ = get_example_file("LMOW.BHE.SAC") 1491 | >>> sac = SACTrace.read(file_) 1492 | >>> sac.lh() # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS 1493 | Reference Time = 04/10/2001 (100) 00:23:00.465000 1494 | iztype IB: begin time 1495 | a = 0.0 1496 | b = 0.0 1497 | delta = 0.009999999... 1498 | depmax = 0.003305610... 1499 | depmen = 0.00243799... 1500 | depmin = 0.00148824... 1501 | e = 0.98999997... 1502 | iftype = itime 1503 | iztype = ib 1504 | kcmpnm = BHE 1505 | kevnm = None 1506 | kstnm = LMOW 1507 | lcalda = True 1508 | leven = True 1509 | lpspol = False 1510 | nevid = 0 1511 | norid = 0 1512 | npts = 100 1513 | nvhdr = 6 1514 | nzhour = 0 1515 | nzjday = 100 1516 | nzmin = 23 1517 | nzmsec = 465 1518 | nzsec = 0 1519 | nzyear = 2001 1520 | stla = -39.409999... 1521 | stlo = 175.75 1522 | unused23 = 0 1523 | """ 1524 | # https://ds.iris.edu/files/sac-manual/commands/listhdr.html 1525 | print(self._format_header_str(hdrlist)) 1526 | 1527 | def lh(self, *args, **kwargs): 1528 | """Alias of listhdr method.""" 1529 | self.listhdr(*args, **kwargs) 1530 | 1531 | def __str__(self): 1532 | return self._format_header_str() 1533 | 1534 | def __repr__(self): 1535 | # XXX: run self._flush_headers first? 1536 | # TODO: make this somehow more readable. 1537 | h = sorted(self._header.items()) 1538 | fmt = ", {}={!r}" * len(h) 1539 | argstr = fmt.format(*chain.from_iterable(h))[2:] 1540 | return self.__class__.__name__ + "(" + argstr + ")" 1541 | 1542 | def copy(self): 1543 | return deepcopy(self) 1544 | 1545 | def _flush_headers(self): 1546 | """ 1547 | Flush to the header arrays any header property values that may not be 1548 | reflected there, such as data min/max/mean, npts, e. 1549 | 1550 | """ 1551 | # XXX: do I really care which byte order it is? 1552 | # self.data = np.require(self.data, native_str(' CHNHDR O GMT 1982 123 13 37 10 103 1573 | SAC> LISTHDR O 1574 | O 123.103 1575 | SAC> CHNHDR ALLT -123.103 IZTYPE IO 1576 | 1577 | ...it is recommended to just make sure your target reference header is 1578 | set and correct, and set the iztype: 1579 | 1580 | >>> from obspy import UTCDateTime 1581 | >>> from obspy.core.util import get_example_file 1582 | >>> file_ = get_example_file("test.sac") 1583 | >>> sac = SACTrace.read(file_) 1584 | >>> sac.o = UTCDateTime(year=1982, julday=123, 1585 | ... hour=13, minute=37, 1586 | ... second=10, microsecond=103) 1587 | >>> sac.iztype = 'io' 1588 | 1589 | The iztype setter will deal with shifting the time values. 1590 | 1591 | """ 1592 | for hdr in ['b', 'o', 'a', 'f'] + ['t'+str(i) for i in range(10)]: 1593 | val = getattr(self, hdr) 1594 | if val is not None: 1595 | setattr(self, hdr, val + shift) 1596 | 1597 | def _set_distances(self, force=False): 1598 | """ 1599 | Calculate dist, az, baz, gcarc. If force=True, ignore lcalda. 1600 | Raises SacHeaderError if force=True and geographic headers are unset. 1601 | 1602 | """ 1603 | if self.lcalda or force: 1604 | try: 1605 | m, az, baz = gps2dist_azimuth(self.evla, self.evlo, self.stla, 1606 | self.stlo) 1607 | dist = m / 1000.0 1608 | gcarc = kilometer2degrees(dist) 1609 | self._hf[HD.FLOATHDRS.index('az')] = az 1610 | self._hf[HD.FLOATHDRS.index('baz')] = baz 1611 | self._hf[HD.FLOATHDRS.index('dist')] = dist 1612 | self._hf[HD.FLOATHDRS.index('gcarc')] = gcarc 1613 | except (ValueError, TypeError): 1614 | # one or more of the geographic values is None 1615 | if force: 1616 | msg = ("Not enough information to calculate distance, " 1617 | "azimuth.") 1618 | raise SacHeaderError(msg) 1619 | --------------------------------------------------------------------------------