Skip to content

Commit f5b8aa5

Browse files
Merge branch 'develop' into weighted-correlations
2 parents 20109be + 3a17a34 commit f5b8aa5

File tree

10 files changed

+213
-41
lines changed

10 files changed

+213
-41
lines changed

CHANGES.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,17 @@
3030
* core.lag_calc._xcorr_interp
3131
- CC-interpolation replaced with resampling (more robust), old method
3232
deprecated. Use new method with use_new_resamp_method=True as **kwarg.
33-
* core.lag_calc:
33+
* core.lag_calc
34+
- Added new option all_vert to transfer P-picks to all channels defined as
35+
vertical_chans.
36+
- Made usage of all_vert, all_horiz consistent across the lag_calc.
3437
- Fixed bug where minimum CC defined via min_cc_from_mean_cc_factor was not
3538
set correctly for negative correlation sums.
39+
* core.template_gen
40+
- Added new option all_vert to transfer P-picks to all channels defined as
41+
vertical_chans.
42+
- Made handling of horizontal_chans and vertical_chans consistent so that user
43+
can freely choose relevant channels.
3644
* utils.correlate
3745
- Weighted correlations supported. Template weights are expected in
3846
individual trace.stats.extra.weight attributes.

eqcorrscan/core/lag_calc.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,9 @@ def _concatenate_and_correlate(streams, template, cores):
223223

224224
def xcorr_pick_family(family, stream, shift_len=0.2, min_cc=0.4,
225225
min_cc_from_mean_cc_factor=None,
226+
all_vert=False, all_horiz=False, vertical_chans=['Z'],
226227
horizontal_chans=['E', 'N', '1', '2'],
227-
vertical_chans=['Z'], cores=1, interpolate=False,
228+
cores=1, interpolate=False,
228229
plot=False, plotdir=None, export_cc=False, cc_dir=None,
229230
**kwargs):
230231
"""
@@ -281,7 +282,9 @@ def xcorr_pick_family(family, stream, shift_len=0.2, min_cc=0.4,
281282
picked_dict = {}
282283
delta = family.template.st[0].stats.delta
283284
detect_streams_dict = _prepare_data(
284-
family=family, detect_data=stream, shift_len=shift_len)
285+
family=family, detect_data=stream, shift_len=shift_len,
286+
all_vert=all_vert, all_horiz=all_horiz, vertical_chans=vertical_chans,
287+
horizontal_chans=horizontal_chans)
285288
detection_ids = list(detect_streams_dict.keys())
286289
detect_streams = [detect_streams_dict[detection_id]
287290
for detection_id in detection_ids]
@@ -398,7 +401,9 @@ def xcorr_pick_family(family, stream, shift_len=0.2, min_cc=0.4,
398401
return picked_dict
399402

400403

401-
def _prepare_data(family, detect_data, shift_len):
404+
def _prepare_data(family, detect_data, shift_len, all_vert=False,
405+
all_horiz=False, vertical_chans=['Z'],
406+
horizontal_chans=['E', 'N', '1', '2']):
402407
"""
403408
Prepare data for lag_calc - reduce memory here.
404409
@@ -425,7 +430,9 @@ def _prepare_data(family, detect_data, shift_len):
425430
"samples".format(length))
426431
prepick = shift_len + family.template.prepick
427432
detect_streams_dict = family.extract_streams(
428-
stream=detect_data, length=length, prepick=prepick)
433+
stream=detect_data, length=length, prepick=prepick,
434+
all_vert=all_vert, all_horiz=all_horiz, vertical_chans=vertical_chans,
435+
horizontal_chans=horizontal_chans)
429436
for key, detect_stream in detect_streams_dict.items():
430437
# Split to remove trailing or leading masks
431438
for i in range(len(detect_stream) - 1, -1, -1):
@@ -453,6 +460,7 @@ def _prepare_data(family, detect_data, shift_len):
453460

