Add PhaseAssociator code
This commit is contained in:
parent
092335b36d
commit
686626108e
114
PyOctoPhaseAssociation.py
Normal file
114
PyOctoPhaseAssociation.py
Normal file
@ -0,0 +1,114 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from obspy import read_inventory, Catalog
|
||||
from obspy.core.inventory import Inventory
|
||||
|
||||
from pyoctoAssociator import PhaseAssociation
|
||||
from catalogConverter import CatalogConverter
|
||||
from qml2picks import qml2picks
|
||||
from count_picks import count_picks_from_stations
|
||||
|
||||
|
||||
def main(qml_picks, inventory, velocity_model, lat_min, lat_max, lon_min, lon_max, z_min,
|
||||
z_max, grid_hor_extent, grid_spacing, p_vel_const, s_vel_const,
|
||||
n_picks=4, n_p_picks=0, n_s_picks=0, n_p_s_picks=0, time_before=10.0, tolerance=1.0, assoc_cut_dist=250.0,
|
||||
pick_match_tolerance=0.5, p_hint="P", s_hint="S"):
|
||||
"""
|
||||
Main function for application: PHASEASSOCIATIONAPP
|
||||
Arguments:
|
||||
velocity_model: input file of type 'mat'
|
||||
qml_picks: input file of type 'quakeml_seismogram_picks'
|
||||
inventory: input file of type 'inventory'
|
||||
grid_hor_extent: parameter of type 'DOUBLE'
|
||||
grid_spacing: parameter of type 'DOUBLE'
|
||||
lat_min: parameter of type 'DOUBLE'
|
||||
lat_max: parameter of type 'DOUBLE'
|
||||
lon_min: parameter of type 'DOUBLE'
|
||||
lon_max: parameter of type 'DOUBLE'
|
||||
z_min: parameter of type 'DOUBLE'
|
||||
z_max: parameter of type 'DOUBLE'
|
||||
n_picks: parameter of type 'INTEGER'
|
||||
n_p_picks: parameter of type 'INTEGER'
|
||||
n_s_picks: parameter of type 'INTEGER'
|
||||
n_p_s_picks: parameter of type 'INTEGER'
|
||||
pick_match_tolerance: parameter of type 'DOUBLE'
|
||||
p_vel_const: parameter of type 'DOUBLE'
|
||||
s_vel_const: parameter of type 'DOUBLE'
|
||||
p_hint: parameter of type 'STRING'
|
||||
s_hint: parameter of type 'STRING'
|
||||
time_before: parameter of type 'DOUBLE'
|
||||
tolerance: parameter of type 'DOUBLE'
|
||||
assoc_cut_dist: parameter of type 'DOUBLE'
|
||||
Returns:
|
||||
File(s) named 'out_catalog' of type 'data_catalog_csv' and format 'PLAIN_TEXT' from working directory
|
||||
"""
|
||||
|
||||
inv = Inventory()
|
||||
for inventory_item in inventory:
|
||||
inv.extend(read_inventory(inventory_item))
|
||||
|
||||
if count_picks_from_stations(quakeml_file=qml_picks, inventory=inv) == 0:
|
||||
logging.error(f"There are no stations in the Station Inventory file(s) corresponding to the picked times. "
|
||||
f"Check if the file(s) is/are correct")
|
||||
sys.exit(1)
|
||||
|
||||
#convert input velocity from m/s to km/s
|
||||
if p_vel_const is not None:
|
||||
p_vel_const = p_vel_const / 1000
|
||||
if s_vel_const is not None:
|
||||
s_vel_const = s_vel_const / 1000
|
||||
|
||||
config_pyocto = {
|
||||
"velocity": {
|
||||
"path": velocity_model,
|
||||
"path_out": "velocity_model_1D",
|
||||
"0D_P_vel": p_vel_const,
|
||||
"0D_S_vel": s_vel_const,
|
||||
"grid_hor_extent": grid_hor_extent,
|
||||
"grid_ver_extent": z_max-z_min,
|
||||
"grid_spacing": grid_spacing
|
||||
},
|
||||
"grid": {
|
||||
"lat_min": lat_min,
|
||||
"lat_max": lat_max,
|
||||
"lon_min": lon_min,
|
||||
"lon_max": lon_max,
|
||||
"z_min": z_min,
|
||||
"z_max": z_max
|
||||
},
|
||||
"params": {
|
||||
"time_before": time_before,
|
||||
"n_picks": n_picks,
|
||||
"n_p_picks": n_p_picks,
|
||||
"n_s_picks": n_s_picks,
|
||||
"n_p_and_s_picks": n_p_s_picks,
|
||||
"tolerance": tolerance,
|
||||
"association_cutoff_distance": assoc_cut_dist,
|
||||
"pick_match_tolerance": pick_match_tolerance
|
||||
}
|
||||
}
|
||||
|
||||
config_conv = {
|
||||
"P_hint": p_hint,
|
||||
"S_hint": s_hint,
|
||||
}
|
||||
picks = qml2picks(qml_picks, p_hint, s_hint)
|
||||
if not picks:
|
||||
# Saving empty qml file
|
||||
cat = Catalog()
|
||||
cat.write("detections.xml", format="QUAKEML")
|
||||
else:
|
||||
pa = PhaseAssociation(inventory=inv, config=config_pyocto)
|
||||
events, assignments = pa.associate_phases(picks)
|
||||
if not events.empty:
|
||||
events['datetime'] = pd.to_datetime(events['time'], unit='s')
|
||||
cc = CatalogConverter(config_conv, picks, events, assignments, "PyOcto")
|
||||
cc.catalog2obspy()
|
||||
cc.save_catalog_to_file("detections.xml")
|
||||
else:
|
||||
# Saving empty qml file
|
||||
cat = Catalog()
|
||||
cat.write("detections.xml", format="QUAKEML")
|
120
catalogConverter.py
Normal file
120
catalogConverter.py
Normal file
@ -0,0 +1,120 @@
|
||||
import pandas as pd
|
||||
|
||||
from obspy.core.event import Catalog
|
||||
from obspy.core.event import Event
|
||||
from obspy.core.event import Pick
|
||||
from obspy.core.event import Origin
|
||||
from obspy.core.event import OriginQuality
|
||||
|
||||
from obspy import UTCDateTime
|
||||
from obspy.core.event.base import WaveformStreamID, Comment
|
||||
|
||||
|
||||
class CatalogConverter:
|
||||
"""
|
||||
Class for converting SeisBench and pyOcto detection results to ObsPy Catalog, which can be saved as QuakeML.
|
||||
"""
|
||||
def __init__(self, config, picks, catalog_df, assignments_df, name_of_algorithm):
|
||||
self._catalog_df = catalog_df
|
||||
self._assignments_df = assignments_df
|
||||
self._picks = picks
|
||||
self._config = config
|
||||
self._name_of_algorithm = name_of_algorithm
|
||||
self.catalog = None
|
||||
|
||||
def _convert_pick(self, p):
|
||||
"""
|
||||
Function converts picks from SeisBench to ObsPy format
|
||||
:param p: SeisBench pick
|
||||
:return: ObsPy pick
|
||||
"""
|
||||
pick = Pick()
|
||||
pick.time = UTCDateTime(p.peak_time.datetime)
|
||||
pick.waveform_id = WaveformStreamID(network_code=p.trace_id.split(".")[0],
|
||||
station_code=p.trace_id.split(".")[1],
|
||||
channel_code=p.trace_id.split(".")[2])
|
||||
if p.phase == 'P':
|
||||
pick.phase_hint = self._config["P_hint"]
|
||||
elif p.phase == 'S':
|
||||
pick.phase_hint = self._config["S_hint"]
|
||||
pick.evaluation_mode = 'automatic'
|
||||
pick.evaluation_status = 'preliminary'
|
||||
return pick
|
||||
|
||||
def _convert_origin(self, origin_sb, list_of_picks_sb):
|
||||
origin = Origin()
|
||||
origin.time = UTCDateTime(pd.to_datetime(origin_sb.time, unit='s').to_pydatetime())
|
||||
origin.latitude = origin_sb.latitude # float
|
||||
origin.longitude = origin_sb.longitude # float
|
||||
origin.depth = origin_sb.depth # float in kilometers (SWIP5 origin version) down the see level
|
||||
origin.depth_type = 'operator assigned'
|
||||
# TODO: make sure that region is not necessary
|
||||
# origin.region = self._config["region"]
|
||||
origin.evaluation_mode = "automatic"
|
||||
origin.evaluation_status = 'preliminary'
|
||||
origin.comments.append(Comment(text=f"Localized by: {self._name_of_algorithm}", force_resource_id=False))
|
||||
origin.quality = OriginQuality(used_phase_count=len(list_of_picks_sb))
|
||||
return origin
|
||||
|
||||
def _convert_event(self, origin_sb, list_of_picks_sb):
|
||||
"""
|
||||
Function convert GaMMa detection to ObsPy Event
|
||||
:param origin_sb:
|
||||
:param list_of_picks_sb:
|
||||
:return:
|
||||
"""
|
||||
event = Event()
|
||||
for p in list_of_picks_sb:
|
||||
pick = self._convert_pick(p)
|
||||
event.picks.append(pick)
|
||||
origin = self._convert_origin(origin_sb, list_of_picks_sb)
|
||||
event.origins.append(origin)
|
||||
return event
|
||||
|
||||
@staticmethod
|
||||
def _append_pick_trace_id(pick, stream):
|
||||
"""
|
||||
Function assigns channel to pick - it is useful for work with SWIP
|
||||
:param pick:
|
||||
:param stream:
|
||||
:return:
|
||||
"""
|
||||
channel = stream[0].stats.channel
|
||||
if pick.phase == "P":
|
||||
pick.trace_id = pick.trace_id + channel[:-1] + "Z"
|
||||
if pick.phase == "S":
|
||||
pick.trace_id = pick.trace_id + channel[:-1] + "E"
|
||||
return pick
|
||||
|
||||
def catalog2obspy(self):
|
||||
"""
|
||||
Function convert GaMMa catalog and SeisBench picks
|
||||
:return: ObsPy Catalog object
|
||||
"""
|
||||
# TODO: make sure that resource id is necessary
|
||||
#cat = Catalog(resource_id=self._config["resource_id"])
|
||||
cat = Catalog()
|
||||
for j, row in self._catalog_df.iterrows():
|
||||
event = self._catalog_df.iloc[j]
|
||||
event_picks = [self._picks[i] for i in
|
||||
self._assignments_df[self._assignments_df["event_idx"] ==
|
||||
event["idx"]]["pick_idx"]]
|
||||
event_obspy = self._convert_event(event, event_picks)
|
||||
cat.append(event_obspy)
|
||||
self.catalog = cat
|
||||
|
||||
def save_catalog_to_file(self, file_path):
|
||||
"""
|
||||
Save ObsPy catalog to a file.
|
||||
|
||||
Args:
|
||||
file_path (str): The file path where the catalog will be saved.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
self.catalog.write(file_path, format="QUAKEML")
|
||||
print(f"Catalog saved successfully to {file_path}")
|
||||
except Exception as e:
|
||||
print(f"Error occurred while saving catalog: {e}")
|
30
count_picks.py
Normal file
30
count_picks.py
Normal file
@ -0,0 +1,30 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from obspy import read_inventory, read_events
|
||||
|
||||
|
||||
def count_picks_from_stations(quakeml_file, inventory):
|
||||
|
||||
# Read the QuakeML file
|
||||
try:
|
||||
events = read_events(quakeml_file)
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading QuakeML file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Extract station network and station codes from the inventory
|
||||
stations = set()
|
||||
for network in inventory:
|
||||
for station in network:
|
||||
stations.add((network.code, station.code))
|
||||
|
||||
# Count the number of picks in the QuakeML file that come from the stations in the inventory
|
||||
pick_count = 0
|
||||
for event in events:
|
||||
for pick in event.picks:
|
||||
net_sta = (pick.waveform_id.network_code, pick.waveform_id.station_code)
|
||||
if net_sta in stations:
|
||||
pick_count += 1
|
||||
|
||||
return pick_count
|
59
phase_associator_wrapper.py
Normal file
59
phase_associator_wrapper.py
Normal file
@ -0,0 +1,59 @@
|
||||
# -----------------
|
||||
# Copyright © 2024 ACK Cyfronet AGH, Poland.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# This work was partially funded by DT-GEO Project.
|
||||
# -----------------
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
from PyOctoPhaseAssociation import main as phaseassoc
|
||||
|
||||
|
||||
def main(argv):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("qml_picks", help="Path to input file of type 'quakeml_seismogram_picks'")
|
||||
parser.add_argument("inventory", nargs='+', help="Paths to input files of type 'inventory'")
|
||||
|
||||
parser.add_argument("--velocity_model", help="Path to input file of type 'velocity_model'", type=str,
|
||||
required=False)
|
||||
parser.add_argument("--lat_min", type=float)
|
||||
parser.add_argument("--lat_max", type=float)
|
||||
parser.add_argument("--lon_min", type=float)
|
||||
parser.add_argument("--lon_max", type=float)
|
||||
parser.add_argument("--z_min", type=float)
|
||||
parser.add_argument("--z_max", type=float)
|
||||
parser.add_argument("--grid_hor_extent", type=float, default=None, required=False)
|
||||
parser.add_argument("--grid_spacing", type=float, default=None, required=False)
|
||||
parser.add_argument("--p_vel_const", type=float, default=None, required=False)
|
||||
parser.add_argument("--s_vel_const", type=float, default=None, required=False)
|
||||
parser.add_argument("--n_picks", type=int, default=4, required=False)
|
||||
parser.add_argument("--n_p_picks", type=int, default=0, required=False)
|
||||
parser.add_argument("--n_s_picks", type=int, default=0, required=False)
|
||||
parser.add_argument("--n_p_s_picks", type=int, default=0, required=False)
|
||||
parser.add_argument("--time_before", type=float, default=10, required=False)
|
||||
parser.add_argument("--tolerance", type=float, default=1, required=False)
|
||||
parser.add_argument("--assoc_cut_dist", type=float, default=250, required=False)
|
||||
parser.add_argument("--pick_match_tolerance", type=float, default=0.5, required=False)
|
||||
parser.add_argument("--p_hint", type=str, default="P", required=False)
|
||||
parser.add_argument("--s_hint", type=str, default="S", required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
phaseassoc(**vars(args))
|
||||
return
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
100
pyoctoAssociator.py
Normal file
100
pyoctoAssociator.py
Normal file
@ -0,0 +1,100 @@
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import scipy.io
|
||||
|
||||
import pyocto
|
||||
|
||||
|
||||
class PhaseAssociation:
|
||||
"""
|
||||
Class for phase association using PyOcto.
|
||||
"""
|
||||
|
||||
def __init__(self, inventory, config):
|
||||
"""
|
||||
Initialize the PhaseAssociation class.
|
||||
|
||||
Args:
|
||||
inventory: ObsPy inventory object.
|
||||
config (dict): Configuration parameters.
|
||||
"""
|
||||
self._inv = inventory
|
||||
self._config = config
|
||||
self._setup_velocity_model()
|
||||
self._setup_associator()
|
||||
|
||||
def _setup_velocity_model(self):
|
||||
"""
|
||||
Set up the velocity model based on configuration.
|
||||
"""
|
||||
velocity_config = self._config["velocity"]
|
||||
if self._config["velocity"]["path"] is None:
|
||||
self.velocity_model = pyocto.VelocityModel0D(
|
||||
p_velocity=self._config["velocity"]["0D_P_vel"],
|
||||
s_velocity=self._config["velocity"]["0D_S_vel"],
|
||||
tolerance=self._config["params"]["tolerance"],
|
||||
association_cutoff_distance=self._config["params"]["association_cutoff_distance"])
|
||||
else:
|
||||
if not os.path.exists(velocity_config["path_out"]):
|
||||
self._create_1d_vel_model()
|
||||
self.velocity_model = pyocto.VelocityModel1D(velocity_config["path_out"],
|
||||
tolerance=self._config["params"]["tolerance"])
|
||||
|
||||
def _mat2df(self):
|
||||
# Load the .mat file
|
||||
mat_contents = scipy.io.loadmat(self._config["velocity"]["path"])
|
||||
data = mat_contents["d"][0][0]
|
||||
depth = data[0].T[0]
|
||||
depth = np.append(depth[::2], depth[-1])
|
||||
vel_p = data[1].T[0]
|
||||
vel_p = np.append(vel_p[0], vel_p[::2])
|
||||
vel_s = data[2].T[0]
|
||||
vel_s = np.append(vel_s[0], vel_s[::2])
|
||||
|
||||
# Create DataFrame from the lists
|
||||
data = {
|
||||
'depth': depth,
|
||||
'vp': vel_p,
|
||||
'vs': vel_s
|
||||
}
|
||||
layers = pd.DataFrame(data)
|
||||
|
||||
return layers
|
||||
|
||||
def _create_1d_vel_model(self):
|
||||
|
||||
layers = self._mat2df()
|
||||
pyocto.VelocityModel1D.create_model(
|
||||
layers,
|
||||
self._config["velocity"]["grid_spacing"],
|
||||
self._config["velocity"]["grid_hor_extent"],
|
||||
self._config["velocity"]["grid_ver_extent"],
|
||||
self._config["velocity"]["path_out"]
|
||||
)
|
||||
|
||||
def _setup_associator(self):
|
||||
"""
|
||||
Set up the associator based on configuration.
|
||||
"""
|
||||
grid_config = self._config["grid"]
|
||||
associator_params = self._config["params"]
|
||||
self.associator = pyocto.OctoAssociator.from_area(
|
||||
lat=(grid_config["lat_min"], grid_config["lat_max"]),
|
||||
lon=(grid_config["lon_min"], grid_config["lon_max"]),
|
||||
zlim=(grid_config["z_min"], grid_config["z_max"]),
|
||||
time_before=associator_params["time_before"],
|
||||
velocity_model=self.velocity_model,
|
||||
n_picks=associator_params["n_picks"],
|
||||
n_p_picks=associator_params["n_p_picks"],
|
||||
n_s_picks=associator_params["n_s_picks"],
|
||||
n_p_and_s_picks=associator_params["n_p_and_s_picks"],
|
||||
pick_match_tolerance=associator_params["pick_match_tolerance"]
|
||||
)
|
||||
|
||||
def associate_phases(self, picks):
|
||||
stations = self.associator.inventory_to_df(self._inv)
|
||||
events, assignments = self.associator.associate_seisbench(picks, stations)
|
||||
events = self.associator.transform_events(events)
|
||||
return events, assignments
|
18
qml2picks.py
Normal file
18
qml2picks.py
Normal file
@ -0,0 +1,18 @@
|
||||
from obspy import read_events
|
||||
from seisbench.util import Pick as SBPick
|
||||
|
||||
|
||||
def qml2picks(path_to_qml_picks, p_hint, s_hint):
|
||||
c = read_events(path_to_qml_picks, format="QUAKEML")
|
||||
picks = list()
|
||||
for p in c.events[0].picks:
|
||||
# PyOcto requires one character "P" and "S" hints.
|
||||
if p.phase_hint == p_hint:
|
||||
picks.append(
|
||||
SBPick(trace_id=f"{p.waveform_id.network_code}.{p.waveform_id.station_code}.", start_time=p.time,
|
||||
peak_time=p.time, end_time=p.time, phase="P"))
|
||||
elif p.phase_hint == s_hint:
|
||||
picks.append(
|
||||
SBPick(trace_id=f"{p.waveform_id.network_code}.{p.waveform_id.station_code}.", start_time=p.time,
|
||||
peak_time=p.time, end_time=p.time, phase="S"))
|
||||
return picks
|
Loading…
Reference in New Issue
Block a user