From b63fd319e79cad708cb05e87c59f258e1fec439b Mon Sep 17 00:00:00 2001 From: Julien Thurin Date: Wed, 20 May 2026 11:50:01 +0900 Subject: [PATCH 1/2] Re-added CPS_metadata handling with cleaner implementation. This commit re-integrates the CPS-based ProcessData handling of Green's functions. It mimic the FK implementation, and try to handle gracefully possible confusion with database and model paths. I have adopted conventions for CPS metadata based on HiBasin examples (P and S arrival in sac.a and sac.t0 fields). As stated in issue #336, not sure if this is universally accepted, but it is working with @Jinyin-Hu HiBasin examples. I decided to also have the CPS_model argument (as in FK), just in case people might have several models in the same Database folder. Could remove if not likely given default CPS outputs style. Also fix a misleading docstrings in `CPS.py` --- mtuq/greens_tensor/CPS.py | 2 +- mtuq/process_data.py | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/mtuq/greens_tensor/CPS.py b/mtuq/greens_tensor/CPS.py index f2f5cb1f..6c22b050 100644 --- a/mtuq/greens_tensor/CPS.py +++ b/mtuq/greens_tensor/CPS.py @@ -7,7 +7,7 @@ class GreensTensor(GreensTensorBase): """ - FK Green's tensor object + CPS Green's tensor object Overloads base class with machinery for working with CPS-style Green's functions diff --git a/mtuq/process_data.py b/mtuq/process_data.py index bd27e0b7..3cf8ee73 100644 --- a/mtuq/process_data.py +++ b/mtuq/process_data.py @@ -72,6 +72,9 @@ class ProcessData(object): - ``'FK_metadata'`` reads P, S travel times from FK metadata + - ``'CPS_metadata'`` + reads P, S travel times from CPS database SAC headers + - ``'SAC_metadata'`` reads P, S travel times from SAC metadata fields `t5`, `t6` @@ -148,9 +151,16 @@ class ProcessData(object): ``FK_database`` (`str`) Path to FK database, required for `pick_type=FK_metadata` + ``FK_model`` (`str`) + FK model name, optional for `pick_type=FK_metadata` + ``CPS_database`` (`str`) Path to CPS database, required for `pick_type=CPS_metadata` + ``CPS_model`` (`str`) + CPS model name (subdirectory of `CPS_database`), optional for `pick_type=CPS_metadata`. + If `CPS_database` already ends with the model subdirectory, omit this argument. + ``capuaf_file`` (`str`) Path to `CAPUAF`-style text file, required for `pick_type=user_supplied` @@ -355,6 +365,18 @@ def __init__(self, self.FK_database = FK_database self.FK_model = FK_model + elif self.pick_type == 'CPS_metadata': + assert CPS_database is not None + assert exists(CPS_database) + if CPS_model is not None and \ + basename(CPS_database.rstrip('/')) == CPS_model: + warn("CPS_model '%s' is already the last component of " + "CPS_database '%s'; setting CPS_model=None to avoid " + "path duplication" % (CPS_model, CPS_database)) + CPS_model = None + self.CPS_database = CPS_database + self.CPS_model = CPS_model # may be None if model is specified in CPS_database path + elif self.pick_type == 'SAC_metadata': pass @@ -579,6 +601,37 @@ def __call__(self, traces, station=None, origin=None, overwrite=False): elif self.pick_type == 'CPS_metadata': raise NotImplemented + # base directory: CPS_model may be None if model is specified in CPS_database path + if self.CPS_model is not None: + base_dir = join(self.CPS_database, self.CPS_model) + else: + base_dir = self.CPS_database + + # find depth folder closest to origin depth + # folder names encode depth as int(depth_km * 10), zero-padded to 4 digits + dep_desired = int(round(origin.depth_in_m / 100.)) + depth_folders = [e for e in listdir(base_dir) + if isdir(join(base_dir, e)) and e.isdigit()] + dep_folder = min(depth_folders, + key=lambda x: abs(int(x) - dep_desired)) + + # same as above for distance file, closest to station distance + # filename prefix encodes distance as int(dist_km * 10), zero-padded to 5 digits + dst_desired = int(round(distance_in_m / 100.)) + folder_path = join(base_dir, dep_folder) + dep_len = len(dep_folder) + dst_values = list({f.split('.')[0][:-dep_len] + for f in listdir(folder_path) + if f.endswith('.ZDD')}) + dst_value = min(dst_values, + key=lambda x: abs(int(x) - dst_desired)) + + sac_headers = obspy.read( + join(folder_path, dst_value + dep_folder + '.ZDD'), + format='sac')[0].stats.sac + + picks['P'] = float(sac_headers.a) # Not sure if universal CPS convention, but compatible with CPS database in HiBasin examples + picks['S'] = float(sac_headers.t0) # same as above elif self.pick_type == 'SAC_metadata': sac_headers = traces[0].sac From c65a9178ee910cc8d53abf0fd6c363d71d4b9dca Mon Sep 17 00:00:00 2001 From: Julien Thurin Date: Wed, 20 May 2026 12:11:44 +0900 Subject: [PATCH 2/2] Update process_data.py Forgot to remove the NotImplemented error. --- mtuq/process_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mtuq/process_data.py b/mtuq/process_data.py index 3cf8ee73..d1c49da6 100644 --- a/mtuq/process_data.py +++ b/mtuq/process_data.py @@ -600,7 +600,6 @@ def __call__(self, traces, station=None, origin=None, overwrite=False): picks['S'] = float(sac_headers.t2) elif self.pick_type == 'CPS_metadata': - raise NotImplemented # base directory: CPS_model may be None if model is specified in CPS_database path if self.CPS_model is not None: base_dir = join(self.CPS_database, self.CPS_model)