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
@@ -0,0 +1 @@
1
+ daspy/LICENSE.txt
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.1
2
+ Name: DASPy-toolbox
3
+ Version: 1.0.0
4
+ Summary: DASPy is an open-source project dedicated to provide a python package for DAS (Distributed Acoustic Sensing) data processing, which comprises classic seismic data processing techniques and Specialized algorithms for DAS applications.
5
+ Home-page: https://github.com/HMZ-03/DASPy
6
+ Author: Minzhe Hu, Zefeng Li
7
+ Author-email: hmz2018@mail.ustc.edu.cn
8
+ Maintainer: Minzhe Hu
9
+ Maintainer-email: hmz2018@mail.ustc.edu.cn
10
+ License: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.9
15
+ License-File: LICENSE.txt
16
+ Requires-Dist: numpy
17
+ Requires-Dist: scipy>=1.13
18
+ Requires-Dist: matplotlib
19
+ Requires-Dist: geographiclib
20
+ Requires-Dist: pyproj
21
+ Requires-Dist: h5py
22
+ Requires-Dist: segyio
23
+ Requires-Dist: nptdms
24
+ Requires-Dist: tqdm
25
+
26
+ <img src="./website/USTC.svg" height="170" />&emsp;<img src="./website/DAMS.png" height="150" />
27
+
28
+
29
+ ## DASPy
30
+
31
+ DASPy is an open-source project dedicated to provide a python package for DAS (Distributed Acoustic Sensing) data processing.
32
+
33
+ The goal of the DASPy project is to lower the bar of DAS data processing. DASPy includes:
34
+ * Classic seismic data processing techniques, including preprocessing, filter, spectrum analysis, and visualization
35
+ * Specialized algorithms for DAS applications, including denoising, waveform decomposition, channel attribute analysis, and strain-velocity conversion.
36
+
37
+ DASPy is licensed under the MIT License. [An English version of DASPy tutorial](https://daspy-tutorial.readthedocs.io/en/latest/), [a Chinese version of DASPy tutorial](https://daspy-tutorial-cn.readthedocs.io/zh-cn/latest/) and [the DASPy paper](document/srl-2024124.1.pdf) is available. If you have any questions, please contact me via <hmz2018@mail.ustc.edu.cn>.
38
+
39
+ ## Installation
40
+ DASPy is currently running on Linux, Windows and Mac OS.
41
+ DASPy runs on Python 3.9 and up. We recommend you use the latest version of python 3 if possible.
42
+
43
+ ### Pip (recommanded)
44
+ ```
45
+ pip install git+https://github.com/HMZ-03/DASPy.git
46
+ ```
47
+
48
+ If you installed DASPy this way, you can upgrade DASPy with the following command:
49
+
50
+ ```
51
+ pip install --upgrade git+https://github.com/HMZ-03/DASPy.git
52
+ ```
53
+
54
+ ### Conda
55
+ ```
56
+ conda install -c hmz-03 daspy
57
+ ```
58
+
59
+ If an error is reported, please try updating conda:
60
+
61
+ ```
62
+ conda update -n base -c conda-forge conda
63
+ ```
64
+
65
+ ### Manual installation
66
+ 1. Install dependent packages: numpy, scipy >=1.13, matplotlib, geographiclib, pyproj, h5py, segyio, nptdms, tqdm
67
+
68
+ 2. Add DASPy into your Python path.
69
+
70
+ ## Getting started
71
+ ```
72
+ from daspy import read
73
+ sec = read() # load example waveform
74
+ sec.bandpass(1, 15)
75
+ sec.plot()
76
+ ```
77
+ <img src="./website/waveform.png" height="500" />
78
+
79
+ ### Contributing
80
+
81
+ Please see details on how to contribute to the project [here](CONTRIBUTING.md) and [here](CodingStyleGuide.md).
82
+
83
+ ### Reference
84
+
85
+ * Minzhe Hu and Zefeng Li (2024), [DASPy: A Python Toolbox for DAS Seismology](https://pubs.geoscienceworld.org/ssa/srl/article/95/5/3055/645865/DASPy-A-Python-Toolbox-for-DAS-Seismology), *Seismological Research Letters*, 95(5), 3055–3066, doi: `https://doi.org/10.1785/0220240124`.
@@ -0,0 +1,49 @@
1
+ daspy/__init__.py,sha256=9ePx1cKAX5SdVk9lCSeu4AjhhzimfIbbwdoeVcosUok,178
2
+ daspy/advanced_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ daspy/advanced_tools/channel.py,sha256=Ymw7B_ZN185_kVhZDFvQHXwycOlvyxQbRB7QOV6qzRU,13903
4
+ daspy/advanced_tools/decomposition.py,sha256=jAth4_xKGaFpemzpci25D4XyZUx0Nl7eGMATBouKmiA,7252
5
+ daspy/advanced_tools/denoising.py,sha256=qpQnuv2JUB7b6hI8MvUHAy0NqFCC0-00uYxd1xhnz2Q,10790
6
+ daspy/advanced_tools/fdct.py,sha256=sCZtBdSFdwcsPsjIPUmHNqUfEUK6GFEp7qVQyVRhzkc,37047
7
+ daspy/advanced_tools/strain2vel.py,sha256=UJpDTqoi8frtBtuxqTbQoiOd9XSHVvNuJ1Gx3eK2Uhg,9936
8
+ daspy/basic_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ daspy/basic_tools/filter.py,sha256=OqfAvSTZWMcqf4gve3-WvKpN_tsmjsz9azBacGpIzvI,10192
10
+ daspy/basic_tools/freqattributes.py,sha256=6Tb_xZ45NU0Kh-eRgzOWPA69oLjDXL7GUbMoKkr1BX4,4397
11
+ daspy/basic_tools/preprocessing.py,sha256=z-YpFWQxtBFrCKqWpxKzaZete0phb3fXzMemQ9SL68E,7175
12
+ daspy/basic_tools/visualization.py,sha256=UCRdCy5MWsBKcg0ELuDPqahCa_vjDWp1rugE-BUgRhY,7568
13
+ daspy/core/__init__.py,sha256=9ePx1cKAX5SdVk9lCSeu4AjhhzimfIbbwdoeVcosUok,178
14
+ daspy/core/collection.py,sha256=kzTR7rQtS15ShfLTsJXh9Jt4bIPJA_nsUG7apxTrJfU,11449
15
+ daspy/core/dasdatetime.py,sha256=ASkPjx5mF0VL4GTrCbKm08NaZsSD_2zn4sHad3yRLaQ,2097
16
+ daspy/core/example.pkl,sha256=cgna7XFux5gSO93y_9GSVUNJa2PKABxD68a_HenzoHc,20000481
17
+ daspy/core/make_example.py,sha256=pfUTZXTzHUIV2bvB197uyQNILXfv6eIACSw3Omt__9Q,994
18
+ daspy/core/read.py,sha256=716YcfMPMaxX4FtkOXNrxjIemup2IiMRkeufl56LXl8,23499
19
+ daspy/core/section.py,sha256=d5Q5x6iwxpOqKFfoi1OgCNd7T1rVZWh_JTzj7em7is8,57401
20
+ daspy/core/write.py,sha256=yZwsuQSJZutBk4gjuCJTm4nAph3VZtTJUYml1lVXHQM,12581
21
+ daspy/seismic_detection/__init__.py,sha256=CwmJ6t6tUu19F15MUAa3A0m-gdevAsbK-XDtlYk_4sw,54
22
+ daspy/seismic_detection/calc_travel_time.py,sha256=DnIQvqACGu35PNwDOeZ1e_aDOi4kIRPbuYU93k-X3nI,716
23
+ daspy/seismic_detection/core.py,sha256=SEReXzTDw3lZkz_IYoBKe8WMRJnOeBM5FxZxRfCWC7c,4817
24
+ daspy/seismic_detection/detection.py,sha256=QNOMIaw_qsxnGZxWEYYUXMEK_NJUSubpiRQEeQK3Pl8,180
25
+ daspy/seismic_detection/location.py,sha256=RVHym2hYOCiohKv6BpJKGYYLGX4j3m5XCcO9gtSIhPU,10375
26
+ daspy/seismic_detection/magnitude.py,sha256=G2FpTtVrxzQ_yaiycMut8R1Cq9gErES4QmWtP9Mzv3c,1181
27
+ daspy/seismic_detection/phase_picking.py,sha256=ggtIT4DXE0S1wHecitNpsnN2xXuUnHXp7X_paxUqy4U,1897
28
+ daspy/seismic_detection/gamma/__init__.py,sha256=fBgNjzcOqyAnA3-oioCzgRxOlKPq_q_QlBl3gmcB2Xw,330
29
+ daspy/seismic_detection/gamma/_base.py,sha256=0MJNNBwImK9AhMhqrABRuHmn1UTZn0mSNJcWNMvgLc4,19235
30
+ daspy/seismic_detection/gamma/_bayesian_mixture.py,sha256=6Fm8d1kYAUj_ZDYGMOQwN_Uvl5-HEniHbQRRE7-nz0w,37508
31
+ daspy/seismic_detection/gamma/_gaussian_mixture.py,sha256=OoeHH6P9Fe0ggaq4X7mUIRThz4dU7RndhaAKB-QYLjw,33646
32
+ daspy/seismic_detection/gamma/app.py,sha256=6udxd6zjU_WOpz6-wQB6zLGR5ExNwkONdSee8dToh60,7104
33
+ daspy/seismic_detection/gamma/seismic_ops.py,sha256=uhqozSdv9zmq6PwMRnEQqOqVXRIroQh7xOQfpZOJaWo,16106
34
+ daspy/seismic_detection/gamma/utils.py,sha256=N4bgksgEMQFIs99VPYKQEJIHpSwsrmqcR-l-LaqIans,20208
35
+ daspy/structure_imaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ daspy/structure_imaging/ambient_noise.py,sha256=S245_h3JeNVN7WyQLnL3ECXPVGZJXfRo28tBO3RcRww,38
37
+ daspy/structure_imaging/dispersion.py,sha256=G7ELTg1tbcfRHIuLzaFiPW0bYQ64qvy-U7pY4R8VhVI,827
38
+ daspy/structure_imaging/fault_zone.py,sha256=yWa-1zAq-Ay0Y8FQGQ_nd-Nf1r-MfWMEuuXwHOY7fm4,1872
39
+ daspy/structure_imaging/inversion.py,sha256=gmaNdqOfLcFiMhZQolXE6M9yJ6jzi_SKKN1KjDX31bA,48
40
+ daspy/traffic_monitoring/JamDetection.py,sha256=gmaNdqOfLcFiMhZQolXE6M9yJ6jzi_SKKN1KjDX31bA,48
41
+ daspy/traffic_monitoring/SpeedMeasurement.py,sha256=gmaNdqOfLcFiMhZQolXE6M9yJ6jzi_SKKN1KjDX31bA,48
42
+ daspy/traffic_monitoring/VehicleDetection.py,sha256=gmaNdqOfLcFiMhZQolXE6M9yJ6jzi_SKKN1KjDX31bA,48
43
+ daspy/traffic_monitoring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ DASPy_toolbox-1.0.0.dist-info/LICENSE.txt,sha256=JQFMMufl9iR-zv6KUSm1HRskwVGj1rLUYaJt_xSYdHU,18
45
+ DASPy_toolbox-1.0.0.dist-info/METADATA,sha256=r62scxU1N8OeNK6dwBUAVcS5wG80bri0JiMBtAo4McA,3208
46
+ DASPy_toolbox-1.0.0.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
47
+ DASPy_toolbox-1.0.0.dist-info/entry_points.txt,sha256=h-kZi48_NB9-2Y985HJ_ZTy343UC8msj-sfqgs8dszs,42
48
+ DASPy_toolbox-1.0.0.dist-info/top_level.txt,sha256=GDX6JX11FYMlTT1iflQkX_daWBA7Vn83fOuOeMXqshM,6
49
+ DASPy_toolbox-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.44.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ daspy = daspy.main:main
@@ -0,0 +1 @@
1
+ daspy
daspy/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from daspy.core.section import Section
2
+ from daspy.core.collection import Collection
3
+ from daspy.core.read import read
4
+ from daspy.core.dasdatetime import DASDateTime, local_tz, utc
File without changes
@@ -0,0 +1,354 @@
1
+ # Purpose: Several functions for analysis data quality and geometry of channels
2
+ # Author: Minzhe Hu, Zefeng Li
3
+ # Date: 2024.10.11
4
+ # Email: hmz2018@mail.ustc.edu.cn
5
+ import numpy as np
6
+ from copy import deepcopy
7
+ from geographiclib.geodesic import Geodesic
8
+ from pyproj import Proj
9
+
10
+
11
+ def robust_polyfit(data, deg, thresh):
12
+ """
13
+ Fit a curve with a robust weighted polynomial.
14
+
15
+ :param data: 1-dimensional array.
16
+ :param deg: int. Degree of the fitting polynomial
17
+ :param thresh: int or float. Defined MAD multiple of outliers.
18
+ :return: Fitting data
19
+ """
20
+ nch = len(data)
21
+ channels = np.arange(nch)
22
+ p_coef = np.polyfit(channels, data, deg)
23
+ p_fit = np.poly1d(p_coef)
24
+ old_data = p_fit(channels)
25
+ mse = 1
26
+
27
+ # robust fitting until the fitting curve changes < 0.1% at every point.
28
+ while mse > 0.001:
29
+ rsl = abs(data - old_data)
30
+ mad = np.median(rsl)
31
+ weights = np.zeros(nch)
32
+ weights[rsl < thresh * mad] = 1
33
+ p_coef = np.polyfit(channels, data, deg, w=weights)
34
+ p_fit = np.poly1d(p_coef)
35
+ new_data = p_fit(channels)
36
+ mse = np.nanmax(np.abs((new_data - old_data) / old_data))
37
+ old_data = new_data
38
+
39
+ return new_data, weights
40
+
41
+
42
+ def _continuity_checking(lst1, lst2, adjacent=2, toleration=2):
43
+ lst1_raw = deepcopy(lst1)
44
+ for chn in lst1_raw:
45
+ discont = [a for a in lst2 if abs(a - chn) <= adjacent]
46
+ if len(discont) >= adjacent * 2 + 1 - toleration:
47
+ lst1.remove(chn)
48
+ lst2.append(chn)
49
+
50
+ return lst1, lst2
51
+
52
+
53
+ def channel_checking(data, deg=10, thresh=5, continuity=True, adjacent=2,
54
+ toleration=2, mode='low', verbose=False):
55
+ """
56
+ Use the energy of each channel to determine which channels are bad.
57
+
58
+ :param data: 2-dimensional np.ndarray. Axis 0 is channel number and axis 1 is
59
+ time series
60
+ :param deg: int. Degree of the fitting polynomial
61
+ :param thresh: int or float. The MAD multiple of bad channel energy lower
62
+ than good channels.
63
+ :param continuity: bool. Perform continuity checks on bad channels and good
64
+ channels.
65
+ :param adjacent: int. The number of nearby channels for continuity checks.
66
+ :param toleration: int. The number of discontinuous channel allowed in each
67
+ channel (including itself) in the continuity check.
68
+ :param mode: str. 'low' means bad channels have low amplitude, 'high' means
69
+ bad channels have high amplitude, and 'both' means bad channels are
70
+ likely to have low or high amplitude.
71
+ :return: Good channels and bad channels.
72
+ """
73
+ nch = len(data)
74
+ energy = np.log10(np.sum(data**2, axis=1))
75
+
76
+ # Remove abnormal value by robust polynomial fitting.
77
+ fitted_energy, weights = robust_polyfit(energy, deg, thresh)
78
+ deviation = energy - fitted_energy
79
+
80
+ # Iterate eliminates outliers.
81
+ mad = np.median(abs(deviation[weights > 0]))
82
+ if mode == 'low':
83
+ bad_chn = np.argwhere(deviation < -thresh * mad).ravel().tolist()
84
+ elif mode == 'high':
85
+ bad_chn = np.argwhere(deviation > thresh * mad).ravel().tolist()
86
+ elif mode == 'high':
87
+ bad_chn = np.argwhere(deviation < -thresh * mad).ravel().tolist() + \
88
+ np.argwhere(deviation > thresh * mad).ravel().tolist()
89
+ good_chn = list(set(range(nch)) - set(bad_chn))
90
+
91
+ if continuity:
92
+ # Discontinuous normal value are part of bad channels.
93
+ good_chn, bad_chn = _continuity_checking(good_chn, bad_chn,
94
+ adjacent=adjacent,
95
+ toleration=toleration)
96
+
97
+ # Discontinuous outliers are usually not bad channels.
98
+ bad_chn, good_chn = _continuity_checking(bad_chn, good_chn,
99
+ adjacent=adjacent,
100
+ toleration=toleration)
101
+
102
+ bad_chn = np.sort(np.array(bad_chn))
103
+ good_chn = np.sort(np.array(good_chn))
104
+ if verbose:
105
+ return good_chn, bad_chn, energy, fitted_energy - thresh * mad
106
+
107
+ return good_chn, bad_chn
108
+
109
+
110
+ def _channel_location(track_pt):
111
+ track, tn = track_pt[:, :-1], track_pt[:, -1]
112
+ dim = track.shape[1]
113
+ l_track = np.sqrt(np.sum(np.diff(track, axis=0) ** 2, axis=1))
114
+ l_track_cum = np.hstack(([0], np.cumsum(l_track)))
115
+ idx_kp = np.where(tn >= 0)[0]
116
+
117
+ interp_ch = []
118
+ chn = np.floor(tn[idx_kp[0]])
119
+ if abs(chn - tn[idx_kp[0]]) < 1e-6:
120
+ interp_ch.append([*track[idx_kp[0]], chn])
121
+
122
+ seg_interval = []
123
+ for i in range(1, len(idx_kp)):
124
+ # calculate actual interval between known-channel points
125
+ istart, iend = idx_kp[i - 1], idx_kp[i]
126
+ n_chn_kp = tn[iend] - tn[istart]
127
+ d_interp = (l_track_cum[iend] - l_track_cum[istart]) / n_chn_kp
128
+ seg_interval.append([tn[istart], tn[iend], d_interp])
129
+
130
+ l_res = 0 # remaining fiber length before counting the next segment
131
+ # consider if the given channelnumber is not an integer
132
+ chn_res = tn[istart] - int(tn[istart])
133
+ for j in range(istart, iend):
134
+ l_start = l_track[j] + l_res
135
+
136
+ # if tp segment length is large for more than one interval, get the
137
+ # channel loc
138
+ if l_start >= d_interp * (1 - chn_res):
139
+ # floor int, num of channel available
140
+ n_chn_tp = int(l_start / d_interp + chn_res)
141
+ l_new = (np.arange(n_chn_tp) + 1 - chn_res) * d_interp - \
142
+ l_res # channel distance from segment start
143
+
144
+ # interpolate the channel loc
145
+ t_new = np.zeros((len(l_new), dim))
146
+ for d in range(dim):
147
+ t_new[:, d] = np.interp(l_new, [0, l_track[j]],
148
+ [track[j, d], track[j + 1, d]])
149
+
150
+ # remaining length to add to next segment
151
+ l_res = l_start - n_chn_tp * d_interp
152
+
153
+ # write interpolated channel loc
154
+ for ti in t_new:
155
+ chn += 1
156
+ interp_ch.append([*ti, chn])
157
+
158
+ # handle floor int problem when l_start/d_interp is near an
159
+ # interger
160
+ if (d_interp - l_res) / d_interp < 1e-6:
161
+ chn += 1
162
+ interp_ch.append([*track[j + 1, :], int(tn[j + 1])])
163
+ l_res = 0
164
+ chn_res = 0
165
+ # if tp segment length is not enough for one interval, simply add
166
+ # the length to next segment
167
+ elif l_start < d_interp:
168
+ l_res = l_start
169
+
170
+ return np.array(seg_interval), np.array(interp_ch)
171
+
172
+
173
+ def location_interpolation(known_pt, track_pt=None, dx=2, data_type='lonlat',
174
+ verbose=False):
175
+ """
176
+ Interpolate to obtain the positions of all channels.
177
+
178
+ :param known_pt: np.ndarray. Points with known channel numbers. Each row
179
+ includes 2 or 3 coordinates and a channel number.
180
+ :param track_pt: np.ndarray. Optional fiber spatial track points without
181
+ channel numbers. Each row includes 2 or 3 coordinates. Please ensure
182
+ that the track points are arranged in increasing order of track number.
183
+ If track points is not dense enough, please insert the coordinates of
184
+ known points into track points in order.
185
+ :param dx: Known points far from the track (> dx) will be excluded.
186
+ Recommended setting is channel interval. The unit is m.
187
+ :param data_type: str. Coordinate type. 'lonlat' ('lonlatheight') for
188
+ longitude, latitude in degree (and height in meters), 'xy' ('xyz') for
189
+ x, y (and z) in meters.
190
+ :param verbose: bool. If True, return interpoleted channel location and
191
+ segment interval.
192
+ :return: Interpoleted channel location if verbose is False.
193
+ """
194
+ known_pt = known_pt[known_pt[:,-1].argsort()]
195
+ dim = known_pt.shape[1] - 1
196
+ if 'lonlat' in data_type:
197
+ zone = np.floor((max(known_pt[:,0]) + min(known_pt[:,0])) / 2 / 6)\
198
+ .astype(int) + 31
199
+ DASProj = Proj(proj='utm', zone=zone, ellps='WGS84',
200
+ preserve_units=False)
201
+ known_pt[:, 0], known_pt[:, 1] = DASProj(known_pt[:, 0], known_pt[:, 1])
202
+ else:
203
+ assert 'xy' in data_type, ('data_type should be \'lonlat\',\''
204
+ 'lonlatheight\', \'xy\' or \'xyz\'')
205
+
206
+ if track_pt is None:
207
+ seg_interval, interp_ch = _channel_location(known_pt)
208
+ else:
209
+ K = len(known_pt)
210
+ T = len(track_pt)
211
+ track_pt = np.c_[track_pt, np.zeros(T) - 1]
212
+ if 'lonlat' in data_type:
213
+ track_pt[:, 0], track_pt[:, 1] = DASProj(track_pt[:, 0],
214
+ track_pt[:, 1])
215
+
216
+ # insert the known points into the fiber track data
217
+ matrix = [np.tile(track_pt[:, d], (K, 1)) -
218
+ np.tile(known_pt[:, d], (T, 1)).T for d in range(dim)]
219
+
220
+ dist = np.sqrt(np.sum(np.array(matrix) ** 2, axis=0))
221
+ for k in range(K):
222
+ if min(dist[k]) < dx:
223
+ t_list = np.sort(np.where(dist[k] == min(dist[k]))[0])
224
+ for t in t_list:
225
+ if track_pt[t, -1] == -1:
226
+ track_pt[t, -1] = known_pt[k, -1]
227
+ last_pt = t
228
+ break
229
+
230
+ # interpolation with regular spacing along the fiber track
231
+ try:
232
+ track_pt = track_pt[:last_pt + 1]
233
+ except NameError:
234
+ print('All known points are too far away from the track points. If '
235
+ 'they are reliable, they can be merged in sequence as track '
236
+ 'points to input')
237
+ return None
238
+
239
+ seg_interval, interp_ch = _channel_location(track_pt)
240
+
241
+ if data_type == 'lonlat':
242
+ interp_ch[:, 0], interp_ch[:, 1] = \
243
+ DASProj(interp_ch[:, 0], interp_ch[:, 1], inverse=True)
244
+
245
+ if verbose:
246
+ return interp_ch, seg_interval
247
+ return interp_ch
248
+
249
+
250
+ def _xcorr(x, y):
251
+ N = len(x)
252
+ meanx = np.mean(x)
253
+ meany = np.mean(y)
254
+ stdx = np.std(np.asarray(x))
255
+ stdy = np.std(np.asarray(y))
256
+ c = np.sum((y - meany) * (x - meanx)) / (N * stdx * stdy)
257
+ return c
258
+
259
+
260
+ def _horizontal_angle_change(geo, gap=10):
261
+ nch = len(geo)
262
+ angle = np.zeros(nch)
263
+ for i in range(1, nch - 1):
264
+ lon, lat = geo[i]
265
+ lon_s, lat_s = geo[max(i - gap, 0)]
266
+ lon_e, lat_e = geo[min(i + gap, nch - 1)]
267
+ azi_s = Geodesic.WGS84.Inverse(lat_s, lon_s, lat, lon)['azi1']
268
+ azi_e = Geodesic.WGS84.Inverse(lat, lon, lat_e, lon_e)['azi1']
269
+ dazi = azi_e - azi_s
270
+ if abs(dazi) > 180:
271
+ dazi = -np.sign(dazi) * (360 - abs(dazi))
272
+ angle[i] = dazi
273
+
274
+ return angle
275
+
276
+
277
+ def _vertical_angle_change(geo, gap=10):
278
+ nch = len(geo)
279
+ angle = np.zeros(nch)
280
+ for i in range(1, nch - 1):
281
+ lon, lat, dep = geo[i]
282
+ lon_s, lat_s, dep_s = geo[max(i - gap, 0)]
283
+ lon_e, lat_e, dep_e = geo[min(i + gap, nch - 1)]
284
+ s12_s = Geodesic.WGS84.Inverse(lat_s, lon_s, lat, lon)['s12']
285
+ theta_s = np.arctan((dep - dep_s) / s12_s) / np.pi * 180
286
+ s12_e = Geodesic.WGS84.Inverse(lat, lon, lat_e, lon_e)['s12']
287
+ theta_e = np.arctan((dep_e - dep) / s12_e) / np.pi * 180
288
+ angle[i] = theta_e - theta_s
289
+
290
+ return angle
291
+
292
+
293
+ def _local_maximum_indexes(data, thresh):
294
+ idx = np.where(data > thresh)[0]
295
+ if len(idx):
296
+ i = list(np.where(np.diff(idx) > 1)[0] + 1)
297
+ if len(idx) - 1 not in i:
298
+ i.append(len(idx) - 1)
299
+ b = 0
300
+ max_idx = []
301
+ for e in i:
302
+ max_idx.append(idx[b] + np.argmax(data[idx[b]:idx[e]]))
303
+ b = e
304
+ return max_idx
305
+ else:
306
+ return []
307
+
308
+
309
+ def turning_points(data, data_type='coordinate', thresh=5, depth_info=False,
310
+ channel_gap=3):
311
+ """
312
+ Seek turning points in the DAS channel.
313
+
314
+ :param data: numpy.ndarray. Data used to seek turning points.
315
+ :param data_type: str. If data_type is 'coordinate', data should include
316
+ longitude and latitude (first two columns), and can also include depth
317
+ (last column). If data_type is 'waveform', data should be continuous
318
+ waveform, preferably containing signal with strong coherence
319
+ (earthquake, traffic signal, etc.).
320
+ :param thresh: For coordinate data, when the angle of the optical cables on
321
+ both sides centered on a certain point exceeds thresh, it is considered
322
+ an turning point. For waveform, thresh means the MAD multiple of
323
+ adjacent channel cross-correlation values lower than their median.
324
+ :param depth_info: bool. Optional if data_type is 'coordinate'. Whether
325
+ depth (in meters) is included in the coordinate data and need to be
326
+ used.
327
+ :param channel_gap: int. Optional if data_type is 'coordinate'. The smaller
328
+ the value is, the finer the segmentation will be. It is recommended to
329
+ set it to half the ratio of gauge length and channel interval.
330
+ :return: list. Channel index of turning points.
331
+ """
332
+ if data_type == 'coordinate':
333
+ angle = _horizontal_angle_change(data[:, :2], gap=channel_gap)
334
+ turning_h = _local_maximum_indexes(abs(angle), thresh)
335
+
336
+ if depth_info:
337
+ angle = _vertical_angle_change(data, gap=channel_gap)
338
+ turning_v = _local_maximum_indexes(abs(angle), thresh)
339
+ return turning_h, turning_v
340
+
341
+ return turning_h
342
+
343
+ elif data_type == 'waveform':
344
+ nch = len(data)
345
+ cc = np.zeros(nch - 1)
346
+ for i in range(nch - 1):
347
+ cc[i] = _xcorr(data[i], data[i + 1])
348
+ median = np.median(cc)
349
+ mad = np.median(abs(cc - median))
350
+
351
+ return np.argwhere(cc < median - thresh * mad)[0]
352
+
353
+ else:
354
+ raise ValueError('Data_type should be \'coordinate\' or \'waveform\'.')
@@ -0,0 +1,165 @@
1
+ # Purpose: Waveform decomposition
2
+ # Author: Minzhe Hu
3
+ # Date: 2024.5.13
4
+ # Email: hmz2018@mail.ustc.edu.cn
5
+ import numpy as np
6
+ from numpy.fft import irfft2, ifftshift
7
+ from daspy.basic_tools.preprocessing import padding, cosine_taper
8
+ from daspy.basic_tools.freqattributes import next_pow_2, fk_transform
9
+ from daspy.advanced_tools.denoising import curvelet_denoising
10
+
11
+
12
+ def fk_fan_mask(f, k, fmin=None, fmax=None, kmin=None, kmax=None, vmin=None,
13
+ vmax=None, edge=0.1, flag=None):
14
+ """
15
+ Make a fan mask in f-k domain for f-k filter.
16
+
17
+ :param f: Frequency sequence.
18
+ :param k: Wavenumber sequence.
19
+ :param fmin, fmax, kmin, kmax, vmin, vmax: float or or sequence of 2 floats.
20
+ Sequence of 2 floats represents the start and end of taper.
21
+ :param edge: float. The width of fan mask taper edge.
22
+ :param flag: -1 keep only negative apparent velocities, 0 keep both postive
23
+ and negative apparent velocities, 1 keep only positive apparent
24
+ velocities.
25
+ :return: Fan mask.
26
+ """
27
+ ff = np.tile(f, (len(k), 1))
28
+ kk = np.tile(k, (len(f), 1)).T
29
+ vv = - np.divide(ff, kk, out=np.ones_like(ff) * 1e10, where=kk != 0)
30
+ mask = np.ones(vv.shape)
31
+ for phy_quan in ['f', 'k', 'v']:
32
+ p = eval(phy_quan * 2)
33
+ pmin = eval(phy_quan + 'min')
34
+ if pmin:
35
+ if isinstance(pmin, (tuple, list, np.ndarray)):
36
+ tp_b, tp_e = min(pmin), max(pmin)
37
+ else:
38
+ tp_b, tp_e = pmin * max(1 - edge / 2, 0), pmin * (1 + edge / 2)
39
+ tp_wid = tp_e - tp_b
40
+ mask[(abs(p) <= tp_b)] = 0
41
+ area = (abs(p) > tp_b) & (abs(p) < tp_e)
42
+ mask[area] *= 0.5 - 0.5 * \
43
+ np.cos(((abs(p[area]) - tp_b) / tp_wid) * np.pi)
44
+
45
+ pmax = eval(phy_quan + 'max')
46
+ if pmax:
47
+ if isinstance(pmax, (tuple, list, np.ndarray)):
48
+ tp_b, tp_e = max(pmax), min(pmax)
49
+ else:
50
+ tp_b, tp_e = pmax * (1 + edge / 2), pmax * (1 - edge / 2)
51
+ tp_wid = tp_b - tp_e
52
+ mask[(abs(p) >= tp_b)] = 0
53
+ area = (abs(p) > tp_e) & (abs(p) < tp_b)
54
+ mask[area] *= 0.5 - 0.5 * \
55
+ np.cos(((tp_b - abs(p[area])) / tp_wid) * np.pi)
56
+
57
+ if flag:
58
+ mask[np.sign(vv) == flag] = 0
59
+ return mask
60
+
61
+
62
+ def fk_filter(data, dx, fs, taper=(0.02, 0.05), pad='default', mode='decompose',
63
+ fmin=None, fmax=None, kmin=None, kmax=None, vmin=None, vmax=None,
64
+ edge=0.1, flag=None, verbose=False):
65
+ """
66
+ Transform the data to the f-k domain using 2-D Fourier transform method, and
67
+ transform back to the x-t domain after filtering.
68
+
69
+ :param data: numpy.ndarray. Data to do fk filter.
70
+ :param dx: Channel interval in m.
71
+ :param fs: Sampling rate in Hz.
72
+ :param taper: float or sequence of floats. Each float means decimal
73
+ percentage of Tukey taper for corresponding dimension (ranging from 0 to
74
+ 1). Default is 0.1 which tapers 5% from the beginning and 5% from the
75
+ end.
76
+ :param pad: Pad the data or not. It can be float or sequence of floats. Each
77
+ float means padding percentage before FFT for corresponding dimension.
78
+ If set to 0.1 will pad 5% before the beginning and after the end.
79
+ 'default' means pad both dimensions to next power of 2. None or False
80
+ means don't pad data before or during Fast Fourier Transform.
81
+ :param mode: str. 'remove' for denoising, 'retain' for extraction, and
82
+ 'decompose' for decomposition.
83
+ :param fmin, fmax, kmin, kmax, vmin, vmax: float or or sequence of 2 floats.
84
+ Sequence of 2 floats represents the start and end of taper.
85
+ :param edge: float. The width of fan mask taper edge.
86
+ :param flag: -1 keep only negative apparent velocities, 0 keep both postive
87
+ and negative apparent velocities, 1 keep only positive apparent
88
+ velocities.
89
+ :param verbose: If True, return filtered data, f-k spectrum, frequency
90
+ sequence, wavenumber sequence and f-k mask.
91
+ :return: Filtered data and some variables in the process if verbose==True.
92
+ """
93
+ data_tp = cosine_taper(data, taper)
94
+ if pad == 'default':
95
+ nch, nt = data.shape
96
+ dn = (next_pow_2(nch) - nch, next_pow_2(nt) - nt)
97
+ nfft = None
98
+ elif pad is None or pad is False:
99
+ dn = 0
100
+ nfft = None
101
+ else:
102
+ dn = np.round(np.array(pad) * data.shape).astype(int)
103
+ nfft = 'default'
104
+
105
+ data_pd = padding(data_tp, dn)
106
+ nch, nt = data_pd.shape
107
+
108
+ fk, f, k = fk_transform(data_pd, dx, fs, taper=0, nfft=nfft)
109
+
110
+ mask = fk_fan_mask(f, k, fmin, fmax, kmin, kmax, vmin, vmax, edge=edge,
111
+ flag=flag)
112
+
113
+ if mode == 'remove':
114
+ mask = 1 - mask
115
+
116
+ if mode == 'decompose':
117
+ data_flt1 = irfft2(ifftshift(fk * mask, axes=0)).real[:nch, :nt]
118
+ data_flt1 = padding(data_flt1, dn, reverse=True)
119
+ data_flt2 = irfft2(ifftshift(fk * (1 - mask), axes=0)).real[:nch, :nt]
120
+ data_flt2 = padding(data_flt2, dn, reverse=True)
121
+ if verbose:
122
+ return data_flt1, data_flt2, fk, f, k, mask
123
+ else:
124
+ return data_flt1, data_flt2
125
+ else:
126
+ data_flt = irfft2(ifftshift(fk * mask, axes=0)).real[:nch, :nt]
127
+ data_flt = padding(data_flt, dn, reverse=True)
128
+ if verbose:
129
+ return data_flt, fk, f, k, mask
130
+ else:
131
+ return data_flt
132
+
133
+
134
+ def curvelet_windowing(data, dx, fs, mode='decompose', vmin=0, vmax=np.inf,
135
+ flag=None, pad=0.3, scale_begin=3, nbscales=None,
136
+ nbangles=16, finest=1):
137
+ """
138
+ Use curevelet transform to keep cooherent signal with certain velocity
139
+ range. {Atterholt et al., 2022 , Geophys. J. Int.}
140
+
141
+ :param data: numpy.ndarray. Data to decomposite.
142
+ :param dx: Channel interval in m.
143
+ :param fs: Sampling rate in Hz.
144
+ :param mode: str. 'remove' for denoising, 'retain' for extraction, and
145
+ 'decompose' for decomposition.
146
+ :param vmin, vmax: float. Velocity range in m/s.
147
+ :param flag: -1 keep only negative apparent velocities, 0 keep both postive
148
+ and negative apparent velocities, 1 keep only positive apparent
149
+ velocities.
150
+ :param pad: float or sequence of floats. Each float means padding percentage
151
+ before FFT for corresponding dimension. If set to 0.1 will pad 5% before
152
+ the beginning and after the end.
153
+ :param scale_begin: int. The beginning scale to do coherent denoising.
154
+ :param nbscales: int. Number of scales including the coarsest wavelet level.
155
+ Default set to ceil(log2(min(M,N)) - 3).
156
+ :param nbangles: int. Number of angles at the 2nd coarsest level,
157
+ minimum 8, must be a multiple of 4.
158
+ :param finest: int. Objects at the finest scale. 1 for curvelets, 2 for
159
+ wavelets. Curvelets are more precise while wavelets are more efficient.
160
+ :return: numpy.ndarray. Decomposed data.
161
+ """
162
+ return curvelet_denoising(data, choice=1, pad=pad, vmin=vmin, vmax=vmax,
163
+ flag=flag, dx=dx, fs=fs, mode=mode,
164
+ scale_begin=scale_begin, nbscales=nbscales,
165
+ nbangles=nbangles, finest=finest)