doublef 0.1.0__py3-none-any.whl

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.
doublef/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ __program__ = "DoubleF"
2
+ __version__ = "0.1.0"
3
+ __author__ = "Longfei Duan"
4
+ __email__ = "duanlongfei20@mails.ucas.ac.cn"
5
+ __description__ = "A fast and flexible phase association and earthquake location method"
@@ -0,0 +1,325 @@
1
+ import torch
2
+ from .batch_weight import BatchNumberScore, BatchTimeScore
3
+
4
+
5
+ def haversine_distance(lat1, lon1, lat2, lon2):
6
+ R = 6371
7
+ delta_lat = lat2 - lat1
8
+ delta_lon = lon2 - lon1
9
+
10
+ a = torch.sin(delta_lat / 2) ** 2 + torch.cos(lat1) * torch.cos(lat2) * torch.sin(delta_lon / 2) ** 2
11
+ c = 2 * torch.atan2(torch.sqrt(a), torch.sqrt(1 - a))
12
+ return R * c
13
+
14
+
15
+ class BatchScore(object):
16
+ """
17
+ input para
18
+ time_offset: maximum travel time deviation
19
+ max_distance: maximum epicenter distance
20
+ location_matrix: N x 4; N: number of events; 4: lat, lon, dep, otime
21
+ station_matrix: S x 4; S: number of stations; 4: id, lat, lon, ele
22
+ p_phase_matrix: M x S x 3; M: max number of pick from one station; S: number of stations; 3: picktime, prob, amp
23
+ s_phase_matrix: M x S x 3; M: max number of pick from one station; S: number of stations; 3: picktime, prob, amp
24
+ p_tt_matrix: H x V; H: number of Horizontal sampling points; V: number of Vertical sampling points
25
+ s_tt_matrix: H x V; H: number of Horizontal sampling points; V: number of Vertical sampling points
26
+ """
27
+
28
+ def __init__(self, max_distance, location_matrix, station_matrix,
29
+ p_tol_min, p_tol_max, s_tol_min, s_tol_max,
30
+ p_phase_matrix, s_phase_matrix, p_tt_matrix, s_tt_matrix,
31
+ P_weight, S_weight, time_weight, number_weight, magnitude_weight,
32
+ number_type, time_type, magnitude_type, dis0, dis1, device='cuda'):
33
+ self.max_distance = max_distance
34
+ self.location_matrix = torch.as_tensor(location_matrix, dtype=torch.float32, device=device)
35
+ self.station_matrix = torch.as_tensor(station_matrix, dtype=torch.float32, device=device)
36
+ self.p_phase_matrix = torch.as_tensor(p_phase_matrix, dtype=torch.float32, device=device)
37
+ self.s_phase_matrix = torch.as_tensor(s_phase_matrix, dtype=torch.float32, device=device)
38
+ self.p_tt_matrix = torch.as_tensor(p_tt_matrix, dtype=torch.float32, device=device)
39
+ self.s_tt_matrix = torch.as_tensor(s_tt_matrix, dtype=torch.float32, device=device)
40
+ self.P_weight = P_weight
41
+ self.S_weight = S_weight
42
+ self.time_weight = time_weight
43
+ self.number_weight = number_weight
44
+ self.magnitude_weight = magnitude_weight
45
+ self.number_type = number_type
46
+ self.time_type = time_type
47
+ self.magnitude_type = magnitude_type
48
+ self.dis0 = dis0
49
+ self.dis1 = dis1
50
+ self.p_tol_min = p_tol_min
51
+ self.p_tol_max = p_tol_max
52
+ self.s_tol_min = s_tol_min
53
+ self.s_tol_max = s_tol_max
54
+ self.device = device
55
+
56
+ def calculate_distances(self):
57
+ """
58
+ Calculate the distance between each station and each event for each batch.
59
+ :return:
60
+ distance_matrix: B x N x S x 3; B: batch size; N: number of events; S: number of stations; 3: epicenter distance, depth, otime
61
+ """
62
+ batch_size = self.location_matrix.shape[0]
63
+ num_events = self.location_matrix.shape[1]
64
+ num_stations = self.station_matrix.shape[0]
65
+
66
+ lat1 = torch.deg2rad(self.location_matrix[:, :, 0]) # B x N
67
+ lon1 = torch.deg2rad(self.location_matrix[:, :, 1]) # B x N
68
+ lat2 = torch.deg2rad(self.station_matrix[:, 1]) # S
69
+ lon2 = torch.deg2rad(self.station_matrix[:, 2]) # S
70
+
71
+ depth_time = self.location_matrix[:, :, 2:4] # B x N x 2
72
+
73
+ lat1_expanded = lat1.unsqueeze(2) # B x N x 1
74
+ lon1_expanded = lon1.unsqueeze(2) # B x N x 1
75
+
76
+ lat2_expanded = lat2.unsqueeze(0).unsqueeze(0) # 1 x 1 x S
77
+ lon2_expanded = lon2.unsqueeze(0).unsqueeze(0) # 1 x 1 x S
78
+
79
+ distances = haversine_distance(lat1_expanded, lon1_expanded, lat2_expanded, lon2_expanded) # B x N x S
80
+
81
+ depth_time_expanded = depth_time.unsqueeze(2).expand(-1, -1, num_stations, -1) # B x N x S x 2
82
+
83
+ self.distances_matrix = torch.cat([distances.unsqueeze(-1), depth_time_expanded], dim=-1) # B x N x S x 3
84
+
85
+ return self.distances_matrix
86
+
87
+ def get_theoretical_time(self):
88
+ """
89
+ Get the theoretical time for every earthquake and station for each batch.
90
+ :return:
91
+ p_tt_phases: B x N x S; B: batch size; N: number of events; S: number of stations
92
+ s_tt_phases: B x N x S; B: batch size; N: number of events; S: number of stations
93
+ ps_tt_phases: B x N x S; B: batch size; N: number of events; S: number of stations
94
+ """
95
+ B, N, S, _ = self.distances_matrix.shape
96
+
97
+ self.p_tt_distance = torch.full((B, N, S), float('nan'), dtype=torch.float32, device=self.device)
98
+ self.s_tt_distance = torch.full((B, N, S), float('nan'), dtype=torch.float32, device=self.device)
99
+
100
+ mask_above_threshold = self.distances_matrix[:, :, :, 0] > self.max_distance # Shape: [B, N, S]
101
+ valid_mask = ~mask_above_threshold # Shape: [B, N, S]
102
+
103
+ distances_filtered = torch.full_like(self.distances_matrix[:, :, :, 0], float('nan')) # Shape: [B, N, S]
104
+ depths_filtered = torch.full_like(self.distances_matrix[:, :, :, 1], float('nan')) # Shape: [B, N, S]
105
+ times_filtered = torch.full_like(self.distances_matrix[:, :, :, 2], float('nan')) # Shape: [B, N, S]
106
+
107
+ distances_filtered[valid_mask] = self.distances_matrix[:, :, :, 0][valid_mask]
108
+ depths_filtered[valid_mask] = self.distances_matrix[:, :, :, 1][valid_mask]
109
+ times_filtered[valid_mask] = self.distances_matrix[:, :, :, 2][valid_mask]
110
+
111
+ distances = torch.round(distances_filtered, decimals=2)
112
+ depths = torch.round(depths_filtered, decimals=2)
113
+
114
+ distance_idx = torch.clamp(torch.round(distances * 100).long(), 0,
115
+ self.p_tt_matrix.shape[0] - 1) # Shape: [B, N, S]
116
+ depth_idx = torch.clamp(torch.round(depths * 100).long(), 0, self.p_tt_matrix.shape[1] - 1) # Shape: [B, N, S]
117
+
118
+ p_time_values = self.p_tt_matrix[distance_idx, depth_idx] # Shape: [B, N, S]
119
+ s_time_values = self.s_tt_matrix[distance_idx, depth_idx] # Shape: [B, N, S]
120
+
121
+ self.p_tt_distance = times_filtered + p_time_values # Shape: [B, N, S]
122
+ self.s_tt_distance = times_filtered + s_time_values # Shape: [B, N, S]
123
+
124
+ self.ps_tt_distance = self.s_tt_distance - self.p_tt_distance # Shape: [B, N, S]
125
+
126
+ del self.p_tt_matrix, self.s_tt_matrix
127
+ return self.p_tt_distance, self.s_tt_distance, self.ps_tt_distance
128
+
129
+ def cal_score_P(self):
130
+ """
131
+ Get phases that under the time_offset for each eqs and stations
132
+ :return:
133
+ p_err: batch_size x N x S
134
+ p_prob: batch_size x N x S
135
+ p_amp: batch_size x N x S
136
+ p_pick: batch_size x N x S
137
+ """
138
+ batch_size, N, S = self.p_tt_distance.shape
139
+ M, S, _, _ = self.p_phase_matrix.shape
140
+
141
+ p_time_offset = (self.distances_matrix[:, :, :, 0] / self.max_distance) * (self.p_tol_max - self.p_tol_min) + self.p_tol_min
142
+
143
+ valid_mask = ~torch.isnan(self.p_tt_distance)
144
+ p_tt_distance_min = self.p_tt_distance - p_time_offset
145
+ p_tt_distance_max = self.p_tt_distance + p_time_offset
146
+
147
+ p_tt_distance_min = torch.floor(p_tt_distance_min / 5)
148
+ p_tt_distance_max = torch.floor(p_tt_distance_max / 5)
149
+ m_min = torch.clamp(p_tt_distance_min, 0, int(86400 / 5) - 1)
150
+ m_max = torch.clamp(p_tt_distance_max, 0, int(86400 / 5) - 1)
151
+
152
+ p_tt_distance_min[~valid_mask] = float('nan')
153
+ p_tt_distance_max[~valid_mask] = float('nan')
154
+ m_min[~valid_mask] = float('nan')
155
+ m_max[~valid_mask] = float('nan')
156
+
157
+ m_min = torch.nan_to_num_(m_min, nan=0).long().unsqueeze(-1).expand(-1, -1, -1, M)
158
+ m_max = torch.nan_to_num_(m_max, nan=0).long().unsqueeze(-1).expand(-1, -1, -1, M)
159
+
160
+ p_phase_matrix_expand = self.p_phase_matrix[:, :, :, 0].unsqueeze(0).unsqueeze(4).expand(batch_size, -1, -1, -1,
161
+ N).permute(0, 4, 2, 1,
162
+ 3)
163
+
164
+ p_phase_matrix_min = torch.gather(p_phase_matrix_expand, dim=4, index=m_min.unsqueeze(-1))
165
+ p_phase_matrix_max = torch.gather(p_phase_matrix_expand, dim=4, index=m_max.unsqueeze(-1))
166
+
167
+ p_tt_difference_min = self.p_tt_distance.unsqueeze(3) - p_phase_matrix_min[..., 0]
168
+ p_tt_difference_max = self.p_tt_distance.unsqueeze(3) - p_phase_matrix_max[..., 0]
169
+
170
+ p_tt_difference = torch.cat((p_tt_difference_min, p_tt_difference_max), dim=3)
171
+ m = torch.cat((m_min, m_max), dim=3)
172
+
173
+ torch.nan_to_num_(p_tt_difference, nan=float('100'), posinf=None, neginf=None)
174
+ min_abs_values, min_abs_indices = torch.min(torch.abs(p_tt_difference), dim=3)
175
+
176
+ mask = torch.abs(min_abs_values) > p_time_offset
177
+ min_abs_values.masked_fill_(mask, float('inf'))
178
+
179
+ self.p_err = torch.full((batch_size, N, S), float('nan'), dtype=torch.float32, device=self.p_tt_distance.device)
180
+ self.p_prob = torch.full((batch_size, N, S), float('nan'), dtype=torch.float32,
181
+ device=self.p_tt_distance.device)
182
+ self.p_amp = torch.full((batch_size, N, S), float('nan'), dtype=torch.float32, device=self.p_tt_distance.device)
183
+ self.p_pick = torch.full((batch_size, N, S), float('nan'), dtype=torch.float32,
184
+ device=self.p_tt_distance.device)
185
+
186
+ valid_mask = torch.isfinite(min_abs_values)
187
+ batch_idx, valid_rows, valid_cols = torch.where(valid_mask)
188
+
189
+ selected_indices = min_abs_indices[batch_idx, valid_rows, valid_cols].long()
190
+ m_adjusted = selected_indices.clone()
191
+ m_adjusted[m_adjusted >= M] -= M
192
+
193
+ self.p_err[batch_idx, valid_rows, valid_cols] = p_tt_difference[
194
+ batch_idx, valid_rows, valid_cols, selected_indices]
195
+
196
+ d = m[batch_idx, valid_rows, valid_cols, selected_indices]
197
+
198
+ self.p_prob[batch_idx, valid_rows, valid_cols] = self.p_phase_matrix[m_adjusted, valid_cols, d, 1]
199
+ self.p_amp[batch_idx, valid_rows, valid_cols] = self.p_phase_matrix[m_adjusted, valid_cols, d, 2]
200
+ self.p_pick[batch_idx, valid_rows, valid_cols] = self.p_phase_matrix[m_adjusted, valid_cols, d, 0]
201
+
202
+ del self.p_phase_matrix
203
+ return self.p_err, self.p_prob, self.p_amp, self.p_pick
204
+
205
+ def cal_score_S(self):
206
+ """
207
+ Get phases that under the time_offset for each eqs and stations
208
+ :return:
209
+ s_err: batch_size x N x S
210
+ s_prob: batch_size x N x S
211
+ s_amp: batch_size x N x S
212
+ s_pick: batch_size x N x S
213
+ """
214
+ batch_size, N, S = self.s_tt_distance.shape
215
+ M, S, _, _ = self.s_phase_matrix.shape
216
+
217
+ s_time_offset = (self.distances_matrix[:, :, :, 0] / self.max_distance) * (self.s_tol_max - self.s_tol_min) + self.s_tol_min
218
+
219
+ valid_mask = ~torch.isnan(self.s_tt_distance)
220
+ s_tt_distance_min = self.s_tt_distance - s_time_offset
221
+ s_tt_distance_max = self.s_tt_distance + s_time_offset
222
+
223
+ s_tt_distance_min = torch.floor(s_tt_distance_min / 5)
224
+ s_tt_distance_max = torch.floor(s_tt_distance_max / 5)
225
+ m_min = torch.clamp(s_tt_distance_min, 0, int(86400 / 5) - 1)
226
+ m_max = torch.clamp(s_tt_distance_max, 0, int(86400 / 5) - 1)
227
+
228
+ s_tt_distance_min[~valid_mask] = float('nan')
229
+ s_tt_distance_max[~valid_mask] = float('nan')
230
+ m_min[~valid_mask] = float('nan')
231
+ m_max[~valid_mask] = float('nan')
232
+
233
+ m_min = torch.nan_to_num_(m_min, nan=0).long().unsqueeze(-1).expand(-1, -1, -1, M)
234
+ m_max = torch.nan_to_num_(m_max, nan=0).long().unsqueeze(-1).expand(-1, -1, -1, M)
235
+
236
+ s_phase_matrix_expand = self.s_phase_matrix[:, :, :, 0].unsqueeze(0).unsqueeze(4).expand(batch_size, -1, -1, -1,
237
+ N).permute(0, 4, 2, 1,
238
+ 3)
239
+
240
+ s_phase_matrix_min = torch.gather(s_phase_matrix_expand, dim=4, index=m_min.unsqueeze(-1))
241
+ s_phase_matrix_max = torch.gather(s_phase_matrix_expand, dim=4, index=m_max.unsqueeze(-1))
242
+
243
+ s_tt_difference_min = self.s_tt_distance.unsqueeze(3) - s_phase_matrix_min[..., 0]
244
+ s_tt_difference_max = self.s_tt_distance.unsqueeze(3) - s_phase_matrix_max[..., 0]
245
+
246
+ s_tt_difference = torch.cat((s_tt_difference_min, s_tt_difference_max), dim=3)
247
+ m = torch.cat((m_min, m_max), dim=3)
248
+
249
+ torch.nan_to_num_(s_tt_difference, nan=float('100'), posinf=None, neginf=None)
250
+ min_abs_values, min_abs_indices = torch.min(torch.abs(s_tt_difference), dim=3)
251
+
252
+ mask = torch.abs(min_abs_values) > s_time_offset
253
+ min_abs_values.masked_fill_(mask, float('inf'))
254
+
255
+ self.s_err = torch.full((batch_size, N, S), float('nan'), dtype=torch.float32, device=self.s_tt_distance.device)
256
+ self.s_prob = torch.full((batch_size, N, S), float('nan'), dtype=torch.float32,
257
+ device=self.s_tt_distance.device)
258
+ self.s_amp = torch.full((batch_size, N, S), float('nan'), dtype=torch.float32, device=self.s_tt_distance.device)
259
+ self.s_pick = torch.full((batch_size, N, S), float('nan'), dtype=torch.float32,
260
+ device=self.s_tt_distance.device)
261
+
262
+ valid_mask = torch.isfinite(min_abs_values)
263
+ batch_idx, valid_rows, valid_cols = torch.where(valid_mask)
264
+
265
+ selected_indices = min_abs_indices[batch_idx, valid_rows, valid_cols].long()
266
+ m_adjusted = selected_indices.clone()
267
+ m_adjusted[m_adjusted >= M] -= M
268
+
269
+ self.s_err[batch_idx, valid_rows, valid_cols] = s_tt_difference[
270
+ batch_idx, valid_rows, valid_cols, selected_indices]
271
+
272
+ d = m[batch_idx, valid_rows, valid_cols, selected_indices]
273
+
274
+ self.s_prob[batch_idx, valid_rows, valid_cols] = self.s_phase_matrix[m_adjusted, valid_cols, d, 1]
275
+ self.s_amp[batch_idx, valid_rows, valid_cols] = self.s_phase_matrix[m_adjusted, valid_cols, d, 2]
276
+ self.s_pick[batch_idx, valid_rows, valid_cols] = self.s_phase_matrix[m_adjusted, valid_cols, d, 0]
277
+ del self.s_phase_matrix
278
+
279
+ return self.s_err, self.s_prob, self.s_amp, self.s_pick
280
+
281
+ def cal_weight_score(self):
282
+ """
283
+ Calculate the weighted score for each earthquake.
284
+ :return:
285
+ score_matrix: B x (N + 1); B: batch size; N: number of events
286
+ """
287
+ self.calculate_distances()
288
+ self.get_theoretical_time()
289
+ self.cal_score_P()
290
+ self.cal_score_S()
291
+ self.cal_score_ps()
292
+
293
+ B, N, S = self.p_prob.shape
294
+ ns = BatchNumberScore(self.p_prob, self.s_prob, self.P_weight, self.S_weight, S, self.number_type,
295
+ device=self.device)
296
+ number_score_matrix = ns.cal() # Shape: [B, N]
297
+ number_score_matrix[torch.isnan(number_score_matrix)] = 0
298
+
299
+ ts = BatchTimeScore(self.p_tol_max, self.s_tol_max, self.p_err, self.s_err, self.p_prob, self.s_prob, self.P_weight,
300
+ self.S_weight, self.distances_matrix[:, :, :, 0], self.dis0, self.dis1, self.time_type,
301
+ device=self.device)
302
+ time_score_matrix = 1 - ts.cal() # Shape: [B, N]
303
+ time_score_matrix[torch.isnan(time_score_matrix)] = 0
304
+
305
+
306
+
307
+ self.score_index = self.number_weight * number_score_matrix + self.time_weight * time_score_matrix.squeeze(-1) # Shape: [B, N]
308
+
309
+ # if self.magnitude_weight > 0:
310
+ # ms = MagnitudeScore(self.p_amp, self.s_amp, self.P_weight, self.S_weight, self.distances_matrix[:, :, 0],
311
+ # self.magnitude_type, device=self.device)
312
+ # magnitude_score_matrix = ms.nan_std() # Shape: [B, N]
313
+ # self.score_index = self.score_index + self.magnitude_weight * (1 - magnitude_score_matrix)
314
+
315
+ score_matrix = torch.cat((self.location_matrix, self.score_index.unsqueeze(-1)), dim=-1) # Shape: [B, N + 1]
316
+ return score_matrix
317
+
318
+ def cal_score_ps(self):
319
+ """
320
+ Calculate the P-S time difference error.
321
+ """
322
+ p_phase_matrix = self.p_tt_distance - self.p_err
323
+ s_phase_matrix = self.s_tt_distance - self.s_err
324
+ ps_phase_matrix = s_phase_matrix - p_phase_matrix
325
+ self.ps_err = self.ps_tt_distance - ps_phase_matrix