DASPy-toolbox 1.0.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.
Files changed (49) hide show
  1. DASPy_toolbox-1.0.0.dist-info/LICENSE.txt +1 -0
  2. DASPy_toolbox-1.0.0.dist-info/METADATA +85 -0
  3. DASPy_toolbox-1.0.0.dist-info/RECORD +49 -0
  4. DASPy_toolbox-1.0.0.dist-info/WHEEL +5 -0
  5. DASPy_toolbox-1.0.0.dist-info/entry_points.txt +2 -0
  6. DASPy_toolbox-1.0.0.dist-info/top_level.txt +1 -0
  7. daspy/__init__.py +4 -0
  8. daspy/advanced_tools/__init__.py +0 -0
  9. daspy/advanced_tools/channel.py +354 -0
  10. daspy/advanced_tools/decomposition.py +165 -0
  11. daspy/advanced_tools/denoising.py +276 -0
  12. daspy/advanced_tools/fdct.py +789 -0
  13. daspy/advanced_tools/strain2vel.py +245 -0
  14. daspy/basic_tools/__init__.py +0 -0
  15. daspy/basic_tools/filter.py +257 -0
  16. daspy/basic_tools/freqattributes.py +117 -0
  17. daspy/basic_tools/preprocessing.py +238 -0
  18. daspy/basic_tools/visualization.py +186 -0
  19. daspy/core/__init__.py +4 -0
  20. daspy/core/collection.py +279 -0
  21. daspy/core/dasdatetime.py +72 -0
  22. daspy/core/example.pkl +0 -0
  23. daspy/core/make_example.py +32 -0
  24. daspy/core/read.py +544 -0
  25. daspy/core/section.py +1319 -0
  26. daspy/core/write.py +282 -0
  27. daspy/seismic_detection/__init__.py +1 -0
  28. daspy/seismic_detection/calc_travel_time.py +23 -0
  29. daspy/seismic_detection/core.py +119 -0
  30. daspy/seismic_detection/detection.py +12 -0
  31. daspy/seismic_detection/gamma/__init__.py +13 -0
  32. daspy/seismic_detection/gamma/_base.py +549 -0
  33. daspy/seismic_detection/gamma/_bayesian_mixture.py +875 -0
  34. daspy/seismic_detection/gamma/_gaussian_mixture.py +866 -0
  35. daspy/seismic_detection/gamma/app.py +192 -0
  36. daspy/seismic_detection/gamma/seismic_ops.py +478 -0
  37. daspy/seismic_detection/gamma/utils.py +512 -0
  38. daspy/seismic_detection/location.py +266 -0
  39. daspy/seismic_detection/magnitude.py +43 -0
  40. daspy/seismic_detection/phase_picking.py +67 -0
  41. daspy/structure_imaging/__init__.py +0 -0
  42. daspy/structure_imaging/ambient_noise.py +4 -0
  43. daspy/structure_imaging/dispersion.py +27 -0
  44. daspy/structure_imaging/fault_zone.py +59 -0
  45. daspy/structure_imaging/inversion.py +6 -0
  46. daspy/traffic_monitoring/JamDetection.py +6 -0
  47. daspy/traffic_monitoring/SpeedMeasurement.py +6 -0
  48. daspy/traffic_monitoring/VehicleDetection.py +6 -0
  49. daspy/traffic_monitoring/__init__.py +0 -0
