Source code for seiscat.fetchdata.sds
# -*- coding: utf8 -*-
# SPDX-License-Identifier: GPL-3.0-or-later
"""
Fetch event waveforms from a local SDS archive.
:copyright:
2022-2026 Claudio Satriano <satriano@ipgp.fr>
:license:
GNU General Public License v3.0 or later
(https://www.gnu.org/licenses/gpl-3.0-standalone.html)
"""
import pathlib
import re
from obspy.clients.filesystem.sds import Client
from .event_waveforms_utils import (
prefer_high_sampling_rate, check_station, get_picked_station_codes
)
[docs]
def get_sds_client(sds_root):
"""
Get an SDS client.
:param sds_root: path to SDS archive
:type sds_root: str
:return: SDS client
:rtype: obspy.clients.filesystem.sds.Client
"""
client = Client(sds_root)
# Ensure the archive has at least one NSLC entry before accepting it.
if client.get_all_nslc():
return client
else:
raise FileNotFoundError(
f'No SDS archive found in {sds_root}')
def _check_channel(channel, channel_codes):
"""
Check if a channel matches the specified channel codes.
The channel codes can contain wildcards:
- '*' matches any number of characters
- '?' matches a single character
:param channel: channel code
:type channel: str
:param channel_codes: string with channel codes separated by commas
:type channel_codes: str
:return: True if the channel is in the list of channel priorities
:rtype: bool
"""
# Escape dots, replace `?` with `.`, replace `*` with `.*`,
# and split by comma
regex_parts = [
code.replace('.', r'\.') # Escape dots if any
.replace('?', '.') # Single-character wildcard
.replace('*', '.*') # Multi-character wildcard
for code in channel_codes.split(',')
]
# Join with `|` to create an OR pattern
regex = f"^({'|'.join(regex_parts)})$"
return bool(re.match(regex, channel))
[docs]
def fetch_sds_waveforms(config, event, client):
"""
Fetch event waveforms from a local SDS archive.
:param config: config object
:type config: dict
:param event: an event dictionary
:type event: dict
:param client: SDS client
:type client: obspy.clients.filesystem.sds.Client
"""
evid = event['evid']
event_dir = pathlib.Path(config['event_dir'])
evid_dir = event_dir / f'{evid}'
waveform_dir = pathlib.Path(evid_dir / config['waveform_dir'])
waveform_dir.mkdir(parents=True, exist_ok=True)
seconds_before = config['seconds_before_origin']
seconds_after = config['seconds_after_origin']
t0 = event['time'] - seconds_before
t1 = event['time'] + seconds_after
channel_codes = config['channel_codes']
station_codes = config['station_codes']
picked_stations_only = config['picked_stations_only']
# Build set of allowed stations from picks, if requested
picked_stations = None
if picked_stations_only:
picked_stations = get_picked_station_codes(evid_dir, evid)
if picked_stations is None:
print(
f'{evid}: picked_stations_only is True but no event QuakeML '
f'file found at {evid_dir / f"{evid}.xml"}. '
'Ignoring pick-based station selection.'
)
elif not picked_stations:
print(
f'{evid}: No P/S picks found in the event QuakeML file. '
'No waveforms will be downloaded.'
)
return
else:
if station_codes:
picked_stations = {
sta for sta in picked_stations
if check_station(sta, station_codes)
}
if not picked_stations:
print(
f'{evid}: No picked stations match station_codes '
f'"{station_codes}". No waveforms will be downloaded.'
)
return
print(f'Fetching waveforms for event: {evid}')
all_nslc = client.get_all_nslc()
for nslc in all_nslc:
net, sta, loc, chan = nslc
if channel_codes and not _check_channel(chan, channel_codes):
continue
if (
station_codes
and picked_stations is None
and not check_station(sta, station_codes)
):
# Only station_codes filter, no picks filter
continue
if picked_stations is not None and sta not in picked_stations:
continue
st = client.get_waveforms(net, sta, loc, chan, t0, t1)
outfile = waveform_dir / f'{net}.{sta}.{loc}.{chan}.mseed'
st.write(outfile, format='MSEED')
print(f' {outfile} written')
if config['prefer_high_sampling_rate']:
prefer_high_sampling_rate(waveform_dir)
print()