454461
def lag_calc(detections, detect_data, template_names, templates,
455462
shift_len=0.2, min_cc=0.4, min_cc_from_mean_cc_factor=None,
463+
all_vert=False, all_horiz=False,
456464
horizontal_chans=['E', 'N', '1', '2'],
457465
vertical_chans=['Z'], cores=1, interpolate=False,
458466
plot=False, plotdir=None, export_cc=False, cc_dir=None, **kwargs):
@@ -599,6 +607,7 @@ def lag_calc(detections, detect_data, template_names, templates,
599607
template_dict = xcorr_pick_family(
600608
family=family, stream=detect_data, min_cc=min_cc,
601609
min_cc_from_mean_cc_factor=min_cc_from_mean_cc_factor,
610+
all_vert=all_vert, all_horiz=all_horiz,
602611
horizontal_chans=horizontal_chans,
603612
vertical_chans=vertical_chans, interpolate=interpolate,
604613
cores=cores, shift_len=shift_len, plot=plot, plotdir=plotdir,

eqcorrscan/core/match_filter/detection.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,9 @@ def _calculate_event(self, template=None, template_st=None,
350350
self.event = ev
351351
return self
352352

353-
def extract_stream(self, stream, length, prepick):
353+
def extract_stream(self, stream, length, prepick, all_vert=False,
354+
all_horiz=False, vertical_chans=['Z'],
355+
horizontal_chans=['E', 'N', '1', '2']):
354356
"""
355357
Extract a cut stream of a given length around the detection.
356358
@@ -376,7 +378,17 @@ def extract_stream(self, stream, length, prepick):
376378
pick = [
377379
p for p in self.event.picks
378380
if p.waveform_id.station_code == station and
379-
p.waveform_id.channel_code == channel]
381+
p.waveform_id.channel_code[0:-1] == channel[0:-1]]
382+
# Allow picks to be transferred to other vertical/horizontal chans
383+
if all_vert and channel[-1] in vertical_chans:
384+
pick = [p for p in pick
385+
if p.waveform_id.channel_code[-1] in vertical_chans]
386+
elif all_horiz and channel[-1] in horizontal_chans:
387+
pick = [p for p in pick
388+
if p.waveform_id.channel_code[-1] in horizontal_chans]
389+
else:
390+
pick = [p for p in pick
391+
if p.waveform_id.channel_code == channel]
380392
if len(pick) == 0:
381393
Logger.info("No pick for {0}.{1}".format(station, channel))
382394
continue

eqcorrscan/core/match_filter/family.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,8 @@ def write(self, filename, format='tar', overwrite=False):
507507
return
508508

509509
def lag_calc(self, stream, pre_processed, shift_len=0.2, min_cc=0.4,
510-
min_cc_from_mean_cc_factor=None,
511-
horizontal_chans=['E', 'N', '1', '2'], vertical_chans=['Z'],
510+
min_cc_from_mean_cc_factor=None, vertical_chans=['Z'],
511+
horizontal_chans=['E', 'N', '1', '2'],
512512
cores=1, interpolate=False, plot=False, plotdir=None,
513513
parallel=True, process_cores=None, ignore_length=False,
514514
ignore_bad_data=False, export_cc=False, cc_dir=None,
@@ -779,7 +779,9 @@ def _process_streams(self, stream, pre_processed, process_cores=1,
779779
processed_stream = stream.merge()
780780
return processed_stream.split()
781781

782-
def extract_streams(self, stream, length, prepick):
782+
def extract_streams(self, stream, length, prepick, all_vert=False,
783+
all_horiz=False, vertical_chans=['Z'],
784+
horizontal_chans=['E', 'N', '1', '2']):
783785
"""
784786
Generate a dictionary of cut streams around detections.
785787
@@ -797,7 +799,9 @@ def extract_streams(self, stream, length, prepick):
797799
"""
798800
# Splitting and merging to remove trailing and leading masks
799801
return {d.id: d.extract_stream(
800-
stream=stream, length=length, prepick=prepick).split().merge()
802+
stream=stream, length=length, prepick=prepick, all_vert=all_vert,
803+
all_horiz=all_horiz, vertical_chans=vertical_chans,
804+
horizontal_chans=horizontal_chans).split().merge()
801805
for d in self.detections}
802806

803807

eqcorrscan/core/match_filter/party.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,8 +833,8 @@ def read(self, filename=None, read_detection_catalog=True,
833833
return self
834834

835835
def lag_calc(self, stream, pre_processed, shift_len=0.2, min_cc=0.4,
836-
min_cc_from_mean_cc_factor=None,
837-
horizontal_chans=['E', 'N', '1', '2'], vertical_chans=['Z'],
836+
min_cc_from_mean_cc_factor=None, vertical_chans=['Z'],
837+
horizontal_chans=['E', 'N', '1', '2'],
838838
cores=1, interpolate=False, plot=False, plotdir=None,
839839
parallel=True, process_cores=None, ignore_length=False,
840840
ignore_bad_data=False, export_cc=False, cc_dir=None,

eqcorrscan/core/template_gen.py

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ def __str__(self):
5252

5353
def template_gen(method, lowcut, highcut, samp_rate, filt_order,
5454
length, prepick, swin="all", process_len=86400,
55-
all_horiz=False, delayed=True, plot=False, plotdir=None,
56-
return_event=False, min_snr=None, parallel=False,
57-
num_cores=False, save_progress=False, skip_short_chans=False,
58-
**kwargs):
55+
all_vert=False, all_horiz=False, delayed=True, plot=False,
56+
plotdir=None, return_event=False, min_snr=None,
57+
parallel=False, num_cores=False, save_progress=False,
58+
skip_short_chans=False, vertical_chans=['Z'],
59+
horizontal_chans=['E', 'N', '1', '2'], **kwargs):
5960
"""
6061
Generate processed and cut waveforms for use as templates.
6162
@@ -82,6 +83,10 @@ def template_gen(method, lowcut, highcut, samp_rate, filt_order,
8283
:func:`eqcorrscan.core.template_gen.template_gen`
8384
:type process_len: int
8485
:param process_len: Length of data in seconds to download and process.
86+
:type all_vert: bool
87+
:param all_vert:
88+
To use all channels defined in vertical_chans for P-arrivals even if
89+
there is only a pick on one of them. Defaults to False.
8590
:type all_horiz: bool
8691
:param all_horiz:
8792
To use both horizontal channels even if there is only a pick on one of
@@ -119,18 +124,26 @@ def template_gen(method, lowcut, highcut, samp_rate, filt_order,
119124
Whether to ignore channels that have insufficient length data or not.
120125
Useful when the quality of data is not known, e.g. when downloading
121126
old, possibly triggered data from a datacentre
127+
:type vertical_chans: list
128+
:param vertical_chans:
129+
List of channel endings on which P-picks are accepted.
130+
:type horizontal_chans: list
131+
:param horizontal_chans:
132+
List of channel endings for horizontal channels, on which S-picks are
133+
accepted.
122134
123135
:returns: List of :class:`obspy.core.stream.Stream` Templates
124136
:rtype: list
125137
126138
.. note::
127139
By convention templates are generated with P-phases on the
128-
vertical channel and S-phases on the horizontal channels, normal
129-
seismograph naming conventions are assumed, where Z denotes vertical
130-
and N, E, R, T, 1 and 2 denote horizontal channels, either oriented
131-
or not. To this end we will **only** use Z channels if they have a
132-
P-pick, and will use one or other horizontal channels **only** if
133-
there is an S-pick on it.
140+
vertical channel [can be multiple, e.g., Z (vertical) and H
141+
(hydrophone) for an ocean bottom seismometer] and S-phases on the
142+
horizontal channels. By default, normal seismograph naming conventions
143+
are assumed, where Z denotes vertical and N, E, 1 and 2 denote
144+
horizontal channels, either oriented or not. To this end we will
145+
**only** use vertical channels if they have a P-pick, and will use one
146+
or other horizontal channels **only** if there is an S-pick on it.
134147
135148
.. warning::
136149
If there is no phase_hint included in picks, and swin=all, all channels
@@ -388,8 +401,9 @@ def template_gen(method, lowcut, highcut, samp_rate, filt_order,
388401
# Cut and extract the templates
389402
template = _template_gen(
390403
event.picks, st, length, swin, prepick=prepick, plot=plot,
391-
all_horiz=all_horiz, delayed=delayed, min_snr=min_snr,
392-
plotdir=plotdir)
404+
all_vert=all_vert, all_horiz=all_horiz, delayed=delayed,
405+
min_snr=min_snr, vertical_chans=vertical_chans,
406+
horizontal_chans=horizontal_chans, plotdir=plotdir)
393407
process_lengths.append(len(st[0].data) / samp_rate)
394408
temp_list.append(template)
395409
catalog_out += event
@@ -582,9 +596,10 @@ def _rms(array):
582596
return np.sqrt(np.mean(np.square(array)))
583597

584598

585-
def _template_gen(picks, st, length, swin='all', prepick=0.05,
599+
def _template_gen(picks, st, length, swin='all', prepick=0.05, all_vert=False,
586600
all_horiz=False, delayed=True, plot=False, min_snr=None,
587-
plotdir=None):
601+
plotdir=None, vertical_chans=['Z'],
602+
horizontal_chans=['E', 'N', '1', '2']):
588603
"""
589604
Master function to generate a multiplexed template for a single event.
590605
@@ -607,6 +622,10 @@ def _template_gen(picks, st, length, swin='all', prepick=0.05,
607622
:param prepick:
608623
Length in seconds to extract before the pick time default is 0.05
609624
seconds.
625+
:type all_vert: bool
626+
:param all_vert:
627+
To use all channels defined in vertical_chans for P-arrivals even if
628+
there is only a pick on one of them. Defaults to False.
610629
:type all_horiz: bool
611630
:param all_horiz:
612631
To use both horizontal channels even if there is only a pick on one
@@ -630,18 +649,26 @@ def _template_gen(picks, st, length, swin='all', prepick=0.05,
630649
:param plotdir:
631650
The path to save plots to. If `plotdir=None` (default) then the figure
632651
will be shown on screen.
652+
:type vertical_chans: list
653+
:param vertical_chans:
654+
List of channel endings on which P-picks are accepted.
655+
:type horizontal_chans: list
656+
:param horizontal_chans:
657+
List of channel endings for horizontal channels, on which S-picks are
658+
accepted.
633659
634660
:returns: Newly cut template.
635661
:rtype: :class:`obspy.core.stream.Stream`
636662
637663
.. note::
638664
By convention templates are generated with P-phases on the
639-
vertical channel and S-phases on the horizontal channels, normal
640-
seismograph naming conventions are assumed, where Z denotes vertical
641-
and N, E, R, T, 1 and 2 denote horizontal channels, either oriented
642-
or not. To this end we will **only** use Z channels if they have a
643-
P-pick, and will use one or other horizontal channels **only** if
644-
there is an S-pick on it.
665+
vertical channel [can be multiple, e.g., Z (vertical) and H
666+
(hydrophone) for an ocean bottom seismometer] and S-phases on the
667+
horizontal channels. By default, normal seismograph naming conventions
668+
are assumed, where Z denotes vertical and N, E, 1 and 2 denote
669+
horizontal channels, either oriented or not. To this end we will
670+
**only** use vertical channels if they have a P-pick, and will use one
671+
or other horizontal channels **only** if there is an S-pick on it.
645672
646673
.. note::
647674
swin argument: Setting to `P` will return only data for channels
@@ -735,13 +762,18 @@ def _template_gen(picks, st, length, swin='all', prepick=0.05,
735762
continue
736763
starttime.update({'picks': s_pick})
737764
elif _swin == 'all':
738-
if all_horiz and tr.stats.channel[-1] in ['1', '2', '3',
739-
'N', 'E']:
765+
if all_vert and tr.stats.channel[-1] in vertical_chans:
766+
# Get all picks on vertical channels
767+
channel_pick = [
768+
pick for pick in station_picks
769+
if pick.waveform_id.channel_code[-1] in
770+
vertical_chans]
771+
elif all_horiz and tr.stats.channel[-1] in horizontal_chans:
740772
# Get all picks on horizontal channels
741773
channel_pick = [
742774
pick for pick in station_picks
743775
if pick.waveform_id.channel_code[-1] in
744-
['1', '2', '3', 'N', 'E']]
776+
horizontal_chans]
745777
else:
746778
channel_pick = [
747779
pick for pick in station_picks
@@ -751,16 +783,19 @@ def _template_gen(picks, st, length, swin='all', prepick=0.05,
751783
starttime.update({'picks': channel_pick})
752784
elif _swin == 'P':
753785
p_pick = [pick for pick in station_picks
754-
if pick.phase_hint.upper()[0] == 'P' and
755-
pick.waveform_id.channel_code == tr.stats.channel]
786+
if pick.phase_hint.upper()[0] == 'P']
787+
if not all_vert:
788+
p_pick = [pick for pick in p_pick
789+
if pick.waveform_id.channel_code ==
790+
tr.stats.channel]
756791
if len(p_pick) == 0:
757792
Logger.debug(
758793
f"No picks with phase_hint P "
759794
f"found for {tr.stats.station}.{tr.stats.channel}")
760795
continue
761796
starttime.update({'picks': p_pick})
762797
elif _swin == 'S':
763-
if tr.stats.channel[-1] in ['Z', 'U']:
798+
if tr.stats.channel[-1] in vertical_chans:
764799
continue
765800
s_pick = [pick for pick in station_picks
766801
if pick.phase_hint.upper()[0] == 'S']

eqcorrscan/tests/template_gen_test.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,8 @@ def test_swin_P(self):
458458

459459
def test_swin_all_and_all_horiz(self):
460460
template = _template_gen(self.picks, self.st.copy(), 10, swin='all',
461-
all_horiz=True)
461+
all_horiz=True,
462+
horizontal_chans=['E', 'N', '1', '2', '3'])
462463
for pick in self.picks:
463464
if pick.phase_hint == 'S':
464465
self.assertGreaterEqual(
@@ -487,6 +488,57 @@ def test_triggered_data(self):
487488
self.assertEqual(len(templates), 0)
488489

489490

491+
class TestEdgeGenObs(unittest.TestCase):
492+
@classmethod
493+
# Extra test case with OBS data with hydrophone channels (HDH) and T-phases
494+
def setUpClass(cls):
495+
import eqcorrscan
496+
cls.testing_path = os.path.dirname(eqcorrscan.__file__) + '/tests'
497+
log = logging.getLogger(template_gen_module.__name__)
498+
cls._log_handler = MockLoggingHandler(level='DEBUG')
499+
log.addHandler(cls._log_handler)
500+
cls.log_messages = cls._log_handler.messages
501+
cls.st = read(os.path.join(
502+
cls.testing_path, 'test_data', 'WAV', 'TEST_',
503+
'2019-08-09-1558-47M.NNSN__038'))
504+
# for tr in cls.st:
505+
# tr.stats.channel = tr.stats.channel[0] + tr.stats.channel[-1]
506+
# Sfile in New Nordic format
507+
event = read_events(os.path.join(
508+
cls.testing_path, 'test_data', 'REA', 'TEST_',
509+
'09-1558-48R.S201908'))[0]
510+
cat = filter_picks(
511+
Catalog([event]), stations=['KBS', 'OBIN1', 'OBIN2', 'SPA0',
512+
'NOR', 'DAG', 'HOPEN', 'HSPB'])
513+
cls.picks = cat[0].picks
514+
515+
def setUp(self):
516+
self._log_handler.reset()
517+
518+
def test_swin_all_and_all_vert_and_all_horiz(self):
519+
# Test that the hydrophone channel on an OBS is included in the
520+
# creation of the vertical channel (P-arrival) template.
521+
template = _template_gen(self.picks, self.st.copy(), 20, swin='all',
522+
all_horiz=True, all_vert=True,
523+
vertical_chans=['Z', 'H'])
524+
for pick in self.picks:
525+
if pick.phase_hint and pick.phase_hint[0] == 'P':
526+
self.assertGreaterEqual(
527+
len(template.select(
528+
station=pick.waveform_id.station_code,
529+
channel='??[ZH]')), 1)
530+
if pick.phase_hint and pick.phase_hint[0] == 'S':
531+
self.assertGreaterEqual(
532+
len(template.select(
533+
station=pick.waveform_id.station_code,
534+
channel='??[NE12]')), 2)
535+
if pick.phase_hint and pick.phase_hint[0] == 'T':
536+
self.assertGreaterEqual(
537+
len(template.select(
538+
station=pick.waveform_id.station_code,
539+
channel='??[ZH]')), 2)
540+
541+
490542
class TestDayLong(unittest.TestCase):
491543
@classmethod
492544
def setUpClass(cls):

0 commit comments

Comments
 (0)