Skip to content

Commit 9e77ad3

Browse files
authored
PySEP v0.6.0 (#143)
* added read function for asdfdatasets and allow RecSec to plot windows on top of record section (#136) * added test to check zero division error, (#137) added logger warning about zero amplitude scaling feature: user can now set kwargs for obs_color and syn_color * Improve SAC header append functions (#138) * extending read_sem_cartesian test to check for evdp related to #132 adding warning messages to sac header append when evdp or mag not present * extended cartesian sac headers to match all normal sac header values except for cmpinc and cmpaz added better warning message for missing sac header values in normal sac header reading extended tests to cover new functionalities * Improve RecSec preprocessing architecture (#139) * reorganizing docstring, restructuring preprocess flag default value for overwrite is now set True so User does not have to unset changed default preprocess flag to True, removed 'both' option * fixed up check function to reflect new preprocess flag * restructuring preprocessing function to put each individual feature behind a boolean flag so that they can be turned on/off at will also expose some key arguments as keyword arguments so the User has more control * all preprocessing now behind tunable knobs * moved kwargs to args of process_st reorganized init parameter input to organize a bit better added new parameters to main docstring * allow preprocess=True to be more flexible and just take st and st_syn if available, removed hard check on preprocess=True requireing st_syn to be more intutive by users bugfix move fill value to after assignment of trace in loop * moved parameters back to internal kwargs to match recsec structure and allow passing them in through command line * bump version 0.6.0 and bump copyright year in docs * update changelog * slim down changelog to include only most recent
1 parent 0c0e505 commit 9e77ad3

File tree

8 files changed

+338
-99
lines changed

8 files changed

+338
-99
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
- Adds version release documentation
7070
- Slightly modifies pysep-docs conda environment to accomodate converted nbooks
7171

72+
7273
## Version 0.5.0
7374
- Improves functions 'read_forcesolution' and 'read_source', which now return
7475
`obspy.core.event.Event` objects, rather than the makeshift Source objects
@@ -109,6 +110,15 @@
109110
- Removed unnused parameters 'legacy_naming' and 'log_level' from
110111
`Pysep.write_config`
111112

113+
112114
## Version 0.5.1
113115

114116
- Bugfix: RecSec subset streams, which checked that 'st' and 'st_syn' had the same stations, would not run for streams of the same length, leading to edge case where same length streams would plot out of order because they had not been sorted. removed the criteria and now subset streams runs at all times
117+
118+
119+
## Version 0.6.0
120+
121+
- #136: New read function for ASDFDataSets for misfit window plotting
122+
- #137: More control over RecSec kwargs and better warning messages
123+
- #138: Improved SAC header creation for SPECFEM synthetics
124+
- #139: Improved RecSec preprocessing setup, more manual control for the User

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def setup(app):
1515
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
1616

1717
project = 'PySEP'
18-
copyright = '2023, adjTomo Dev Team'
18+
copyright = '2024, adjTomo Dev Team'
1919
author = 'adjTomo Dev Team'
2020
release = ''
2121
# Grab version number from 'pyproject.toml'

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pysep-adjtomo"
7-
version = "0.5.1"
7+
version = "0.6.0"
88
description = "Python Seismogram Extraction and Processing"
99
readme = "README.md"
1010
requires-python = ">=3.8"

pysep/recsec.py

+192-88
Large diffs are not rendered by default.

pysep/tests/test_recsec.py

+21
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,24 @@ def test_recsec_calc_time_offset(recsec_w_synthetics):
137137
recsec_w_synthetics.get_parameters()
138138
for tr in recsec_w_synthetics.st:
139139
assert(tr.stats.time_offset == -100)
140+
141+
def test_recsec_zero_amplitude(recsec):
142+
"""
143+
waveforms that have zero amplitude and are normalized should be able
144+
to bypass normalizations which lead to weird plotting (see #131).
145+
146+
.. note::
147+
148+
This does not really test that the method is working correctly because
149+
dividing a NumPy array by zero leads to NaNs in the array which just
150+
won't plot. This is more of a visual test to make sure that the
151+
zero amplitude is plotting correctly, look for green lines
152+
"""
153+
recsec.kwargs.scale_by = "normalize"
154+
recsec.kwargs.obs_color = "green"
155+
recsec.linewidth = 30
156+
for tr in recsec.st:
157+
tr.data *= 0
158+
recsec.process_st()
159+
recsec.get_parameters()
160+
recsec.plot()

pysep/tests/test_utils.py

+28
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ def test_append_sac_headers(test_st, test_inv, test_event):
5959
assert(st[0].stats.sac["evla"] == test_event.preferred_origin().latitude)
6060

6161

62+
def test_append_sac_headers_cartesian(test_st, test_inv, test_event):
63+
"""
64+
Make sure we can write SAC headers correctly
65+
"""
66+
st = append_sac_headers(st=test_st, inv=test_inv, event=test_event)
67+
assert(not hasattr(test_st[0].stats, "sac"))
68+
assert(hasattr(st[0].stats, "sac"))
69+
assert(st[0].stats.sac["evla"] == test_event.preferred_origin().latitude)
70+
71+
6272
def test_event_tag_and_event_tag_legacy(test_event):
6373
"""
6474
Check that event tagging works as expected
@@ -166,6 +176,14 @@ def test_read_sem():
166176
st += read_sem(fid=test_synthetic, source=test_cmtsolution,
167177
stations=test_stations)
168178
assert(st)
179+
180+
expected_headers = ["iztype", "b", "e", "evla", "evlo", "stla", "stlo",
181+
"stel", "kevnm", "nzyear", "nzjday", "nzhour", "nzmin",
182+
"nzsec", "nzmsec", "dist", "az", "baz", "gcarc",
183+
"lpspol", "lcalda", "evdp", "mag"]
184+
for expected_header in expected_headers:
185+
assert(expected_header in st[0].stats.sac)
186+
169187
assert(st[0].stats.sac.evla == -40.5405)
170188

171189

@@ -182,7 +200,17 @@ def test_read_sem_cartesian():
182200
st += read_sem(fid=test_synthetic, source=test_cmtsolution,
183201
stations=test_stations)
184202
assert(st)
203+
204+
expected_headers = ["iztype", "b", "e", "evla", "evlo", "stla", "stlo",
205+
"kevnm", "nzyear", "nzjday", "nzhour", "nzmin",
206+
"nzsec", "nzmsec", "dist", "az", "baz", "gcarc",
207+
"lpspol", "lcalda", "evdp"]
208+
for expected_header in expected_headers:
209+
assert(expected_header in st[0].stats.sac)
210+
185211
assert(st[0].stats.sac.stla == 67000.0)
212+
assert(st[0].stats.sac.evdp == 30.)
213+
assert(st[0].stats.sac.b == -10.)
186214

187215
def test_estimate_prefilter_corners(test_st):
188216
"""

pysep/utils/cap_sac.py

+23-8
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def _append_sac_headers_trace(tr, event, inv):
155155
156156
We explicitely set 'iztype, 'b' and 'e' in the SAC header to tell ObsPy
157157
that the trace start is NOT the origin time. Otherwise all the relative
158-
timing (e.g., picks) will be wrong.
158+
timing (e.g., picks) in SAC will be wrong.
159159
160160
:type tr: obspy.core.trace.Trace
161161
:param tr: Trace to append SAC header to
@@ -206,6 +206,7 @@ def _append_sac_headers_trace(tr, event, inv):
206206
"lpspol": 0, # 1 if left-hand polarity (usually no in passive seis)
207207
"lcalda": 1, # 1 if DIST, AZ, BAZ, GCARC to be calc'd from metadata
208208
}
209+
209210
# Some Inventory objects will not go all the way to channel, only to station
210211
try:
211212
cha = sta[0]
@@ -227,6 +228,13 @@ def _append_sac_headers_trace(tr, event, inv):
227228
except Exception: # NOQA
228229
pass
229230

231+
# Warn User that the following SAC headers could not be found
232+
_warn_about = []
233+
for key in ["cmpinc", "cmpaz", "evdp", "mag"]:
234+
if key not in sac_header:
235+
_warn_about.append(key)
236+
logger.warning(f"no SAC header values found for: {_warn_about}")
237+
230238
# Append SAC header and include back azimuth for rotation
231239
tr.stats.sac = sac_header
232240
tr.stats.back_azimuth = baz
@@ -297,24 +305,29 @@ def _append_sac_headers_cartesian_trace(tr, event, rcv_x, rcv_y):
297305
:rtype: obspy.core.trace.Trace
298306
:return: Trace with appended SAC header
299307
"""
300-
net_sta = f"{tr.stats.network}.{tr.stats.station}"
301-
302308
src_y = event.preferred_origin().latitude
303309
src_x = event.preferred_origin().longitude
310+
otime = event.preferred_origin().time
311+
evdepth_km = event.preferred_origin().depth / 1E3 # units: m -> km
304312

305313
# Calculate Cartesian distance and azimuth/backazimuth
306314
dist_m = np.sqrt(((rcv_x - src_x) ** 2) + ((rcv_y - src_y) ** 2))
315+
dist_km = dist_m * 1E-3 # units: m -> km
316+
dist_deg = kilometer2degrees(dist_km) # spherical earth approximation
307317
azimuth = np.degrees(np.arctan2((rcv_x - src_x), (rcv_y - src_y))) % 360
308318
backazimuth = (azimuth - 180) % 360
309-
otime = event.preferred_origin().time
310-
311-
# Barebones SAC header, we only append values required by RecSec
319+
312320
sac_header = {
321+
"iztype": 9, # Ref time equivalence, IB (9): Begin time
322+
"b": tr.stats.starttime - otime, # begin time
323+
"e": tr.stats.npts * tr.stats.delta, # end time
313324
"stla": rcv_y,
314325
"stlo": rcv_x,
315326
"evla": src_y,
316327
"evlo": src_x,
317-
"dist": dist_m * 1E-3, # units: km
328+
"evdp": evdepth_km,
329+
"dist": dist_km,
330+
"gcarc": dist_deg, # degrees
318331
"az": azimuth,
319332
"baz": backazimuth,
320333
"kevnm": format_event_tag_legacy(event), # only take date code
@@ -324,7 +337,9 @@ def _append_sac_headers_cartesian_trace(tr, event, rcv_x, rcv_y):
324337
"nzmin": otime.minute,
325338
"nzsec": otime.second,
326339
"nzmsec": otime.microsecond,
327-
}
340+
"lpspol": 0, # 1 if left-hand polarity (usually no in passive seis)
341+
"lcalda": 1, # 1 if DIST, AZ, BAZ, GCARC to be calc'd from metadata
342+
}
328343

329344
tr.stats.sac = sac_header
330345

pysep/utils/io.py

+62-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from pysep import logger
1616
from pysep.utils.mt import moment_magnitude, seismic_moment
17-
from pysep.utils.fmt import format_event_tag_legacy, channel_code
17+
from pysep.utils.fmt import channel_code
1818
from pysep.utils.cap_sac import append_sac_headers, append_sac_headers_cartesian
1919

2020

@@ -498,6 +498,67 @@ def read_event_file(fid):
498498
return list_out
499499

500500

501+
def read_asdfdataset(path, evaluation):
502+
"""
503+
Read an ASDFDataSet created by a SeisFlows Pyaflowa inversion run.
504+
The dataset should contain observed and synthetic waveforms, and
505+
optionally contains misfit windows. Will return Streams with SAC headers
506+
507+
.. note::
508+
509+
This function makes assumptions about the PyASDF data structure which
510+
is defined by the external package Pyatoa. If Pyatoa changes that
511+
structure, this function will break.
512+
513+
:type path: str
514+
:param path: Path to the ASDF dataset to be read
515+
:type evaluation: str
516+
:param evaluation: evaluation to take synthetics from. These are saved
517+
following a format specified by Pyatoa, but usually follow the form
518+
iteration/step_count, e.g., i01s00 gives iteration 1, step count 0.
519+
Take a look at the waveform tags in `ASDFDataSet.waveforms[<station>]`
520+
for tags following the 'synthetic_' prefix
521+
"""
522+
# PySEP, by default will not require PyASDF to be installed
523+
try:
524+
from pyasdf import ASDFDataSet # NOQA
525+
except ImportError:
526+
logger.critical("pyasdf is not installed. Please install pyasdf "
527+
"to read ASDF datasets")
528+
return None, None
529+
530+
with ASDFDataSet(path) as ds:
531+
event = ds.events[0]
532+
st_out = Stream()
533+
st_syn_out = Stream()
534+
for station in ds.waveforms.list():
535+
inv = ds.waveforms[station].StationXML
536+
st = ds.waveforms[station].observed
537+
st_syn = ds.waveforms[station][f"synthetic_{evaluation}"]
538+
539+
st_out += append_sac_headers(st, event, inv)
540+
st_syn_out += append_sac_headers(st_syn, event, inv)
541+
542+
# Read windows from the dataset
543+
windows = {}
544+
if hasattr(ds.auxiliary_data, "MisfitWindows"):
545+
iter_ = evaluation[:3] # 'i01s00' -> 'i01'
546+
step = evaluation[3:]
547+
for station in ds.auxiliary_data.MisfitWindows[iter_][step].list():
548+
parameters = ds.auxiliary_data.MisfitWindows[iter_][step][
549+
station].parameters
550+
trace_id = parameters["channel_id"]
551+
starttime = parameters["relative_starttime"]
552+
endtime = parameters["relative_endtime"]
553+
# initialize empty window array
554+
if trace_id not in windows:
555+
windows[trace_id] = []
556+
557+
windows[trace_id].append((starttime, endtime))
558+
559+
return st_out, st_syn_out, windows
560+
561+
501562
def write_sem(st, unit, path="./", time_offset=0):
502563
"""
503564
Write an ObsPy Stream in the two-column ASCII format that Specfem uses

0 commit comments

Comments
 (0)