daspy/core/section.py ADDED
@@ -0,0 +1,1319 @@
1
+ # Purpose: Module for handling Section objects.
2
+ # Author: Minzhe Hu
3
+ # Date: 2024.10.31
4
+ # Email: hmz2018@mail.ustc.edu.cn
5
+ import warnings
6
+ import os
7
+ import numpy as np
8
+ from copy import deepcopy
9
+ from typing import Iterable
10
+ from datetime import datetime
11
+ from daspy.core.dasdatetime import DASDateTime, utc
12
+ from daspy.core.write import write
13
+ from daspy.basic_tools.visualization import plot
14
+ from daspy.basic_tools.preprocessing import (phase2strain, normalization,
15
+ demeaning, detrending, stacking,
16
+ cosine_taper, downsampling,
17
+ padding, trimming,
18
+ time_integration,
19
+ time_differential,
20
+ distance_integration)
21
+ from daspy.basic_tools.filter import (bandpass, bandstop, lowpass,
22
+ lowpass_cheby_2, highpass, envelope)
23
+ from daspy.basic_tools.freqattributes import (spectrum, spectrogram,
24
+ fk_transform)
25
+ from daspy.advanced_tools.channel import channel_checking, turning_points
26
+ from daspy.advanced_tools.denoising import (curvelet_denoising,
27
+ common_mode_noise_removal,
28
+ spike_removal)
29
+ from daspy.advanced_tools.decomposition import fk_filter, curvelet_windowing
30
+ from daspy.advanced_tools.strain2vel import (slant_stacking, fk_rescaling,
31
+ curvelet_conversion)
32
+
33
+
34
+ class Section(object):
35
+ def __init__(self, data, dx, fs, start_channel=0, start_distance=0,
36
+ start_time=0, **kwargs):
37
+ """
38
+ :param data: numpy.ndarray. Data recorded by DAS.
39
+ :param dx: number. Channel interval in m.
40
+ :param fs: number. Sampling rate in Hz.
41
+ :param start_channel: int. Channel number of the first channel.
42
+ :param start_distance: number. Distance of the first channel, in m.
43
+ :param start_time: number or DASDateTime. Time of the first
44
+ sampling point. If number, the unit is s.
45
+ :param origin_time: number or DASDateTime. Ocurance time of the event.
46
+ :param gauge_length: number. Gauge length in m.
47
+ :param data_type: str. Can be 'phase shift', 'phase change rate',
48
+ 'strain', 'strain rate', 'displacement', 'velocity', 'acceleration',
49
+ or normalized above parameters.
50
+ :param scale: number. Scale or gain of data.
51
+ :param geometry: numpy.ndarray. Should include latitude and longitude
52
+ (first two columns), and can also include depth (last column).
53
+ :param turning_channels: sequnce of channel numbers. Channel numbers of
54
+ turning points.
55
+ :param headers: dict. Other headers.
56
+ :param source: str or pathlib.PosixPath. Path to the source file.
57
+ :param source_type: str. Raw type it read from.
58
+ """
59
+ if data.ndim == 1:
60
+ data = data[np.newaxis, :]
61
+ self.data = data
62
+ self.dx = dx
63
+ self.fs = fs
64
+ self.start_channel = start_channel
65
+ self.start_distance = start_distance
66
+ self.start_time = start_time
67
+ opt_attrs = ['origin_time', 'gauge_length', 'data_type', 'scale',
68
+ 'geometry', 'turning_channels', 'headers', 'source',
69
+ 'source_type']
70
+ for attr in opt_attrs:
71
+ if attr in kwargs:
72
+ setattr(self, attr, kwargs.pop(attr))
73
+
74
+ def __str__(self):
75
+ n = max(map(len, self.__dict__.keys()))
76
+ describe = '{}: shape{}\n'.format('data'.rjust(n), self.data.shape)
77
+ for key, value in self.__dict__.items():
78
+ if key == 'data':
79
+ continue
80
+ if key == 'geometry':
81
+ describe += '{}: shape{}\n'.format(key.rjust(n), value.shape)
82
+ elif key in ['dx', 'start_distance', 'gauge_length']:
83
+ describe += '{}: {} m\n'.format(key.rjust(n), value)
84
+ elif key == 'fs':
85
+ describe += '{}: {} Hz\n'.format(key.rjust(n), value)
86
+ elif key == 'start_time':
87
+ if isinstance(value, DASDateTime):
88
+ describe += '{}: {}\n'.format(key.rjust(n), value)
89
+ else:
90
+ describe += '{}: {} s\n'.format(key.rjust(n), value)
91
+ else:
92
+ describe += '{}: {}\n'.format(key.rjust(n), value)
93
+ return describe
94
+
95
+ __repr__ = __str__
96
+
97
+ def __add__(self, other):
98
+ """
99
+ Join two sections in time.
100
+ """
101
+ out = self.copy()
102
+ if isinstance(other, Section):
103
+ if other.dx != self.dx:
104
+ if self.dx is None:
105
+ out.dx = other.dx
106
+ elif other.dx is not None:
107
+ raise ValueError('These two Sections have different dx, '
108
+ 'please check.')
109
+ if other.fs != self.fs:
110
+ if self.fs is None:
111
+ out.fs = other.fs
112
+ elif other.fs is not None:
113
+ raise ValueError('These two Sections have different fs, '
114
+ 'please check.')
115
+ if isinstance(self.start_time, DASDateTime) and \
116
+ isinstance(other.start_time, DASDateTime):
117
+ if abs(other.start_time - self.end_time) > 0.1:
118
+ if abs(other.end_time - self.start_time) <= 0.1:
119
+ warnings.warn('According to the time information of the'
120
+ ' two Sections, the order of addition is '
121
+ 'reversed.')
122
+ return other + self
123
+ else:
124
+ warnings.warn('The start time of the second Section '
125
+ f'({other.start_time}) is inconsistent '
126
+ 'with the end time of the first Section ('
127
+ f'{self.end_time}).')
128
+ data = other.data
129
+ elif isinstance(other, np.ndarray):
130
+ data = other
131
+ elif isinstance(other, list):
132
+ data = np.array(other)
133
+ else:
134
+ raise TypeError('The input should be Section or np.ndarray.')
135
+
136
+ if len(data) != self.nch:
137
+ if len(data[0]) == self.nch:
138
+ data = data.T
139
+ else:
140
+ raise ValueError('These two Sections have different number of '
141
+ 'channels, please check.')
142
+ if out.data is None:
143
+ out.data = data
144
+ else:
145
+ out.data = np.hstack((out.data, data))
146
+
147
+ return out
148
+
149
+ @property
150
+ def shape(self):
151
+ return self.data.shape
152
+
153
+ @property
154
+ def dt(self):
155
+ return 1 / self.fs
156
+
157
+ @property
158
+ def nch(self):
159
+ return len(self.data)
160
+
161
+ @property
162
+ def nt(self):
163
+ return self.data.shape[1]
164
+
165
+ @property
166
+ def end_channel(self):
167
+ return self.start_channel + self.nch - 1
168
+
169
+ @property
170
+ def distance(self):
171
+ return self.nch * self.dx
172
+
173
+ @property
174
+ def end_distance(self):
175
+ return self.start_distance + self.nch * self.dx
176
+
177
+ @property
178
+ def duration(self):
179
+ return self.nt / self.fs
180
+
181
+ @property
182
+ def end_time(self):
183
+ return self.start_time + self.nt / self.fs
184
+
185
+ def copy(self):
186
+ return deepcopy(self)
187
+
188
+ @classmethod
189
+ def from_obspy_stream(cls, st):
190
+ """
191
+ Construct a Section from a obspy.core.stream.Stream instance.
192
+
193
+ :param patch: obspy.core.stream.Stream. An instance of
194
+ obspy.core.stream.Stream for construction.
195
+ """
196
+ stime = min([tr.stats['starttime'] for tr in st])
197
+ etime = max([tr.stats['endtime'] for tr in st])
198
+ st.trim(starttime=stime, endtime=etime, pad=True, fill_value=0)
199
+ matadata = [(tr.stats['sampling_rate'], tr.stats['delta'],
200
+ tr.stats['npts'], tr.stats['calib']) for tr in st]
201
+ assert len(set(matadata)) == 1, ('The metadata of all traces in the '
202
+ 'stream should be the same.')
203
+ nch = len(st)
204
+ nt = st[0].stats.npts
205
+ fs = st[0].stats.sampling_rate
206
+ start_time = DASDateTime.from_datetime(st[0].stats.starttime.datetime).\
207
+ replace(tzinfo=utc)
208
+ scale = st[0].stats.calib
209
+ source = type(st)
210
+ data = np.zeros((nch, nt))
211
+
212
+ if str.isdigit(st[0].stats.channel):
213
+ channel_no = np.zeros(nch)
214
+ for i, tr in enumerate(st):
215
+ channel_no[i] = int(tr.stats.channel)
216
+ if sum(np.diff(np.sort(channel_no)) - 1) > 0:
217
+ channel_no = np.arange(nch)
218
+ else:
219
+ channel_no = np.arange(nch)
220
+ start_channel = min(channel_no)
221
+ channel_no -= start_channel
222
+ for i, tr in enumerate(st):
223
+ data[channel_no[i]] = tr.data
224
+
225
+ warnings.warn('obspy.core.stream.Stream doesn\'t include channel '
226
+ 'interval. Please set dx manually.')
227
+ return cls(data.astype(float), None, fs, start_channel=start_channel,
228
+ start_time=start_time, scale=scale, source=source)
229
+
230
+ @classmethod
231
+ def from_dascore_patch(cls, patch):
232
+ """
233
+ Construct a Section from a dascore.core.patch.Patch instance.
234
+
235
+ :param patch: dascore.core.patch.Patch. An instance of
236
+ dascore.core.patch.Patch for construction.
237
+ :return: daspy.Section.
238
+ """
239
+ kwargs = {}
240
+ if patch.dims == ('time', 'distance'):
241
+ data = patch.data.T
242
+ dx = patch.coords.coord_map['distance'].step
243
+ kwargs['start_distance'] = patch.coords.coord_map['distance'].start
244
+ elif patch.dims == ('time', 'channel'):
245
+ data = patch.data.T
246
+ dx = 1
247
+ warnings.warn('This dascore.core.patch.Patch instance doesn\'t '
248
+ 'include channel interval. Set dx to 1.')
249
+ kwargs['start_channel'] = patch.coords.coord_map['channel'].start
250
+ elif patch.dims == ('distance', 'time'):
251
+ data = patch.data
252
+ dx = patch.coords.coord_map['distance'].step
253
+ kwargs['start_distance'] = patch.coords.coord_map['distance'].start
254
+ elif patch.dim == ('channel', 'time'):
255
+ data = patch.data
256
+ dx = 1
257
+ warnings.warn('This dascore.core.patch.Patch instance doesn\'t '
258
+ 'include channel interval. Set dx to 1.')
259
+ kwargs['start_channel'] = patch.coords.coord_map['channel'].start
260
+
261
+ if isinstance(patch.coords.coord_map['time'].step, np.timedelta64):
262
+ start_time = DASDateTime.fromtimestamp(
263
+ patch.coords.coord_map['time'].start.item() / 1e9, tz=utc)
264
+ fs = np.timedelta64(1, 's') / patch.coords.coord_map['time'].step
265
+ else:
266
+ start_time = patch.coords.coord_map['time'].start
267
+ fs = patch.coords.coord_map['time'].step
268
+
269
+ if hasattr(patch.attrs, 'gauge_length'):
270
+ kwargs['gauge_length'] = patch.attrs.gauge_length
271
+ if patch.attrs.data_type:
272
+ kwargs['data_type'] = ' '.join(patch.attrs.data_type.split('_'))
273
+ sec = cls(data, dx, fs, start_time=start_time,
274
+ headers=patch.attrs, source=type(patch), **kwargs)
275
+ return sec
276
+
277
+ @classmethod
278
+ def from_lightguide_blast(cls, blast):
279
+ """
280
+ Construct a Section from a lightguide.blast.Blast instance.
281
+
282
+ :param blast: lightguide.blast.Blast. An instance of
283
+ lightguide.blast.Blast for construction.
284
+ :return: daspy.Section.
285
+ """
286
+ sec = cls(blast.data, blast.channel_spacing, blast.sampling_rate,
287
+ start_time=DASDateTime.from_datetime(blast.start_time),
288
+ start_channel=blast.start_channel, data_type=blast.unit,
289
+ source=type(blast))
290
+ return sec
291
+
292
+ def to_obspy_stream(self):
293
+ """
294
+ Construct an instance of obspy.core.stream.Stream.
295
+
296
+ :return: obspy.core.stream.Stream.
297
+ """
298
+ from obspy import Stream, Trace, UTCDateTime
299
+ st = Stream()
300
+ header = {'sampling_rate': self.fs}
301
+ if not isinstance(datetime, self.start_time):
302
+ warnings.warn('The type of start_time is not DASDateTime. The '
303
+ 'starttime of Trace instances may be wrong')
304
+ header['starttime'] = UTCDateTime(self.start_time)
305
+ else:
306
+ header['starttime'] = UTCDateTime(self.start_time.timestamp())
307
+ if hasattr(self, 'scale'):
308
+ header['calib'] = self.scale
309
+ for i in range(self.nch):
310
+ header_tr = deepcopy(header)
311
+ header_tr['channel'] = str(self.start_channel + i)
312
+ tr = Trace(self.data[i], header_tr)
313
+ st += tr
314
+
315
+ warnings.warn('obspy.core.stream.Stream doesn\'t include channel '
316
+ 'interval.')
317
+ return st
318
+
319
+ def to_dascore_patch(self):
320
+ """
321
+ Construct an instance of dascore.core.patch.Patch.
322
+
323
+ :return: dascore.core.patch.Patch.
324
+ """
325
+ from pint import Quantity
326
+ from datetime import datetime
327
+ from dascore.core import Patch, CoordManager
328
+ from dascore.core.coords import CoordRange
329
+ from dascore.utils.mapping import FrozenDict
330
+
331
+ dims = ('time', 'distance')
332
+ if isinstance(self.start_time, datetime):
333
+ if self.start_time.tzinfo:
334
+ stime = np.datetime64(self.start_time.astimezone(utc).
335
+ replace(tzinfo=None))
336
+ etime = np.datetime64(self.end_time.astimezone(utc).
337
+ replace(tzinfo=None))
338
+ else:
339
+ stime = np.datetime64(self.start_time.replace(tzinfo=None))
340
+ etime = np.datetime64(self.end_time.replace(tzinfo=None))
341
+
342
+ time_range = CoordRange(units=(Quantity(1, 'second')),
343
+ step=np.timedelta64(int(self.dt * 1e9),
344
+ 'ns'),
345
+ start=stime, stop=etime)
346
+ else:
347
+ stime = self.start_time
348
+ etime = self.end_time
349
+ time_range = CoordRange(units=(Quantity(1, 'second')), step=self.dt,
350
+ start=stime, stop=etime)
351
+
352
+ dist_range = CoordRange(units=(Quantity(1, 'meter')), step=self.dx,
353
+ start=self.start_distance,
354
+ stop=self.end_distance)
355
+ coord_map = FrozenDict({'time': time_range, 'distance': dist_range})
356
+ dim_map = FrozenDict({'time': ('time',), 'distance': ('distance', )})
357
+ coords = CoordManager(dims=dims, coord_map=coord_map,
358
+ dim_map=dim_map)
359
+
360
+ if self.source == Patch:
361
+ attrs = self.headers
362
+ else:
363
+ from dascore.io.prodml.core import ProdMLPatchAttrs
364
+ attrs = ProdMLPatchAttrs(coords=coords)
365
+ kwargs = {}
366
+ if hasattr(self, 'data_type'):
367
+ kwargs['data_type'] = '_'.join(self.data_type.split(' '))
368
+ if hasattr(self, 'gauge_length'):
369
+ kwargs['gauge_length'] = self.gauge_length
370
+ kwargs['gauge_length_units'] = Quantity(1, 'meter')
371
+
372
+ return Patch(self.data.T, coords, dims, attrs)
373
+
374
+ def to_lightguide_blast(self):
375
+ """
376
+ Construct an instance of lightguide.blast.Blast
377
+
378
+ :return: lightguide.blast.Blast.
379
+ """
380
+ from lightguide.blast import Blast
381
+ if not isinstance(self.start_time, datetime):
382
+ warnings.warn('The type of start_time is not DASDateTime. The '
383
+ 'starttime of Trace instances may be wrong')
384
+ start_time = datetime.fromtimestamp(self.start_time)
385
+ elif isinstance(self.start_time, DASDateTime):
386
+ start_time = self.start_time.to_datetime()
387
+ if start_time.tzinfo is None:
388
+ start_time = start_time.astimezone(utc)
389
+ if hasattr(self, 'data_type'):
390
+ for key in ['strain rate', 'strain', 'displacement', 'velocity',
391
+ 'acceleration']:
392
+ if key in self.data_type.lower():
393
+ if self.data_type.lower() == key:
394
+ unit = key
395
+ else:
396
+ warnings.warn(f'Data type {self.data_type} is not '
397
+ f'supported in lightguide. Set to {key}.')
398
+ break
399
+ else:
400
+ warnings.warn(f'Data type {unit} is not supported in '
401
+ 'lightguide. Set to default (starin rate).')
402
+ unit = 'strain rate'
403
+ else:
404
+ print('Set unit to default (starin rate).')
405
+ unit = 'strain rate'
406
+ return Blast(self.data, start_time, self.fs,
407
+ start_channel=self.start_channel, channel_spacing=self.dx,
408
+ unit=unit)
409
+
410
+ def save(self, fname=None, ftype=None, keep_format=False):
411
+ """
412
+ Save the instance as a pickle file or update the raw file and resave as
413
+ new file.
414
+
415
+ :param fname: str or pathlib.PosixPath. Path of new DAS data file to
416
+ save.
417
+ :param ftype: None or str. None for automatic detection), or 'pkl',
418
+ 'pickle', 'tdms', 'h5', 'hdf5', 'segy', 'sgy', 'npy'.
419
+ :param keep_format: bool. If True, we will make a copy of the
420
+ self.source file and make changes to it. This will strictly preserve
421
+ the original format, but will cost more IO resources.
422
+ """
423
+ if fname is None:
424
+ if hasattr(self, 'source'):
425
+ fname_list = self.source.split('.')
426
+ fname_list[-2] += '_new'
427
+ fname = '.'.join(fname_list)
428
+ else:
429
+ fname = 'section.pkl'
430
+
431
+ if ftype is None:
432
+ ftype = str(fname).lower().split('.')[-1]
433
+
434
+ for rtp in [('pickle', 'pkl'), ('hdf5', 'h5'), ('segy', 'sgy')]:
435
+ ftype = ftype.replace(*rtp)
436
+
437
+ if keep_format:
438
+ if not hasattr(self, 'source'):
439
+ raise ValueError('self.source not exit.')
440
+ if not os.path.isfile(self.source):
441
+ raise ValueError('self.source is not a file.')
442
+ if ftype != self.source_type:
443
+ raise ValueError('self.source_type is different from ftype.')
444
+ write(self, fname, ftype=ftype, raw_fname=self.source)
445
+ else:
446
+ write(self, fname, ftype=ftype)
447
+
448
+ return self
449
+
450
+ def channel_data(self, use_channel, replace=False):
451
+ """
452
+ Extract data of one channel or several channels.
453
+ """
454
+ channel = deepcopy(use_channel)
455
+ channel -= self.start_channel
456
+ data = self.data[channel]
457
+ if replace:
458
+ self.data = data
459
+ self.start_channel += channel[0]
460
+ self.start_distance += channel[0] * self.dx
461
+ return self
462
+ else:
463
+ return data
464
+
465
+ def plot(self, xmode='distance', tmode='origin', obj='waveform',
466
+ kwargs_pro={}, **kwargs):
467
+ """
468
+ Plot several types of 2-D seismological data.
469
+
470
+ :param xmode: str. 'distance' or 'channel'.
471
+ :param tmode: str. 'origin', 'start', 'time' or 'sampling'. If
472
+ origin_time is not defined, 'origin' and 'start' is the same.
473
+ :param obj: str. Type of data to plot. It should be one of 'waveform',
474
+ 'phasepick', 'spectrum', 'spectrogram', 'fk', or 'dispersion'.
475
+ :param kwargs_pro: dict. If obj is one of 'spectrum', 'spectrogram',
476
+ 'fk' and data is not specified, this parameter will be used to
477
+ process the data to plot.
478
+ :param ax: Matplotlib.axes.Axes or tuple. Axes to plot. A tuple for new
479
+ figsize. If not specified, the function will directly display the
480
+ image using matplotlib.pyplot.show().
481
+ :param dpi: int. The resolution of the figure in dots-per-inch.
482
+ :param title: str. The title of this axes.
483
+ :param transpose: bool. Transpose the figure or not.
484
+ :param cmap: str or Colormap. The Colormap instance or registered
485
+ colormap name used to map scalar data to colors.
486
+ :param vmin, vmax: Define the data range that the colormap covers.
487
+ :param xlim, ylim: Set the x-axis and y-axis view limits.
488
+ :param xlog, ylog: bool. If True, set the x-axis' or y-axis' scale as
489
+ log.
490
+ :param xinv, yinv: bool. If True, invert x-axis or y-axis.
491
+ :param xaxis, yaxis: bool. Show ticks and labels for x-axis or y-axis.
492
+ :param colorbar: bool, str or Matplotlib.axes.Axes. Bool means plot
493
+ colorbar or not. Str means the location of colorbar. Axes means the
494
+ Axes into which the colorbar will be drawn.
495
+ :param t0, x0: The beginning of time and space. Use instance's
496
+ properties by default
497
+ :param pick: Sequence of picked phases. Required if obj=='phasepick'.
498
+ :param c: Phase velocity sequence. Required if obj=='dispersion'.
499
+ :param data: numpy.ndarray. Data to plot. Required if obj is not
500
+ 'spectrum', 'spectrogram' and 'fk'.
501
+ :param f: Frequency sequence. Required if obj is one of 'spectrum',
502
+ 'spectrogram', 'fk' and data is specified, or obj is 'dispersion'.
503
+ :param k: Wavenumber sequence. Required if obj=='fk' and data is
504
+ specified.
505
+ :param t: Time sequence. Required if obj=='spectrogram' and data is
506
+ specified.
507
+ :param savefig: str or bool. Figure name to save if needed. If True,
508
+ it will be set to parameter obj.
509
+ """
510
+ if 'data' not in kwargs.keys():
511
+ if obj == 'waveform':
512
+ data = deepcopy(self.data)
513
+ elif obj == 'spectrum':
514
+ data, f = self.spectrum(**kwargs_pro)
515
+ kwargs['f'] = f
516
+ elif obj == 'spectrogram':
517
+ data, f, t = self.spectrogram(**kwargs_pro)
518
+ kwargs['f'] = f
519
+ kwargs['t'] = t
520
+ elif obj == 'fk':
521
+ data, f, k = self.fk_transform(**kwargs_pro)
522
+ kwargs['f'] = f
523
+ kwargs['k'] = k
524
+ if hasattr(self, 'scale'):
525
+ data *= self.scale
526
+ else:
527
+ data = kwargs.pop('data')
528
+
529
+ if 'ax' not in kwargs.keys() and 'title' not in kwargs.keys():
530
+ kwargs['title'] = obj
531
+ if hasattr(self, 'data_type'):
532
+ kwargs['title'] += f' ({self.data_type})'
533
+
534
+ if xmode == 'channel':
535
+ kwargs.setdefault('x0', self.start_channel)
536
+ elif xmode == 'distance':
537
+ kwargs.setdefault('x0', self.start_distance)
538
+ if tmode in ['origin', 'start', 'time']:
539
+ kwargs.setdefault('t0', self.start_time)
540
+ if tmode == 'origin':
541
+ if hasattr(self, 'origin_time'):
542
+ kwargs['t0'] -= self.origin_time
543
+ else:
544
+ tmode == 'start'
545
+ if tmode == 'start':
546
+ kwargs['t0'] -= self.start_time
547
+ tmode = 'time'
548
+ if hasattr(self, 'datatype'):
549
+ kwargs.setdefault('colorbar_label', self.datatype)
550
+
551
+ plot(data, self.dx, self.fs, obj=obj, xmode=xmode, tmode=tmode,
552
+ **kwargs)
553
+
554
+ def rescaling(self, scale=None):
555
+ """
556
+ Scale data according to a scale factor.
557
+
558
+ :param scale: float. It is required if the Section instance does
559
+ not specify the attribute 'scale'.
560
+ """
561
+ if scale is None:
562
+ try:
563
+ self.data *= self['scale']
564
+ except ValueError:
565
+ print('Please specify a scale factor.')
566
+ else:
567
+ if hasattr(self, 'scale') and self.scale != scale:
568
+ warnings.warn('The set scale is different from the previous '
569
+ 'self.scale.')
570
+ self.data *= scale
571
+ self.scale = 1
572
+ return self
573
+
574
+ def phase2strain(self, lam, e, n, gl=None):
575
+ """
576
+ Convert the optical phase shift in radians to strain, or phase change
577
+ rate to strain rate.
578
+
579
+ :param lam: float. Operational optical wavelength in vacuum.
580
+ :param e: float. photo-slastic scaling factor for logitudinal strain in
581
+ isotropic material.
582
+ :param n: float. Refractive index of the sensing fiber.
583
+ :paran gl: float. Gauge length. Required if self.gauge_length has not
584
+ been set.
585
+ """
586
+ if gl:
587
+ self.gauge_length = gl
588
+ self.data = phase2strain(self.data, lam, e, n, self.gauge_length)
589
+ if hasattr(self, 'data_type'):
590
+ if 'phase' not in self.data_type:
591
+ warnings.warn('The data type is {}, not phase shift. But it' +
592
+ 'still takes effect.'.format(self.data_type))
593
+ else:
594
+ self.data_type = self.data_type.replace(
595
+ 'phase shift', 'strain')
596
+ self.data_type = self.data_type.replace('phase change rate',
597
+ 'strain rate')
598
+ return self
599
+
600
+ def normalization(self, method='z-score'):
601
+ """
602
+ Normalize for each individual channel using Z-score method.
603
+
604
+ :param method: str. Method for normalization, should be one of 'max' or
605
+ 'z-score'.
606
+ """
607
+ self.data = normalization(self.data, method=method)
608
+ if hasattr(self, 'data_type'):
609
+ self.data_type = 'normed ' + self.data_type
610
+ return self
611
+
612
+ def demeaning(self):
613
+ """
614
+ Demean signal by subtracted mean of each channel.
615
+ """
616
+ self.data = demeaning(self.data)
617
+ return self
618
+
619
+ def detrending(self):
620
+ """
621
+ Detrend signal by subtracted a linear least-squares fit to data.
622
+ """
623
+ self.data = detrending(self.data)
624
+ return self
625
+
626
+ def stacking(self, N, step=None):
627
+ """
628
+ Stack several channels to increase the signal-noise ratio(SNR).
629
+
630
+ :param N: int. N adjacent channels stacked into 1.
631
+ :param step: int. Interval of data stacking.
632
+ """
633
+ if step is None:
634
+ step = N
635
+ self.data = stacking(self.data, N, step=step)
636
+ self.dx *= step
637
+ if hasattr(self, 'gauge_length'):
638
+ self.gauge_length += self.dx * (N - 1)
639
+ return self
640
+
641
+ def cosine_taper(self, p=0.1):
642
+ """
643
+ Taper using Tukey window.
644
+
645
+ :param p: float or sequence of floats. Each float means decimal
646
+ percentage of Tukey taper for corresponding dimension (ranging from
647
+ 0 to 1). Default is 0.1 which tapers 5% from the beginning and 5%
648
+ from the end. If only one float is given, it only do for time
649
+ dimension.
650
+ """
651
+ self.data = cosine_taper(self.data, p=p)
652
+ return self
653
+
654
+ def downsampling(self, xint=None, tint=None, stack=True,
655
+ lowpass_filter=True):
656
+ """
657
+ Downsample DAS data.
658
+
659
+ :param xint: int. Spatial downsampling factor.
660
+ :param tint: int. Time downsampling factor.
661
+ :param stack: bool. If True, stacking will replace decimation.
662
+ :param lowpass_filter: bool. Lowpass cheby2 filter before time
663
+ downsampling or not.
664
+ :return: Downsampled data.
665
+ """
666
+ self.data = downsampling(self.data, xint=xint, tint=tint, stack=stack,
667
+ lowpass_filter=lowpass_filter)
668
+ if xint and xint > 1:
669
+ self.dx *= xint
670
+ if hasattr(self, 'gauge_length'):
671
+ self.gauge_length += self.dx * (xint - 1)
672
+ if tint and tint > 1:
673
+ self.fs /= tint
674
+ return self
675
+
676
+ def trimming(self, mode=1, xmin=None, xmax=None, tmin=None, tmax=None):
677
+ """
678
+ Cut data to given start and end distance/channel or time/sampling
679
+ points.
680
+
681
+ :param mode: int. 0 means the unit of boundary is channel number and
682
+ sampling points; 1 means the unit of boundary is meters and seconds.
683
+ :param xmin, xmax: int or float. Boundary of channel number (mode=0)
684
+ or boundary of distance (mode=1).
685
+ in meters.
686
+ :param tmin, tmax: int, float or DASDateTime. Boundary of sampling
687
+ points (mode=0) or boundary of time (mode=1). When mode=1,
688
+ start_time is of type DASDateTime and tmin/tmax are floats,
689
+ tmin/tmax means the time relative to start_time.
690
+ """
691
+ if mode == 1:
692
+ if tmin is not None:
693
+ try:
694
+ tmin = round((tmin - self.start_time) * self.fs)
695
+ except TypeError:
696
+ tmin = round(tmin * self.fs)
697
+ if tmin < 0:
698
+ warnings.warn('tmin is earlier than start_time. Set tmin '
699
+ 'to start_time.')
700
+ tmin = 0
701
+ elif tmin >= self.nt:
702
+ raise ValueError('tmin is later than end_time.')
703
+ else:
704
+ tmin = 0
705
+
706
+ if tmax is not None:
707
+ try:
708
+ tmax = round((tmax - self.start_time) * self.fs)
709
+ except TypeError:
710
+ tmax = round(tmax * self.fs)
711
+ if tmax <= 0:
712
+ raise ValueError('tmax is earlier than start_time.')
713
+ if tmax > self.nt:
714
+ warnings.warn('tmax is later than end_time. Set tmax to the'
715
+ ' end_time.')
716
+ tmax = self.nt
717
+
718
+ if xmin is not None:
719
+ xmin = round((xmin - self.start_distance) / self.dx)
720
+ if xmin < 0:
721
+ warnings.warn('xmin is smaller than start_distance. Set '
722
+ 'xmin to 0.')
723
+ xmin = 0
724
+ elif xmin >= self.nch:
725
+ raise ValueError('xmin is later than end_distance.')
726
+ else:
727
+ xmin = 0
728
+
729
+ if xmax is not None:
730
+ xmax = round((xmax - self.start_distance) / self.dx)
731
+ if xmax <= 0:
732
+ raise ValueError('xmax is smaller than start_distance.')
733
+ if xmax > self.nch:
734
+ warnings.warn('xmax is later than end_distance. Set xmax '
735
+ 'to the array length.')
736
+ xmax = self.nch
737
+
738
+ elif mode == 0:
739
+ if tmin is None:
740
+ tmin = 0
741
+ else:
742
+ tmin = int(tmin)
743
+ if xmin is None:
744
+ xmin = 0
745
+ else:
746
+ xmin = int(xmin - self.start_channel)
747
+ if xmax is not None:
748
+ xmax = int(xmax - self.start_channel)
749
+
750
+ self.data = trimming(self.data, dx=self.dx, fs=self.fs, xmin=xmin,
751
+ xmax=xmax, tmin=tmin, tmax=tmax)
752
+
753
+ self.start_time += tmin / self.fs
754
+ self.start_distance += xmin * self.dx
755
+ self.start_channel += xmin
756
+
757
+ return self
758
+
759
+ def padding(self, dn, reverse=False):
760
+ """
761
+ Pad DAS data with 0.
762
+
763
+ :param dn: int or sequence of ints. Number of points to pad for both
764
+ dimensions.
765
+ :param reverse: bool. Set True to reverse the operation.
766
+ """
767
+ self.data = padding(self.data, dn, reverse=reverse)
768
+ return self
769
+
770
+ def _time_int_dif_attr(self, mode=0):
771
+ for type_group in [['phase change rate', 'phase shift'],
772
+ ['strain rate', 'strain'],
773
+ ['acceleration', 'velocity', 'displacement']]:
774
+ for (i, tp) in enumerate(type_group):
775
+ if tp in self.data_type:
776
+ try:
777
+ self.data_type = self.data_type.replace(
778
+ tp, type_group[i + mode])
779
+ except BaseException:
780
+ operate = ('differentiate', 'integrate')[mode > 0]
781
+ print(f'Data type conversion error. Can not {operate} '
782
+ f'{self.data_type} data.')
783
+ return self
784
+ warnings.warn('Unable to convert data type.')
785
+
786
+ def time_integration(self, c=0):
787
+ """
788
+ Integrate DAS data in time.
789
+
790
+ :param c: float. A constant added to the result.
791
+ """
792
+ self.data = time_integration(self.data, self.fs, c=c)
793
+ if hasattr(self, 'data_type'):
794
+ self._time_int_dif_attr(mode=1)
795
+ return self
796
+
797
+ def time_differential(self, prepend=0):
798
+ """
799
+ Differentiate DAS data in time.
800
+
801
+ :param prepend: 'mean' or values to prepend to `data` along axis prior to
802
+ performing the difference.
803
+ """
804
+ self.data = time_differential(self.data, self.fs, prepend=prepend)
805
+ if hasattr(self, 'data_type'):
806
+ self._time_int_dif_attr(mode=-1)
807
+ return self
808
+
809
+ def distance_integration(self, c=0):
810
+ """
811
+ Differentiate DAS data in distance.
812
+
813
+ :param c: float. A constant added to the result.
814
+ """
815
+ self.data = distance_integration(self.data, self.dx, c=c)
816
+ self._strain2vel_attr()
817
+ return self
818
+
819
+ def bandpass(self, freqmin, freqmax, zi=None, **kwargs):
820
+ """
821
+ Filter data from 'freqmin' to 'freqmax' using Butterworth bandpass
822
+ filter of 'corners' corners.
823
+
824
+ :param freqmin: Pass band low corner frequency.
825
+ :param freqmax: Pass band high corner frequency.
826
+ :param zi : None, 0, or array_like. Initial conditions for the cascaded
827
+ filter delays. It is a vector of shape (n_sections, nch, 2). Set to
828
+ 0 to trigger a output of the final filter delay values.
829
+ :param corners: Filter corners / order.
830
+ :param zerophase: If True, apply filter once forwards and once
831
+ backwards. This results in twice the number of corners but zero
832
+ phase shift in the resulting filtered data. Only valid when zi is
833
+ None.
834
+ """
835
+ if zi is None:
836
+ self.data = bandpass(self.data, self.fs, freqmin, freqmax, **kwargs)
837
+ return self
838
+ else:
839
+ self.data, zf = bandpass(self.data, self.fs, freqmin, freqmax,
840
+ zi=zi, **kwargs)
841
+ return zf
842
+
843
+ def bandstop(self, freqmin, freqmax, zi=None, **kwargs):
844
+ """
845
+ Filter data removing data between frequencies 'freqmin' and 'freqmax'
846
+ using Butterworth bandstop filter of 'corners' corners.
847
+
848
+ :param freqmin: Stop band low corner frequency.
849
+ :param freqmax: Stop band high corner frequency.
850
+ :param zi : None, 0, or array_like. Initial conditions for the cascaded
851
+ filter delays. It is a vector of shape (n_sections, nch, 2). Set to
852
+ 0 to trigger a output of the final filter delay values.
853
+ :param corners: Filter corners / order.
854
+ :param zerophase: If True, apply filter once forwards and once
855
+ backwards. This results in twice the number of corners but zero
856
+ phase shift in the resulting filtered data. Only valid when zi is
857
+ None.
858
+ """
859
+
860
+ if zi is None:
861
+ self.data = bandstop(self.data, self.fs, freqmin, freqmax, **kwargs)
862
+ return self
863
+ else:
864
+ self.data, zf = bandstop(self.data, self.fs, freqmin, freqmax,
865
+ zi=zi, **kwargs)
866
+ return zf
867
+
868
+ def lowpass(self, freq, zi=None, **kwargs):
869
+ """
870
+ Filter data removing data over certain frequency 'freq' using
871
+ Butterworth lowpass filter of 'corners' corners.
872
+
873
+ :param freq: Filter corner frequency.
874
+ :param zi : None, 0, or array_like. Initial conditions for the cascaded
875
+ filter delays. It is a vector of shape (n_sections, nch, 2). Set to
876
+ 0 to trigger a output of the final filter delay values.
877
+ :param corners: Filter corners / order.
878
+ :param zerophase: If True, apply filter once forwards and once
879
+ backwards. This results in twice the number of corners but zero
880
+ phase shift in the resulting filtered data. Only valid when zi is
881
+ None.
882
+ """
883
+ if zi is None:
884
+ self.data = lowpass(self.data, self.fs, freq, **kwargs)
885
+ return self
886
+ else:
887
+ self.data, zf = lowpass(self.data, self.fs, freq, zi=zi, **kwargs)
888
+ return zf
889
+
890
+ def lowpass_cheby_2(self, freq, **kwargs):
891
+ """
892
+ Filter data by passing data only below a certain frequency. The main
893
+ purpose of this cheby2 filter is downsampling. This method will
894
+ iteratively design a filter, whose pass band frequency is determined
895
+ dynamically, such that the values above the stop band frequency are
896
+ lower than -96dB.
897
+
898
+ :param freq: The frequency above which signals are attenuated with 95
899
+ dB.
900
+ :param maxorder: Maximal order of the designed cheby2 filter.
901
+ :param zi : None, 0, or array_like. Initial conditions for the cascaded
902
+ filter delays. It is a vector of shape (n_sections, nch, 2). Set to 0 to
903
+ trigger a output of the final filter delay values.
904
+ :param ba: If True return only the filter coefficients (b, a) instead of
905
+ filtering.
906
+ :param freq_passband: If True return additionally to the filtered data,
907
+ the iteratively determined pass band frequency.
908
+ """
909
+ output = lowpass_cheby_2(self.data, self.fs, freq, **kwargs)
910
+ if isinstance(output, tuple):
911
+ self.data = output[0]
912
+ if len(output) == 2:
913
+ return output[1]
914
+ else:
915
+ return output[1:]
916
+ else:
917
+ if kwargs.pop('ba', False):
918
+ self.data = output
919
+ return self
920
+ else:
921
+ return output
922
+
923
+ def highpass(self, freq, zi=None, **kwargs):
924
+ """
925
+ Filter data removing data below certain frequency 'freq' using
926
+ Butterworth highpass filter of 'corners' corners.
927
+
928
+ :param freq: Filter corner frequency.
929
+ :param zi : None, 0, or array_like. Initial conditions for the cascaded
930
+ filter delays. It is a vector of shape (n_sections, nch, 2). Set to
931
+ 0 to trigger a output of the final filter delay values.
932
+ :param corners: Filter corners / order.
933
+ :param zerophase: If True, apply filter once forwards and once
934
+ backwards. This results in twice the number of corners but zero
935
+ phase shift in the resulting filtered data. Only valid when zi is
936
+ None.
937
+ """
938
+ if zi is None:
939
+ self.data = highpass(self.data, self.fs, freq, **kwargs)
940
+ return self
941
+ else:
942
+ self.data, zf = highpass(self.data, self.fs, freq, zi=zi, **kwargs)
943
+ return zf
944
+
945
+ def envelope(self):
946
+ """
947
+ Computes the envelope of the given data. The envelope is determined by
948
+ adding the squared amplitudes of the data and it's Hilbert-Transform and
949
+ then taking the square-root. The envelope at the start/end should not be
950
+ taken too seriously.
951
+ """
952
+ self.data = envelope(self.data)
953
+ if hasattr(self, 'data_type'):
954
+ self.data_type += ' envelope'
955
+ return self
956
+
957
+ def spectrum(self, taper=0.05, nfft='default'):
958
+ """
959
+ Computes the spectrum of the given data.
960
+
961
+ :param taper: Decimal percentage of Tukey taper.
962
+ :param nfft: Number of points for FFT. None = sampling points, 'default'
963
+ = next power of 2 of sampling points.
964
+ :return: Spectrum and frequency sequence.
965
+ """
966
+ return spectrum(self.data, self.fs, taper=taper, nfft=nfft)
967
+
968
+ def spectrogram(self, **kwargs):
969
+ """
970
+ Computes the spectrogram of the given data.
971
+
972
+ :param xmin, xmax: int. Start channel and end channel for calculating
973
+ the average spectrogram.
974
+ :param nperseg: int. Length of each segment.
975
+ :param noverlap: int. Number of points to overlap between segments. If
976
+ None, noverlap = nperseg // 2.
977
+ :param nfft: int. Length of the FFT used. None = nperseg.
978
+ :param detrend : str or bool. Specifies whether and how to detrend each
979
+ segment. 'linear' or 'detrend' or True = detrend, 'constant' or
980
+ 'demean' = demean.
981
+ :param boundary: str or None. Specifies whether the input signal is
982
+ extended at both ends, and how to generate the new values, in order
983
+ to center the first windowed segment on the first input point. This
984
+ has the benefit of enabling reconstruction of the first input point
985
+ when the employed window function starts at zero. Valid options are
986
+ ['even', 'odd', 'constant', 'zeros', None].
987
+ :return: Spectrogram, frequency sequence and time sequence.
988
+ """
989
+ if 'xmin' in kwargs.keys():
990
+ xmin = int(kwargs.pop('xmin') - self.start_channel)
991
+ else:
992
+ xmin = 0
993
+ if 'xmax' in kwargs.keys():
994
+ xmax = int(kwargs.pop('xmax') - self.start_channel)
995
+ else:
996
+ xmax = len(self.data)
997
+
998
+ return spectrogram(self.data[xmin:xmax], self.fs, **kwargs)
999
+
1000
+ def fk_transform(self, **kwargs):
1001
+ """
1002
+ Transform the data to the fk domain using 2-D Fourier transform method
1003
+
1004
+ :param taper: float or sequence of floats. Each float means decimal
1005
+ percentage of Tukey taper for corresponding dimension (ranging from
1006
+ 0 to 1). Default is 0.1 which tapers 5% from the beginning and 5%
1007
+ from the end.
1008
+ :param nfft: Number of points for FFT. None means sampling points;
1009
+ 'default' means next power of 2 of sampling points, which makes
1010
+ result smoother.
1011
+ """
1012
+ return fk_transform(self.data, self.dx, self.fs, **kwargs)
1013
+
1014
+ def channel_checking(self, use=False, **kwargs):
1015
+ """
1016
+ Use the energy of each channel to determine which channels are bad.
1017
+
1018
+ :param use: bool. If True, only keep the data of good channels in
1019
+ self.data and return self.
1020
+ :param deg: int. Degree of the fitting polynomial.
1021
+ :param thresh: int or float. The MAD multiple of bad channel energy
1022
+ lower than good channels.
1023
+ :param continuity: bool. Perform continuity checks on bad channels and
1024
+ good channels.
1025
+ :param adjacent: int. The number of nearby channels for continuity
1026
+ checks.
1027
+ :param toleration: int. The number of discontinuous channel allowed in
1028
+ each channel (including itself) in the continuity check.
1029
+ :param plot: bool or str. False means no plotting. Str or True means
1030
+ plotting while str gives a non-default filename.
1031
+ :return: self or Good channels and bad channels.
1032
+ """
1033
+ good_chn, bad_chn = channel_checking(self.data, **kwargs)
1034
+ if use:
1035
+ self.channel_data(good_chn, replace=True)
1036
+ return self
1037
+ else:
1038
+ return good_chn, bad_chn
1039
+
1040
+ def turning_points(self, data_type='default', **kwargs):
1041
+ """
1042
+ Seek turning points in the DAS channel.
1043
+
1044
+ :param data_type: str. If data_type is 'coordinate', data should include
1045
+ latitude and longitude (first two columns), and can also include
1046
+ depth (last column). If data_type is 'waveform', data should be
1047
+ continuous waveform, preferably containing signal with strong
1048
+ coherence (earthquake, traffic signal, etc.).
1049
+ :param thresh: For coordinate data, when the angle of the optical cables
1050
+ on both sides centered on a certain point exceeds thresh, it is
1051
+ considered an turning point. For waveform, thresh means the MAD
1052
+ multiple of adjacent channel cross-correlation values lower than
1053
+ their median.
1054
+ :param depth_info: bool. Optional if data_type is 'coordinate'. Whether
1055
+ depth (in meters) is included in the coordinate data and need to be
1056
+ used.
1057
+ :param channel_gap: int. Optional if data_type is 'coordinate'. The
1058
+ smaller the value is, the finer the segmentation will be. It is
1059
+ set to half the ratio of gauge length and channel interval by
1060
+ default.
1061
+ :return: list. Channel index of turning points.
1062
+ """
1063
+ if data_type == 'default':
1064
+ data_type = 'coordinate' if hasattr(self, 'geometry') else \
1065
+ 'waveform'
1066
+ if data_type == 'coordinate':
1067
+ if hasattr(self, 'gauge_length'):
1068
+ kwargs.setdefault(
1069
+ 'channel_gap', self.gauge_length / self.dx / 2)
1070
+ if 'data' in kwargs.keys():
1071
+ output = turning_points(data_type=data_type, **kwargs)
1072
+ elif hasattr(self, 'geometry'):
1073
+ output = turning_points(self.geometry, data_type=data_type,
1074
+ **kwargs)
1075
+ else:
1076
+ raise ValueError('Geometry needs to be defined in DASdata, or '
1077
+ 'coordinate data should be given.')
1078
+ else:
1079
+ output = turning_points(self.data, data_type=data_type, **kwargs)
1080
+
1081
+ if isinstance(output, tuple):
1082
+ output = np.array(list(set(output[0]) | set(output[1])))
1083
+ else:
1084
+ output = np.array(output)
1085
+ output += self.start_channel
1086
+ self.turning_channels = output
1087
+ return output
1088
+
1089
+ def spike_removal(self, nch=50, nsp=5, thresh=10):
1090
+ """
1091
+ Use a median filter to remove high-strain spikes in the data.
1092
+
1093
+ :param nch: int. Number of channels over which to compute the median.
1094
+ :param nsp: int. Number of sampling points over which to compute the
1095
+ median.
1096
+ :param thresh: Ratio threshold over the median over which a number is
1097
+ considered to be an outlier.
1098
+ """
1099
+ self.data = spike_removal(self.data, nch=nch, nsp=nsp, thresh=thresh)
1100
+ return self
1101
+
1102
+ def common_mode_noise_removal(self):
1103
+ """
1104
+ Remove common mode noise (sometimes called horizontal noise) from data.
1105
+ """
1106
+ self.data = common_mode_noise_removal(self.data)
1107
+ return self
1108
+
1109
+ def curvelet_denoising(self, **kwargs):
1110
+ """
1111
+ Use curevelet transform to filter stochastic or/and cooherent noise.
1112
+
1113
+ :param choice: int. 0 for Gaussian denoising using soft thresholding, 1
1114
+ for velocity filtering using the standard FK methodology and 2 for
1115
+ both.
1116
+ :param pad: float or sequence of floats. Each float means padding
1117
+ percentage before FFT for corresponding dimension. If set to 0.1
1118
+ will pad 5% before the beginning and after the end.
1119
+ :param noise: numpy.ndarray. Noise record as reference.
1120
+ :param soft_thresh: bool. True for soft thresholding and False for hard
1121
+ thresholding.
1122
+ :param vmin, vmax: float. Velocity range in m/s.
1123
+ :param flag: -1 choose only negative apparent velocities, 0 choose both
1124
+ postive and negative apparent velocities, 1 choose only positive
1125
+ apparent velocities.
1126
+ :param mode: str. 'remove' for denoising and 'retain' for decomposition.
1127
+ :param scale_begin: int. The beginning scale to do coherent denoising.
1128
+ :param nbscales: int. Number of scales including the coarsest wavelet
1129
+ level. Default set to ceil(log2(min(M,N)) - 3).
1130
+ :param nbangles: int. Number of angles at the 2nd coarsest level,
1131
+ minimum 8, must be a multiple of 4.
1132
+ """
1133
+ self.data = curvelet_denoising(self.data, dx=self.dx, fs=self.fs,
1134
+ **kwargs)
1135
+ return self
1136
+
1137
+ def fk_filter(self, mode='retain', verbose=False, **kwargs):
1138
+ """
1139
+ Transform the data to the f-k domain using 2-D Fourier transform method,
1140
+ and transform back to the x-t domain after filtering.
1141
+
1142
+ :param mode: str. 'remove' for denoising, 'retain' for extraction, and
1143
+ 'decompose' for decomposition and not update self.data.
1144
+ :param verbose: If True, return filtered data, f-k spectrum,
1145
+ frequency sequence, wavenumber sequence and f-k mask.
1146
+ :param taper: float or sequence of floats. Each float means decimal
1147
+ percentage of Tukey taper for corresponding dimension (ranging from
1148
+ 0 to 1). Default is 0.1 which tapers 5% from the beginning and 5%
1149
+ from the end.
1150
+ :param pad: Pad the data or not. It can be float or sequence of floats.
1151
+ Each float means padding percentage before FFT for corresponding
1152
+ dimension. If set to 0.1 will pad 5% before the beginning and after
1153
+ the end. 'default' means pad both dimensions to next power of 2.
1154
+ None or False means don't pad data before or during Fast Fourier
1155
+ Transform.
1156
+ :param fmin, fmax, kmin, kmax, vmin, vmax: float or or sequence of 2
1157
+ floats. Sequence of 2 floats represents the start and end of taper.
1158
+ :param edge: float. The width of fan mask taper edge.
1159
+ :param flag: -1 keep only negative apparent velocities, 0 keep both
1160
+ postive and negative apparent velocities, 1 keep only positive
1161
+ apparent velocities.
1162
+ """
1163
+ output = fk_filter(self.data, self.dx, self.fs, mode=mode,
1164
+ verbose=verbose, **kwargs)
1165
+ if mode == 'decompose':
1166
+ sec1 = self.copy()
1167
+ sec2 = self.copy()
1168
+ sec1.data, sec2.data = output[:2]
1169
+ if verbose:
1170
+ return sec1, sec2, *output[2:]
1171
+ else:
1172
+ return sec1, sec2
1173
+ elif verbose:
1174
+ self.data = output[0]
1175
+ return output
1176
+ else:
1177
+ self.data = output
1178
+ return self
1179
+
1180
+ def curvelet_windowing(self, mode='retain', **kwargs):
1181
+ """
1182
+ Use curevelet transform to keep cooherent signal with certain velocity
1183
+ range.
1184
+
1185
+ :param mode: str. 'remove' for denoising, 'retain' for extraction, and
1186
+ 'decompose' for decomposition and not update self.data.
1187
+ :param vmin, vmax: float. Velocity range in m/s.
1188
+ :param flag: -1 keep only negative apparent velocities, 0 keep both
1189
+ postive and negative apparent velocities, 1 keep only positive
1190
+ apparent velocities.
1191
+ :param pad: float or sequence of floats. Each float means padding
1192
+ percentage before FFT for corresponding dimension. If set to 0.1
1193
+ will pad 5% before the beginning and after the end.
1194
+ :param scale_begin: int. The beginning scale to do coherent denoising.
1195
+ :param nbscales: int. Number of scales including the coarsest wavelet
1196
+ level. Default set to ceil(log2(min(M,N)) - 3).
1197
+ :param nbangles: int. Number of angles at the 2nd coarsest level,
1198
+ minimum 8, must be a multiple of 4.
1199
+ """
1200
+ output = curvelet_windowing(self.data, self.dx, self.fs, mode=mode,
1201
+ **kwargs)
1202
+ if mode == 'decompose':
1203
+ sec1 = self.copy()
1204
+ sec2 = self.copy()
1205
+ sec1.data, sec2.data = output
1206
+ return sec1, sec2
1207
+ else:
1208
+ self.data = output
1209
+ return self
1210
+
1211
+ def _strain2vel_attr(self):
1212
+ if hasattr(self, 'data_type'):
1213
+ if 'strain rate' in self.data_type:
1214
+ self.data_type = 'acceleration'
1215
+ elif 'strain' in self.data_type:
1216
+ self.data_type = 'velocity'
1217
+ else:
1218
+ warnings.warn(f'The data type is {self.data_type}, neither '
1219
+ 'strain nor strain rate. But it still takes '
1220
+ 'effect.')
1221
+ else:
1222
+ self.data_type = 'velocity or acceleration'
1223
+ return self
1224
+
1225
+ def fk_rescaling(self, turning=None, verbose=False, **kwargs):
1226
+ """
1227
+ Convert strain / strain rate to velocity / acceleration by fk rescaling.
1228
+
1229
+ :param turning: Sequence of int. Channel number of turning points. If
1230
+ self.turning exists, it will be used by default unless the parameter
1231
+ turning is set to False.
1232
+ :param verbose: If True and turning is not set, return f-k spectrum,
1233
+ frequency sequence, wavenumber sequence and f-k mask.
1234
+ :param taper: float or sequence of floats. Each float means decimal
1235
+ percentage of Tukey taper for corresponding dimension (ranging from
1236
+ 0 to 1). Default is 0.1 which tapers 5% from the beginning and 5%
1237
+ from the end. If the turning parameter is set, this parameter will
1238
+ be invalid.
1239
+ :param pad: Pad the data or not. It can be float or sequence of floats.
1240
+ Each float means padding percentage before FFT for corresponding
1241
+ dimension. If set to 0.1 will pad 5% before the beginning and after
1242
+ the end. 'default' means pad both dimensions to next power of 2.
1243
+ None or False means don't pad data before or during Fast Fourier
1244
+ Transform.
1245
+ :param fmax, kmin, vmax: float or or sequence of 2 floats. Sequence of 2
1246
+ floats represents the start and end of taper. Setting these
1247
+ parameters can reduce artifacts.
1248
+ :param edge: float. The width of fan mask taper edge.
1249
+ """
1250
+ if hasattr(self, 'turning_channels') and turning is None:
1251
+ turning = np.array(self.turning_channels) - self.start_channel
1252
+
1253
+ self._strain2vel_attr()
1254
+ if verbose and not turning:
1255
+ data_res, fk, f, k, mask = fk_rescaling(self.data, self.dx, self.fs,
1256
+ verbose=True, **kwargs)
1257
+ self.data = data_res
1258
+ return fk, f, k, mask
1259
+ else:
1260
+ self.data = fk_rescaling(self.data, self.dx, self.fs, **kwargs)
1261
+ return self
1262
+
1263
+ def curvelet_conversion(self, turning=None, **kwargs):
1264
+ """
1265
+ Use curevelet transform to convert strain/strain rate to
1266
+ velocity/acceleration.
1267
+
1268
+ :param turning: Sequence of int. Channel number of turning points. If
1269
+ self.turning exists, it will be used by default unless the parameter
1270
+ turning is set to False.
1271
+ :param pad: float or sequence of floats. Each float means padding
1272
+ percentage before FFT for corresponding dimension. If set to 0.1
1273
+ will pad 5% before the beginning and after the end.
1274
+ :param scale_begin: int. The beginning scale to do conversion.
1275
+ :param nbscales: int. Number of scales including the coarsest wavelet
1276
+ level. Default set to ceil(log2(min(M,N)) - 3).
1277
+ :param nbangles: int. Number of angles at the 2nd coarsest level,
1278
+ minimum 8, must be a multiple of 4.
1279
+ """
1280
+ if hasattr(self, 'turning_channels') and turning is None:
1281
+ turning = np.array(self.turning_channels) - self.start_channel
1282
+
1283
+ self.data = curvelet_conversion(self.data, self.dx, self.fs,
1284
+ turning=turning, **kwargs)
1285
+ self._strain2vel_attr()
1286
+ return self
1287
+
1288
+ def slant_stacking(self, channel='all', turning=None, **kwargs):
1289
+ """
1290
+ Convert strain to velocity based on slant-stack.
1291
+
1292
+ :param channel: int or list or 'all'. convert a certain channel number /
1293
+ certain channel range / all channels.
1294
+ :param turning: Sequence of int. Channel number of turning points. If
1295
+ self.turning exists, it will be used by default unless the parameter
1296
+ turning is set to False.
1297
+ :param L: int. the number of adjacent channels over which slowness is
1298
+ estimated.
1299
+ :param slm: float. Slowness x max
1300
+ :param sls: float. slowness step
1301
+ :param freqmin: Pass band low corner frequency.
1302
+ :param freqmax: Pass band high corner frequency.
1303
+ """
1304
+ if hasattr(self, 'turning_channels') and turning is None:
1305
+ turning = np.array(self.turning_channels) - self.start_channel
1306
+
1307
+ if isinstance(channel, int):
1308
+ channel = [channel - self.start_channel]
1309
+ elif isinstance(channel, Iterable):
1310
+ channel = np.array(channel) - self.start_channel
1311
+ elif isinstance(channel, str) and channel == 'all':
1312
+ channel = list(range(self.nch))
1313
+
1314
+ self.start_channel += channel[0]
1315
+ self.start_distance += channel[0] * self.dx
1316
+ self.data = slant_stacking(self.data, self.dx, self.fs, channel=channel,
1317
+ turning=turning, **kwargs)
1318
+ self._strain2vel_attr()
1319
+ return self