pysmo.aimbat 1.0.8__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.
@@ -0,0 +1,46 @@
1
+ """
2
+ AIMBAT
3
+ ======
4
+
5
+ AIMBAT (Automated and Interactive Measurement of Body wave Arrival Times)
6
+ is an open-source software package for efficiently measuring teleseismic
7
+ body wave arrival times for large seismic arrays (Lou et al., 2012). It is
8
+ based on a widely used method called MCCC (Multi-Channel Cross-Correlation)
9
+ developed by VanDecar and Crosson (1990). The package is automated in the
10
+ sense of initially aligning seismograms for MCCC which is achieved by an
11
+ ICCS (Iterative Cross Correlation and Stack) algorithm. Meanwhile, a
12
+ graphical user interface is built to perform seismogram quality control
13
+ interactively. Therefore, user processing time is reduced while valuable
14
+ input from a user\'s expertise is retained. As a byproduct, SAC (Goldstein
15
+ et al., 2003) plotting and phase picking functionalities are replicated
16
+ and enhanced.
17
+
18
+ """
19
+
20
+ import sys
21
+ import warnings
22
+
23
+ name = "aimbat"
24
+
25
+ _MIGRATION_NOTICE = (
26
+ "pysmo.aimbat is the legacy AIMBAT 1 line and is no longer actively "
27
+ "developed. It has been superseded by AIMBAT 2, which is distributed as "
28
+ "'aimbat' (pip install aimbat) and imported as 'aimbat', no longer under "
29
+ "the pysmo namespace. See https://github.com/pysmo/aimbat."
30
+ )
31
+
32
+ warnings.warn(_MIGRATION_NOTICE, DeprecationWarning, stacklevel=2)
33
+
34
+ _cli_notice_shown = False
35
+
36
+
37
+ def cli_deprecation_notice():
38
+ """Print the AIMBAT 1 migration notice to stderr, once per process.
39
+
40
+ Called by the console-script entry points, where a DeprecationWarning
41
+ would otherwise be hidden by Python's default warning filters.
42
+ """
43
+ global _cli_notice_shown
44
+ if not _cli_notice_shown:
45
+ print("WARNING: " + _MIGRATION_NOTICE, file=sys.stderr)
46
+ _cli_notice_shown = True
@@ -0,0 +1,502 @@
1
+ #!/usr/bin/env python
2
+ # ------------------------------------------------
3
+ # Filename: algiccs.py
4
+ # Author: Xiaoting Lou
5
+ # Email: xlou@u.northwestern.edu
6
+ #
7
+ # Copyright (c) 2009 Xiaoting Lou
8
+ # ------------------------------------------------
9
+ """
10
+ Python module for the ICCS (iterative cross-correlation and stack) algorithm.
11
+
12
+ :copyright:
13
+ Xiaoting Lou
14
+
15
+ :license:
16
+ GNU General Public License, Version 3 (GPLv3)
17
+ http://www.gnu.org/licenses/gpl.html
18
+ """
19
+
20
+ import copy
21
+ import os
22
+ import sys
23
+ from optparse import OptionParser
24
+
25
+ from numpy import array, corrcoef, dot, linspace, mean, ones, sqrt, transpose, zeros
26
+ from numpy import linalg as LA
27
+
28
+ from pysmo.aimbat import cli_deprecation_notice, qualsort, ttconfig
29
+ from pysmo.aimbat import prepdata as pdata
30
+ from pysmo.aimbat import sacpickle as sacpkl
31
+
32
+
33
+ def getOptions():
34
+ """Parse arguments and options."""
35
+ usage = "Usage: %prog [options] <sacfile(s) or a picklefile>"
36
+ parser = OptionParser(usage=usage)
37
+ twcorr = -15, 15
38
+ ipick = "t0"
39
+ wpick = "t1"
40
+ minccc = 0.5
41
+ minsnr = 0.5
42
+ mincoh = 0.0
43
+ minqual = minccc, minsnr, mincoh
44
+ minnsel = 5
45
+ parser.set_defaults(twcorr=twcorr)
46
+ parser.set_defaults(ipick=ipick)
47
+ parser.set_defaults(wpick=wpick)
48
+ parser.set_defaults(minqual=minqual)
49
+ parser.set_defaults(minnsel=minnsel)
50
+ parser.add_option(
51
+ "-S",
52
+ "--srate",
53
+ dest="srate",
54
+ type="float",
55
+ help="Sampling rate to load SAC data. Default is None, "
56
+ "use the original rate of first files.",
57
+ )
58
+ parser.add_option(
59
+ "-i",
60
+ "--ipick",
61
+ dest="ipick",
62
+ type="str",
63
+ help="SAC header variable to read input time pick.",
64
+ )
65
+ parser.add_option(
66
+ "-w",
67
+ "--wpick",
68
+ dest="wpick",
69
+ type="str",
70
+ help="SAC header variable to write output time pick.",
71
+ )
72
+ parser.add_option(
73
+ "-t",
74
+ "--twcorr",
75
+ dest="twcorr",
76
+ type="float",
77
+ nargs=2,
78
+ help="Time window for cross-correlation. Default is "
79
+ f"[{twcorr[0]:.1f}, {twcorr[1]:.1f}] s.",
80
+ )
81
+ parser.add_option(
82
+ "-f",
83
+ "--fstack",
84
+ dest="fstack",
85
+ type="str",
86
+ help="SAC file name to save final array stack.",
87
+ )
88
+ parser.add_option(
89
+ "-p",
90
+ "--plotiter",
91
+ action="store_true",
92
+ dest="plotiter",
93
+ help="Plot array stack of each iteration.",
94
+ )
95
+ parser.add_option(
96
+ "-a",
97
+ "--auto_on",
98
+ action="store_true",
99
+ dest="auto_on",
100
+ help="Run ICCS and select/delete seismograms automatically.",
101
+ )
102
+ parser.add_option(
103
+ "-A",
104
+ "--auto_on_all",
105
+ action="store_true",
106
+ dest="auto_on_all",
107
+ help="Run ICCS with -a option but initially use all seismograms.",
108
+ )
109
+ parser.add_option(
110
+ "-q",
111
+ "--minqual",
112
+ dest="minqual",
113
+ type="float",
114
+ nargs=3,
115
+ help="Minimum quality factor (ccc,snr,coh) for auto selection. "
116
+ f"Defaults are {minccc:.2f} {minsnr:.2f} {mincoh:.2f}.",
117
+ )
118
+ parser.add_option(
119
+ "-n",
120
+ "--minnsel",
121
+ dest="minnsel",
122
+ type="int",
123
+ help="Minimum number of selected seismograms for auto selection. "
124
+ f"Default is {minnsel:d}.",
125
+ )
126
+ opts, files = parser.parse_args(sys.argv[1:])
127
+ if not files:
128
+ print(parser.usage)
129
+ sys.exit()
130
+ return opts, files
131
+
132
+
133
+ def corrmax(datai, dataj, delta, xcorr, shift):
134
+ """Calculate time lag at maximum cross correlation between two time series."""
135
+ delay, ccmax, ccpol = xcorr(datai, dataj, shift)
136
+ return delay * delta, ccmax, ccpol
137
+
138
+
139
+ def meanStack(data, taperwidth, tapertype):
140
+ """Calculate array stack by averaging without weighting."""
141
+ sdata = mean(data, 0)
142
+ sdata = sacpkl.taper(sdata, taperwidth, tapertype)
143
+ return sdata
144
+
145
+
146
+ def weightStack(data, wgts, taperwidth, tapertype):
147
+ """Calculate array stack by averaging with weighting."""
148
+ sdata = mean(transpose(data) * wgts, 1)
149
+ sdata = sacpkl.taper(sdata, taperwidth, tapertype)
150
+ return sdata
151
+
152
+
153
+ def normWeightStack(data, wgts, taperwidth, tapertype):
154
+ """Calculate array stack by averaging with weighting and normalization."""
155
+ mdata = [d / max(d) for d in data]
156
+ sdata = mean(transpose(mdata) * wgts, 1)
157
+ sdata = sacpkl.taper(sdata, taperwidth, tapertype)
158
+ return sdata
159
+
160
+
161
+ def ccWeightStack(saclist, opts):
162
+ """
163
+ Align seismograms by the iterative cross-correlation and stack algorithm.
164
+
165
+ Parameters
166
+ ----------
167
+ opts.delta : sample time interval
168
+ opts.ccpara : a class instance for ICCS parameters
169
+ * qqhdrs : SAC headers to save quality factors: ccc, snr, coh
170
+ * maxiter : maximum number of iteration
171
+ * converg : convergence critrion
172
+ * cchdrs : inputand output picks of cross-correlation
173
+ * twcorr : time window for cross-correlation
174
+ """
175
+ ccpara = opts.ccpara
176
+ delta = opts.delta
177
+ maxiter = ccpara.maxiter
178
+ convtype = ccpara.convtype
179
+ convepsi = ccpara.convepsi
180
+ taperwidth = ccpara.taperwidth
181
+ tapertype = ccpara.tapertype
182
+ xcorr = ccpara.xcorr
183
+ shift = ccpara.shift
184
+ twhdrs = ccpara.twhdrs
185
+ qqhdrs = ccpara.qheaders
186
+ cchdrs = ccpara.cchdrs
187
+ twcorr = ccpara.twcorr
188
+ (
189
+ hdrccc,
190
+ hdrsnr,
191
+ hdrcoh,
192
+ ) = qqhdrs[:3]
193
+ cchdr0, cchdr1 = cchdrs
194
+ if convtype == "coef":
195
+ convergence = coConverg
196
+ elif convtype == "resi":
197
+ convergence = reConverg
198
+ else:
199
+ print(f"Unknown convergence criterion: {convtype:s}. Exit.")
200
+ sys.exit()
201
+
202
+ out = "\n--> Run ICCS at window [{0:5.1f}, {1:5.1f}] wrt {2:s}. Write to header: {3:s}"
203
+ print(out.format(twcorr[0], twcorr[1], cchdr0, cchdr1))
204
+ print(f" Convergence criterion: {convtype:s}")
205
+ if ccpara.stackwgt == "coef":
206
+ wgtcoef = True
207
+ else:
208
+ wgtcoef = False
209
+ taperwindow = sacpkl.taperWindow(twcorr, taperwidth)
210
+ # get initial time picks
211
+ tinis = array([sacdh.gethdr(cchdr0) for sacdh in saclist])
212
+ tfins = tinis.copy()
213
+ nseis = len(saclist)
214
+ ccc = zeros(nseis)
215
+ snr = zeros(nseis)
216
+ coh = zeros(nseis)
217
+ wgts = ones(nseis)
218
+ stkdata = []
219
+ datatype = "datamem"
220
+ for it in range(maxiter):
221
+ # recut data and update array stack
222
+ nstart, ntotal = sacpkl.windowIndex(saclist, tfins, twcorr, taperwindow)
223
+ windata = sacpkl.windowData(
224
+ saclist, nstart, ntotal, taperwidth, tapertype, datatype
225
+ )
226
+ sdata = normWeightStack(windata, wgts, taperwidth, tapertype)
227
+ stkdata.append(sdata)
228
+ if it == 0:
229
+ print(f"=== Iteration {it:d} : epsilon")
230
+ else:
231
+ conv = convergence(stkdata[it], stkdata[it - 1])
232
+ print(f"=== Iteration {it:d} : {conv:8.6f}")
233
+ if conv <= convepsi:
234
+ print(
235
+ f" Array stack converged... Done. Mean corrcoef={mean(ccc):.3f}"
236
+ )
237
+ break
238
+ # Find time lag at peak correlation between each trace and the array stack.
239
+ # Calculate cross correlation coefficient, signal/noise ratio and temporal coherence
240
+ sdatanorm = sdata / LA.norm(sdata)
241
+ for i in range(nseis):
242
+ datai = windata[i]
243
+ delay, ccmax, ccpol = corrmax(sdata, datai, delta, xcorr, shift)
244
+ tfins[i] += delay
245
+ sacdh = saclist[i]
246
+ sacdh.sethdr(cchdr1, tfins[i])
247
+ if wgtcoef: # update weight only when stackwgt == coef
248
+ wgts[i] = ccpol * ccmax
249
+ ccc[i] = ccmax
250
+ sacdh.sethdr(hdrccc, ccc[i])
251
+ snr[i] = snratio(datai, delta, twcorr)
252
+ sacdh.sethdr(hdrsnr, snr[i])
253
+ coh[i] = coherence(datai * ccpol, sdatanorm)
254
+ sacdh.sethdr(hdrcoh, coh[i])
255
+ # get maximum time window for plot (excluding taperwindow)
256
+ bb, ee = [], []
257
+ for i in range(nseis):
258
+ sacdh = saclist[i]
259
+ b = sacdh.b - tfins[i]
260
+ e = b + (sacdh.npts - 1) * delta
261
+ bb.append(b + delta)
262
+ ee.append(e - delta)
263
+ b = max(bb)
264
+ e = min(ee)
265
+ d = (e - b) * taperwidth / 2
266
+ twplot = [b + d, e - d]
267
+ # calculate final stack at twplot, save to a sacdh object: stkdh
268
+ # set time picks of stkdh as mean of tinis and tfins
269
+ taperwindow = sacpkl.taperWindow(twplot, taperwidth)
270
+ nstart, ntotal = sacpkl.windowIndex(saclist, tfins, twplot, taperwindow)
271
+ windata = sacpkl.windowData(
272
+ saclist, nstart, ntotal, taperwidth, tapertype, datatype
273
+ )
274
+ sdatamem = normWeightStack(windata, wgts, taperwidth, tapertype)
275
+ # also create stack from original data
276
+ datatype = "data"
277
+ windata = sacpkl.windowData(
278
+ saclist, nstart, ntotal, taperwidth, tapertype, datatype
279
+ )
280
+ sdata = normWeightStack(windata, wgts, taperwidth, tapertype)
281
+ tinimean = mean(tinis)
282
+ tfinmean = mean(tfins)
283
+ stkdh = copy.copy(saclist[0])
284
+ stkdh.thdrs = [
285
+ -12345.0,
286
+ ] * 10
287
+ stkdh.users = [
288
+ -12345.0,
289
+ ] * 10
290
+ stkdh.kusers = [
291
+ "-1234567",
292
+ ] * 3
293
+ stkdh.b = twplot[0] - taperwindow * 0.5 + tfinmean
294
+ stkdh.npts = len(sdata)
295
+ stkdh.data = sdata
296
+ stkdh.sethdr(cchdr0, tinimean)
297
+ stkdh.sethdr(cchdr1, tfinmean)
298
+ stkdh.knetwk = "Array"
299
+ stkdh.kstnm = "Stack"
300
+ stkdh.netsta = "Array.Stack"
301
+ stkdh.gcarc = -1
302
+ stkdh.dist = -1
303
+ stkdh.baz = -1
304
+ stkdh.az = -1
305
+ stkdh.stla = 0
306
+ stkdh.stlo = 0
307
+ stkdh.stel = 0
308
+ stkdh.delta = delta
309
+ stkdh.e = stkdh.b + (stkdh.npts - 1) * delta
310
+ stkdh.time = linspace(stkdh.b, stkdh.b + (stkdh.npts - 1) * stkdh.delta, stkdh.npts)
311
+ stkdh.datamem = sdatamem
312
+ # set time window
313
+ stkdh.sethdr(twhdrs[0], twcorr[0] + tfinmean)
314
+ stkdh.sethdr(twhdrs[1], twcorr[1] + tfinmean)
315
+ stkdh.twindow = twcorr[0] + tfinmean, twcorr[1] + tfinmean
316
+ if opts.fstack is None:
317
+ stkdh.filename = ccpara.fstack
318
+ else:
319
+ stkdh.filename = opts.fstack
320
+ for sacdh, tfin in zip(saclist, tfins):
321
+ sacdh.sethdr(twhdrs[0], tfin + twcorr[0])
322
+ sacdh.sethdr(twhdrs[1], tfin + twcorr[1])
323
+ sacdh.twindow = tfin + twcorr[0], tfin + twcorr[1]
324
+ quas = array([ccc, snr, coh])
325
+ return stkdh, stkdata, quas
326
+
327
+
328
+ def coConverg(stack0, stack1):
329
+ """
330
+ Calcuate criterion of convergence by correlation coefficient.
331
+ stack0 and stack1 are current stack and stack from last iteration.
332
+ """
333
+ return 1 - corrcoef(stack0, stack1)[0][1]
334
+
335
+
336
+ def reConverg(stack0, stack1):
337
+ """
338
+ Calcuate criterion of convergence by change of stack.
339
+ stack0 and stack1 are current stack and stack from last iteration.
340
+ """
341
+ return LA.norm(stack0 - stack1, 1) / LA.norm(stack0, 2) / len(stack0)
342
+
343
+
344
+ def snratio(data, delta, timewindow):
345
+ """
346
+ Calculate signal/noise ratio within the given time window.
347
+ Time window is relative, such as [-10, 20], to the onset of the arrival.
348
+ """
349
+ tw0, tw1 = timewindow
350
+ nn = int(round(-tw0 / delta))
351
+ yn = data[:nn]
352
+ ys = data[nn:]
353
+ ns = len(ys)
354
+ rr = LA.norm(ys) / LA.norm(yn) * sqrt(nn) / sqrt(ns)
355
+ if LA.norm(yn) == 0:
356
+ print("snr", LA.norm(yn))
357
+ # the same as:
358
+ # rr = sqrt(sum(square(ys))/sum(square(yn))*nn/ns)
359
+ # shoud signal be the whole time seris?
360
+ # yw = data[:]
361
+ # nw = len(yw)
362
+ # rw = sqrt(sum(square(yw))/sum(square(yn))*nn/nw)
363
+ return rr
364
+
365
+
366
+ def coherence(datai, datas):
367
+ """
368
+ Calculate time domain coherence.
369
+ Coherence is 1 - sin of the angle made by two vectors: di and ds.
370
+ Di is data vector, and ds is the unit vector of array stack.
371
+ res(di) = di - (di . ds) ds
372
+ coh(di) = 1 - res(di) / ||di||
373
+ """
374
+ return 1 - LA.norm(datai - dot(datai, datas) * datas) / LA.norm(datai)
375
+
376
+
377
+ def plotiter(stkdata):
378
+ import matplotlib.pyplot as plt
379
+
380
+ plt.figure()
381
+ for i in range(len(stkdata)):
382
+ plt.plot(stkdata[i], label="iter" + str(i))
383
+ plt.legend()
384
+ plt.show()
385
+
386
+
387
+ def autoiccs(gsac, opts):
388
+ """Run ICCS and delete low quality seismograms automatically."""
389
+ saclist = gsac.saclist
390
+ hdrsel = opts.ccpara.hdrsel
391
+ minqual = opts.minqual
392
+ minnsel = opts.minnsel
393
+ minccc, minsnr, mincoh = minqual
394
+
395
+ selist, _ = qualsort.seleSeis(saclist)
396
+ print(
397
+ f"\n*** Run ICCS until all low quality seismograms removed: Min_ccc={minccc:.2f} Min_snr={minsnr:.1f} Min_coh={mincoh:.2f} *** "
398
+ )
399
+ rerun = True
400
+ while rerun and len(selist) >= minnsel:
401
+ stkdh, _, quas = ccWeightStack(selist, opts)
402
+ tquas = transpose(quas)
403
+ indsel, inddel = [], []
404
+ for i in range(len(selist)):
405
+ sacdh = selist[i]
406
+ ccc, snr, coh = tquas[i]
407
+ if ccc < minccc or snr < minsnr or coh < mincoh:
408
+ inddel.append(i)
409
+ sacdh.sethdr(hdrsel, "False")
410
+ sacdh.selected = False
411
+ print(
412
+ f"--> Seismogram: {sacdh.filename:s} quality factors {ccc:.2f} {snr:.2f} {coh:.2f} < min. Deleted. "
413
+ )
414
+ else:
415
+ indsel.append(i)
416
+ if len(inddel) > 0:
417
+ selist = [selist[i] for i in indsel]
418
+ else:
419
+ rerun = False
420
+ gsac.stkdh = stkdh
421
+ gsac.selist = selist
422
+ nsel = len(selist)
423
+ print(f"\nDone selecting seismograms: {nsel:d} out of {len(saclist):d} selected.")
424
+
425
+ save = input("Save to file? [y/n] \n")
426
+ if save[0].lower() == "y":
427
+ if opts.filemode == "sac":
428
+ for sacdh in saclist:
429
+ sacdh.writeHdrs()
430
+ gsac.stkdh.savesac()
431
+ elif opts.filemode == "pkl":
432
+ print(" Saving gsac to pickle file...")
433
+ sacpkl.writePickle(gsac, opts.pklfile, opts.zipmode)
434
+ if opts.zipmode is not None:
435
+ pklfile = opts.pklfile + "." + opts.zipmode
436
+ else:
437
+ pklfile = opts.pklfile
438
+ if nsel < minnsel:
439
+ os.rename(pklfile, "deleted." + pklfile)
440
+ print(f" Less than {minnsel:d} seismograms selected. Remove pkl.")
441
+
442
+
443
+ def checkCoverage(gsac, opts, textra=0.0):
444
+ """Check if each seismogram has enough samples around the time window relative to ipick."""
445
+ ipick = opts.ipick
446
+ tw0, tw1 = opts.twcorr
447
+ saclist = gsac.saclist
448
+ nsac = len(saclist)
449
+ indsel, inddel = [], []
450
+ for i in range(nsac):
451
+ sacdh = saclist[i]
452
+ t0 = sacdh.gethdr(ipick)
453
+ b = sacdh.b
454
+ e = b + (sacdh.npts - 1) * sacdh.delta
455
+ if b - textra > t0 + tw0 or e + textra < t0 + tw1:
456
+ inddel.append(i)
457
+ print(
458
+ f"Seismogram {sacdh.filename:s} does not have enough sample. Deleted."
459
+ )
460
+ elif LA.norm(sacdh.data) == 0.0:
461
+ inddel.append(i)
462
+ print(f"Seismogram {sacdh.filename:s} has zero L2 norm. Deleted.")
463
+ else:
464
+ indsel.append(i)
465
+ if inddel != []:
466
+ gsac.saclist = [saclist[i] for i in indsel]
467
+ # print ('Updating gsac pickle file..')
468
+ # writePickle(gsac, opts.pklfile, opts.zipmode)
469
+
470
+
471
+ def main():
472
+ cli_deprecation_notice()
473
+ opts, ifiles = getOptions()
474
+ ccpara = ttconfig.CCConfig()
475
+ gsac = sacpkl.loadData(ifiles, opts, ccpara)
476
+ opts.ccpara = ccpara
477
+ ccpara.twcorr = opts.twcorr
478
+ ccpara.cchdrs = [opts.ipick, opts.wpick]
479
+ # check data coverage, initialize quality factors
480
+ checkCoverage(gsac, opts)
481
+ qualsort.initQual(gsac.saclist, opts.ccpara.hdrsel, opts.ccpara.qheaders)
482
+ pdata.seisTimeData(gsac.saclist)
483
+
484
+ if opts.auto_on:
485
+ autoiccs(gsac, opts)
486
+ elif opts.auto_on_all:
487
+ print("Selecting all seismograms..")
488
+ hdrsel = opts.ccpara.hdrsel
489
+ for sacdh in gsac.saclist:
490
+ sacdh.selected = True
491
+ sacdh.sethdr(hdrsel, "True")
492
+ autoiccs(gsac, opts)
493
+ else:
494
+ stkdh, stkdata, _ = ccWeightStack(gsac.saclist, opts)
495
+ gsac.stkdh = stkdh
496
+ sacpkl.saveData(gsac, opts)
497
+ if opts.plotiter:
498
+ plotiter(stkdata)
499
+
500
+
501
+ if __name__ == "__main__":
502
+ main()