pyckster 25.9.4__tar.gz → 25.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyckster-25.9.4 → pyckster-25.11.1}/CHANGES.md +4 -0
- {pyckster-25.9.4/pyckster.egg-info → pyckster-25.11.1}/PKG-INFO +1 -1
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/__init__.py +1 -1
- pyckster-25.11.1/pyckster/auto_picking.py +407 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/core.py +5087 -527
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/inversion_app.py +381 -8
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/pyqtgraph_utils.py +12 -1
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/sw_utils.py +10 -5
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/visualization_utils.py +203 -38
- {pyckster-25.9.4 → pyckster-25.11.1/pyckster.egg-info}/PKG-INFO +1 -1
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster.egg-info/SOURCES.txt +1 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/LICENCE +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/MANIFEST.in +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/README.md +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/images/pyckster.png +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/images/pyckster.svg +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/images/screenshot_01.png +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/__main__.py +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/bayesian_inversion.py +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/inversion_manager.py +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/inversion_visualizer.py +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/obspy_utils.py +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/pac_inversion.py +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/surface_wave_analysis.py +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/surface_wave_profiling.py +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster/tab_factory.py +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster.egg-info/dependency_links.txt +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster.egg-info/entry_points.txt +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster.egg-info/not-zip-safe +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster.egg-info/requires.txt +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/pyckster.egg-info/top_level.txt +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/setup.cfg +0 -0
- {pyckster-25.9.4 → pyckster-25.11.1}/setup.py +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
v25.11.1, Nov. 2025 -- Add autopicking, add spectro and dispersion views, minor fixes and improvements
|
|
2
|
+
|
|
3
|
+
v25.10.1, Oct. 2025 -- Add bottom menu and coord display, fix picks loading (partly - still behaves weirdly with roll alongs), other small fixes
|
|
4
|
+
|
|
1
5
|
v25.9.4, Sep. 2025 -- Fix batch import and edit, fix view, add help, try icons
|
|
2
6
|
|
|
3
7
|
v25.9.3, Sep. 2025 -- Mouse and display controls improvements
|
|
@@ -15,7 +15,7 @@ except ImportError:
|
|
|
15
15
|
pass # matplotlib not available, that's fine
|
|
16
16
|
|
|
17
17
|
# Define version and metadata in one place
|
|
18
|
-
__version__ = "25.
|
|
18
|
+
__version__ = "25.11.1"
|
|
19
19
|
__author__ = "Sylvain Pasquet"
|
|
20
20
|
__email__ = "sylvain.pasquet@sorbonne-universite.fr"
|
|
21
21
|
__license__ = "GPLv3"
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import signal, stats
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
from obspy import Stream, Trace
|
|
5
|
+
|
|
6
|
+
from .obspy_utils import read_seismic_file
|
|
7
|
+
|
|
8
|
+
def mnw_picker(trace, Td, beta=0.005):
|
|
9
|
+
"""
|
|
10
|
+
Implements the Multi-Nested Windows (MNW) method to detect first arrivals.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
trace (obspy.Trace): Seismic trace.
|
|
14
|
+
Td (float): Dominant period of the P-wave (in seconds).
|
|
15
|
+
beta (float): Constant to avoid numerical instability.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
tuple: (BPZ, tP1, tE1)
|
|
19
|
+
- BPZ (float): Beginning of the potential zone (in seconds).
|
|
20
|
+
- tP1 (float): Potential arrival time (in seconds).
|
|
21
|
+
- tE1 (float): Associated error (in seconds).
|
|
22
|
+
"""
|
|
23
|
+
data = trace.data
|
|
24
|
+
fs = trace.stats.sampling_rate
|
|
25
|
+
N = len(data)
|
|
26
|
+
|
|
27
|
+
# Window lengths in samples
|
|
28
|
+
Lb = int(4 * Td * fs)
|
|
29
|
+
La = int(Td * fs)
|
|
30
|
+
d = int(0.6 * Td * fs)
|
|
31
|
+
Ld = int((1 - 0.6) * Td * fs)
|
|
32
|
+
|
|
33
|
+
# Initialize arrays for BEA, AEA, DEA
|
|
34
|
+
BEA = np.zeros(N)
|
|
35
|
+
AEA = np.zeros(N)
|
|
36
|
+
DEA = np.zeros(N)
|
|
37
|
+
|
|
38
|
+
# Calculate energies
|
|
39
|
+
for t in range(N):
|
|
40
|
+
# BEA calculation with bounds checking
|
|
41
|
+
if (t - Lb) >= 0:
|
|
42
|
+
window = data[max(0, t - Lb):t]
|
|
43
|
+
else:
|
|
44
|
+
window = data[:t]
|
|
45
|
+
|
|
46
|
+
if len(window) > 0:
|
|
47
|
+
BEA[t] = np.mean(window ** 2)
|
|
48
|
+
else:
|
|
49
|
+
BEA[t] = beta
|
|
50
|
+
|
|
51
|
+
# AEA calculation
|
|
52
|
+
aea_window = data[t:min(t + La, N)]
|
|
53
|
+
if len(aea_window) > 0:
|
|
54
|
+
AEA[t] = np.mean(aea_window ** 2)
|
|
55
|
+
else:
|
|
56
|
+
AEA[t] = beta
|
|
57
|
+
|
|
58
|
+
# DEA calculation
|
|
59
|
+
if (t + d) < N:
|
|
60
|
+
dea_window = data[min(t + d, N):min(t + d + Ld, N)]
|
|
61
|
+
if len(dea_window) > 0:
|
|
62
|
+
DEA[t] = np.mean(dea_window ** 2)
|
|
63
|
+
else:
|
|
64
|
+
DEA[t] = 0
|
|
65
|
+
else:
|
|
66
|
+
DEA[t] = 0
|
|
67
|
+
|
|
68
|
+
# Avoid division by zero
|
|
69
|
+
BEA[BEA == 0] = beta
|
|
70
|
+
|
|
71
|
+
# Calculate energy ratios
|
|
72
|
+
ER1 = AEA / (BEA + beta)
|
|
73
|
+
ER2 = DEA / (BEA + beta)
|
|
74
|
+
CF_mnw = ER1 + ER2
|
|
75
|
+
|
|
76
|
+
# Smooth CF_mnw
|
|
77
|
+
window_length = int(0.5 * Td * fs)
|
|
78
|
+
if window_length % 2 == 0:
|
|
79
|
+
window_length += 1 # Ensure odd length for savgol_filter
|
|
80
|
+
|
|
81
|
+
# Ensure window length is valid
|
|
82
|
+
window_length = max(3, min(window_length, len(CF_mnw)))
|
|
83
|
+
if window_length % 2 == 0:
|
|
84
|
+
window_length -= 1
|
|
85
|
+
|
|
86
|
+
if window_length >= 3 and len(CF_mnw) >= window_length:
|
|
87
|
+
SCF_mnw = signal.savgol_filter(CF_mnw, window_length, 2)
|
|
88
|
+
else:
|
|
89
|
+
# Fallback to simple smoothing if savgol_filter can't be applied
|
|
90
|
+
SCF_mnw = CF_mnw.copy()
|
|
91
|
+
|
|
92
|
+
# Dynamic threshold
|
|
93
|
+
sigma = np.std(CF_mnw[:int(0.5 * N)])
|
|
94
|
+
Thr = 2 + 3 * sigma
|
|
95
|
+
|
|
96
|
+
# Detect BPZ (beginning of the potential zone)
|
|
97
|
+
BPZ_indices = np.where(SCF_mnw > Thr)[0]
|
|
98
|
+
BPZ = BPZ_indices[0] / fs if len(BPZ_indices) > 0 else 0
|
|
99
|
+
|
|
100
|
+
# Search for local maxima after BPZ
|
|
101
|
+
search_window = int(1.5 * Td * fs)
|
|
102
|
+
start_search = int(BPZ * fs)
|
|
103
|
+
end_search = min(start_search + search_window, N)
|
|
104
|
+
local_max_indices = signal.argrelextrema(SCF_mnw[start_search:end_search], np.greater)[0]
|
|
105
|
+
local_max_values = SCF_mnw[start_search:end_search][local_max_indices]
|
|
106
|
+
|
|
107
|
+
if len(local_max_values) > 0:
|
|
108
|
+
tP1_index = start_search + local_max_indices[np.argmax(local_max_values)]
|
|
109
|
+
tP1 = tP1_index / fs
|
|
110
|
+
else:
|
|
111
|
+
tP1 = BPZ
|
|
112
|
+
|
|
113
|
+
# Estimate error
|
|
114
|
+
if len(local_max_indices) > 1:
|
|
115
|
+
tE1 = max(
|
|
116
|
+
abs(BPZ - (start_search + local_max_indices[0]) / fs),
|
|
117
|
+
abs((start_search + local_max_indices[0] - start_search + local_max_indices[1]) / fs),
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
tE1 = abs(BPZ - tP1)
|
|
121
|
+
|
|
122
|
+
return BPZ, tP1, tE1
|
|
123
|
+
|
|
124
|
+
def hos_picker(trace, tP1, tE1, Td):
|
|
125
|
+
"""
|
|
126
|
+
Implements the Higher Order Statistics (HOS) method using kurtosis to refine picking.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
trace (obspy.Trace): Seismic trace.
|
|
130
|
+
tP1 (float): Potential arrival time from MNW (in seconds).
|
|
131
|
+
tE1 (float): Error associated with tP1 (in seconds).
|
|
132
|
+
Td (float): Dominant period of the P-wave (in seconds).
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
tuple: (tP2, tE2)
|
|
136
|
+
- tP2 (float): Potential arrival time (in seconds).
|
|
137
|
+
- tE2 (float): Associated error (in seconds).
|
|
138
|
+
"""
|
|
139
|
+
data = trace.data
|
|
140
|
+
fs = trace.stats.sampling_rate
|
|
141
|
+
|
|
142
|
+
# Window size for kurtosis
|
|
143
|
+
Nk = max(int(2 * tE1 * fs), 1)
|
|
144
|
+
Nk = min(Nk, int(2 * Td * fs))
|
|
145
|
+
|
|
146
|
+
# Calculate kurtosis
|
|
147
|
+
CF_k = np.zeros(len(data))
|
|
148
|
+
for t in range(Nk, len(data)):
|
|
149
|
+
window = data[t - Nk : t]
|
|
150
|
+
if len(window) >= 3: # Need at least 3 points for meaningful kurtosis
|
|
151
|
+
try:
|
|
152
|
+
CF_k[t] = stats.kurtosis(window, fisher=True)
|
|
153
|
+
# Handle potential NaN or infinite values
|
|
154
|
+
if not np.isfinite(CF_k[t]):
|
|
155
|
+
CF_k[t] = 0.0
|
|
156
|
+
except Exception:
|
|
157
|
+
CF_k[t] = 0.0
|
|
158
|
+
else:
|
|
159
|
+
CF_k[t] = 0.0
|
|
160
|
+
|
|
161
|
+
# Smooth CF_k
|
|
162
|
+
window_length = int(0.5 * Td * fs)
|
|
163
|
+
if window_length % 2 == 0:
|
|
164
|
+
window_length += 1
|
|
165
|
+
|
|
166
|
+
# Ensure window length is valid
|
|
167
|
+
window_length = max(3, min(window_length, len(CF_k)))
|
|
168
|
+
if window_length % 2 == 0:
|
|
169
|
+
window_length -= 1
|
|
170
|
+
|
|
171
|
+
if window_length >= 3 and len(CF_k) >= window_length:
|
|
172
|
+
SCF_k = signal.savgol_filter(CF_k, window_length, 2)
|
|
173
|
+
else:
|
|
174
|
+
SCF_k = CF_k.copy()
|
|
175
|
+
|
|
176
|
+
# Baillard transformation
|
|
177
|
+
SCF_Bk = -SCF_k
|
|
178
|
+
|
|
179
|
+
# Search for local minimum around tP1
|
|
180
|
+
search_window = int(tE1 * fs + Td * fs)
|
|
181
|
+
start_idx = max(0, int((tP1 - tE1) * fs))
|
|
182
|
+
end_idx = min(len(data), int((tP1 + Td) * fs))
|
|
183
|
+
|
|
184
|
+
# Ensure valid search window
|
|
185
|
+
if start_idx >= end_idx or end_idx > len(SCF_Bk):
|
|
186
|
+
tP2 = tP1 # Fallback to original pick
|
|
187
|
+
tE2 = tE1
|
|
188
|
+
return tP2, tE2
|
|
189
|
+
|
|
190
|
+
search_window_data = SCF_Bk[start_idx:end_idx]
|
|
191
|
+
if len(search_window_data) == 0:
|
|
192
|
+
tP2 = tP1
|
|
193
|
+
tE2 = tE1
|
|
194
|
+
return tP2, tE2
|
|
195
|
+
|
|
196
|
+
local_min_index = start_idx + np.argmin(search_window_data)
|
|
197
|
+
tP2 = local_min_index / fs
|
|
198
|
+
|
|
199
|
+
# Calculate error more robustly
|
|
200
|
+
try:
|
|
201
|
+
cf_search_window = CF_k[start_idx:end_idx]
|
|
202
|
+
if len(cf_search_window) > 0:
|
|
203
|
+
max_idx = np.argmax(cf_search_window)
|
|
204
|
+
tE2 = abs(max_idx / fs - (local_min_index - start_idx) / fs)
|
|
205
|
+
else:
|
|
206
|
+
tE2 = tE1
|
|
207
|
+
except Exception:
|
|
208
|
+
tE2 = tE1
|
|
209
|
+
|
|
210
|
+
return tP2, tE2
|
|
211
|
+
|
|
212
|
+
def aic_picker(trace, tP1, tP2, tE1, tE2):
|
|
213
|
+
"""
|
|
214
|
+
Implements the Akaike Information Criterion (AIC) method to refine picking.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
trace (obspy.Trace): Seismic trace.
|
|
218
|
+
tP1 (float): Potential arrival time from MNW (in seconds).
|
|
219
|
+
tP2 (float): Potential arrival time from HOS (in seconds).
|
|
220
|
+
tE1 (float): Error associated with tP1 (in seconds).
|
|
221
|
+
tE2 (float): Error associated with tP2 (in seconds).
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
tuple: (tP3, tE3)
|
|
225
|
+
- tP3 (float): Potential arrival time (in seconds).
|
|
226
|
+
- tE3 (float): Associated error (in seconds).
|
|
227
|
+
"""
|
|
228
|
+
data = trace.data
|
|
229
|
+
fs = trace.stats.sampling_rate
|
|
230
|
+
|
|
231
|
+
# Search window for AIC
|
|
232
|
+
SW3 = 2 * max(tE1, tE2)
|
|
233
|
+
center = 0.5 * (tP1 + tP2)
|
|
234
|
+
start_idx = max(0, int((center - SW3 / 2) * fs))
|
|
235
|
+
end_idx = min(len(data), int((center + SW3 / 2) * fs))
|
|
236
|
+
|
|
237
|
+
# Calculate AIC function
|
|
238
|
+
CF_aic = np.zeros(len(data))
|
|
239
|
+
for t in range(1, len(data) - 1):
|
|
240
|
+
if t > 0 and t < len(data): # Ensure valid indices
|
|
241
|
+
var1 = np.var(data[:t])
|
|
242
|
+
var2 = np.var(data[t:])
|
|
243
|
+
|
|
244
|
+
# Add small epsilon to avoid log(0) and handle constant signals
|
|
245
|
+
epsilon = 1e-10
|
|
246
|
+
var1 = max(var1, epsilon)
|
|
247
|
+
var2 = max(var2, epsilon)
|
|
248
|
+
|
|
249
|
+
CF_aic[t] = t * np.log(var1) + (len(data) - t) * np.log(var2)
|
|
250
|
+
|
|
251
|
+
# Normalize Akaike weights
|
|
252
|
+
if start_idx >= end_idx or end_idx > len(CF_aic):
|
|
253
|
+
# Invalid range, return center of search window
|
|
254
|
+
tP3 = center
|
|
255
|
+
tE3 = SW3 / 4 # Default error
|
|
256
|
+
return tP3, tE3
|
|
257
|
+
|
|
258
|
+
aic_window = CF_aic[start_idx:end_idx]
|
|
259
|
+
if len(aic_window) == 0:
|
|
260
|
+
tP3 = center
|
|
261
|
+
tE3 = SW3 / 4
|
|
262
|
+
return tP3, tE3
|
|
263
|
+
|
|
264
|
+
min_CF_aic = np.min(aic_window)
|
|
265
|
+
Delta = aic_window - min_CF_aic
|
|
266
|
+
|
|
267
|
+
# Handle potential numerical issues
|
|
268
|
+
exp_values = np.exp(-Delta / 2)
|
|
269
|
+
sum_exp = np.sum(exp_values)
|
|
270
|
+
|
|
271
|
+
if sum_exp == 0 or not np.isfinite(sum_exp):
|
|
272
|
+
# Fallback to uniform weights
|
|
273
|
+
AW = np.ones(len(aic_window)) / len(aic_window)
|
|
274
|
+
else:
|
|
275
|
+
AW = exp_values / sum_exp
|
|
276
|
+
|
|
277
|
+
# Calculate weighted average arrival time
|
|
278
|
+
indices = np.arange(start_idx, end_idx)
|
|
279
|
+
if len(indices) == 0 or len(AW) == 0:
|
|
280
|
+
tP3 = center
|
|
281
|
+
tE3 = SW3 / 4
|
|
282
|
+
return tP3, tE3
|
|
283
|
+
|
|
284
|
+
sum_weights = np.sum(AW)
|
|
285
|
+
if sum_weights == 0:
|
|
286
|
+
tP3 = center
|
|
287
|
+
tE3 = SW3 / 4
|
|
288
|
+
return tP3, tE3
|
|
289
|
+
|
|
290
|
+
tP3 = np.sum(AW * indices / fs) / sum_weights
|
|
291
|
+
|
|
292
|
+
# Estimate error
|
|
293
|
+
threshold = 0.1 * np.max(AW)
|
|
294
|
+
error_indices = np.where(AW > threshold)[0]
|
|
295
|
+
tE3 = (error_indices[-1] - error_indices[0]) / fs if len(error_indices) > 0 else 0
|
|
296
|
+
|
|
297
|
+
return tP3, tE3
|
|
298
|
+
|
|
299
|
+
def calculate_snr(trace, pick_time, Td):
|
|
300
|
+
"""
|
|
301
|
+
Calculates the signal-to-noise ratio (SNR) around an arrival time.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
trace (obspy.Trace): Seismic trace.
|
|
305
|
+
pick_time (float): Arrival time (in seconds).
|
|
306
|
+
Td (float): Dominant period of the P-wave (in seconds).
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
float: Signal-to-noise ratio in dB.
|
|
310
|
+
"""
|
|
311
|
+
fs = trace.stats.sampling_rate
|
|
312
|
+
noise_window_start = max(0, int((pick_time - 3 * Td) * fs))
|
|
313
|
+
noise_window_end = int(pick_time * fs)
|
|
314
|
+
signal_window_start = int(pick_time * fs)
|
|
315
|
+
signal_window_end = min(len(trace.data), int((pick_time + Td) * fs))
|
|
316
|
+
|
|
317
|
+
# Ensure valid windows
|
|
318
|
+
if noise_window_end <= noise_window_start:
|
|
319
|
+
noise_window_end = noise_window_start + max(1, int(0.1 * Td * fs))
|
|
320
|
+
|
|
321
|
+
if signal_window_end <= signal_window_start:
|
|
322
|
+
signal_window_end = signal_window_start + max(1, int(0.1 * Td * fs))
|
|
323
|
+
|
|
324
|
+
# Extract windows and calculate RMS
|
|
325
|
+
noise_data = trace.data[noise_window_start:min(noise_window_end, len(trace.data))]
|
|
326
|
+
signal_data = trace.data[signal_window_start:min(signal_window_end, len(trace.data))]
|
|
327
|
+
|
|
328
|
+
if len(noise_data) == 0 or len(signal_data) == 0:
|
|
329
|
+
return 0 # Cannot calculate SNR
|
|
330
|
+
|
|
331
|
+
noise_rms = np.sqrt(np.mean(noise_data ** 2))
|
|
332
|
+
signal_rms = np.sqrt(np.mean(signal_data ** 2))
|
|
333
|
+
|
|
334
|
+
# Add small epsilon to avoid division by zero
|
|
335
|
+
epsilon = 1e-12
|
|
336
|
+
noise_rms = max(noise_rms, epsilon)
|
|
337
|
+
|
|
338
|
+
if signal_rms > 0:
|
|
339
|
+
return 20 * np.log10(signal_rms / noise_rms)
|
|
340
|
+
else:
|
|
341
|
+
return 0
|
|
342
|
+
|
|
343
|
+
def adaptive_picker(trace, Td):
|
|
344
|
+
"""
|
|
345
|
+
Integrates the three methods (MNW, HOS, AIC) for adaptive picking.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
trace (obspy.Trace): Seismic trace.
|
|
349
|
+
Td (float): Dominant period of the P-wave (in seconds).
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
tuple: (tP_final, tE_final)
|
|
353
|
+
- tP_final (float): Final arrival time (in seconds).
|
|
354
|
+
- tE_final (float): Associated error (in seconds).
|
|
355
|
+
"""
|
|
356
|
+
# Validate input
|
|
357
|
+
if trace is None or len(trace.data) == 0 or Td <= 0:
|
|
358
|
+
return None, None
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
BPZ, tP1, tE1 = mnw_picker(trace, Td)
|
|
362
|
+
tP2, tE2 = hos_picker(trace, tP1, tE1, Td)
|
|
363
|
+
tP3, tE3 = aic_picker(trace, tP1, tP2, tE1, tE2)
|
|
364
|
+
except Exception:
|
|
365
|
+
# If any picker fails, return None
|
|
366
|
+
return None, None
|
|
367
|
+
|
|
368
|
+
# Check for NaN or invalid values
|
|
369
|
+
picks = [tP1, tP2, tP3]
|
|
370
|
+
valid_picks = []
|
|
371
|
+
valid_Qs = []
|
|
372
|
+
|
|
373
|
+
for i, pick in enumerate(picks):
|
|
374
|
+
if (pick is not None and not np.isnan(pick) and
|
|
375
|
+
np.isfinite(pick) and pick >= 0 and
|
|
376
|
+
pick < len(trace.data) / trace.stats.sampling_rate):
|
|
377
|
+
try:
|
|
378
|
+
Q = calculate_snr(trace, pick, Td)
|
|
379
|
+
if Q > -50 and np.isfinite(Q): # Reasonable SNR range
|
|
380
|
+
valid_picks.append(pick)
|
|
381
|
+
valid_Qs.append(max(0.1, Q)) # Ensure positive weight
|
|
382
|
+
except Exception:
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
if len(valid_picks) == 0:
|
|
386
|
+
return None, None
|
|
387
|
+
|
|
388
|
+
# Calculate weighted average with robust error handling
|
|
389
|
+
try:
|
|
390
|
+
if len(valid_Qs) > 0 and all(w > 0 for w in valid_Qs):
|
|
391
|
+
tP_final = np.average(valid_picks, weights=valid_Qs)
|
|
392
|
+
else:
|
|
393
|
+
tP_final = np.mean(valid_picks)
|
|
394
|
+
|
|
395
|
+
if len(valid_picks) > 1:
|
|
396
|
+
tE_final = np.std(valid_picks)
|
|
397
|
+
else:
|
|
398
|
+
tE_final = Td / 4 # Default error based on dominant period
|
|
399
|
+
|
|
400
|
+
# Validate final result
|
|
401
|
+
if not np.isfinite(tP_final) or tP_final < 0:
|
|
402
|
+
return None, None
|
|
403
|
+
|
|
404
|
+
except Exception:
|
|
405
|
+
return None, None
|
|
406
|
+
|
|
407
|
+
return tP_final, tE_final
|