coodddaaaa 1.4.2__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.
coodddaaaa/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from coodddaaaa.version import __version__
coodddaaaa/butter.py ADDED
@@ -0,0 +1,251 @@
1
+ """
2
+ Modified after sigy 1.5.3, M.L. 21/04/2023
3
+
4
+ Time / Fourier domain butterworth filter
5
+ warning : the fourier domain filter has a slightly different response that the time domain one
6
+ comparing both reveals that the time domain filter may include a water-level that do not
7
+ not exist with fourier domain, this results in slight differences near the signal edges
8
+ taper the waveform properly fixes the difference
9
+ TODO : use ba_analog ?
10
+ TODO : use scipy.fft => parallel
11
+ """
12
+
13
+ from typing import Optional
14
+ from scipy.signal import butter, sosfilt, sosfiltfilt, sosfreqz
15
+ from scipy.fftpack import fftfreq, fft, ifft, fft2, ifft2
16
+ import numpy as np
17
+
18
+
19
+ class ButterworthFilter(object):
20
+ """
21
+
22
+ :param freqmin: lower frequency in Hz, or None for highpass filtering
23
+ :param freqmax: upper frequency in Hz, or None for lowpass filtering
24
+ :param sampling_rate: in Hz
25
+ :param order: of the filter
26
+ """
27
+
28
+ _sos = None
29
+ _sampling_rate = None
30
+
31
+ def __init__(self,
32
+ freqmin: Optional[float],
33
+ freqmax: Optional[float],
34
+ sampling_rate: Optional[float],
35
+ order: float = 4.):
36
+
37
+ nyquist = 0.5 * sampling_rate
38
+ self._freqmin = freqmin
39
+ self._freqmax = freqmax
40
+ self._order = order
41
+ self._sampling_rate = sampling_rate
42
+
43
+ if freqmin is None and freqmax is None:
44
+ raise ValueError(freqmin, freqmax)
45
+
46
+ elif freqmin is not None and freqmax is not None:
47
+ self._sos = butter(order, [freqmin / nyquist, freqmax / nyquist],
48
+ output="sos", btype="band")
49
+
50
+ elif freqmin is not None:
51
+ self._sos = butter(order, [freqmin / nyquist],
52
+ output="sos", btype="high")
53
+
54
+ elif freqmax is not None:
55
+ self._sos = butter(order, [freqmax / nyquist],
56
+ output="sos", btype="low")
57
+
58
+ else:
59
+ raise ValueError(freqmin, freqmax)
60
+
61
+ def timecall(self, data, zerophase=False, axis=-1):
62
+ if not zerophase:
63
+ filtered_data = sosfilt(sos=self._sos, x=data, axis=axis)
64
+ # filtered_data = lfilter(b=self.b, a=self.a, x=data, axis=axis)
65
+
66
+ else:
67
+ filtered_data = sosfiltfilt(sos=self._sos, x=data, axis=axis)
68
+
69
+
70
+ return filtered_data
71
+
72
+ def response(self, npts, zerophase=False, input_domain="fft", qc=False):
73
+ """
74
+ Almost equivalent to timecall
75
+ the response looks better than timecall, no water level applied
76
+ """
77
+
78
+ if input_domain == "fft":
79
+ freqs = fftfreq(npts, 1. / self._sampling_rate)
80
+ # equivalent to (except for freqs (0 to nyquist, no wrapping))
81
+ # freqs, response = sosfreqz(self._sos, worN=npts, whole=True, fs=self._sampling_rate)
82
+
83
+ elif input_domain == "rfft":
84
+ raise Exception(
85
+ 'warning : the behavior of scipy.fftpack.rfft '
86
+ 'differs from scipy.fft.rfft')
87
+ freqs = rfftfreq(npts, 1. / self._sampling_rate)
88
+
89
+ else:
90
+ raise NotImplementedError(input_domain)
91
+
92
+ _, response = sosfreqz(self._sos, worN=freqs, whole=True, fs=self._sampling_rate)
93
+
94
+ if zerophase:
95
+ response = np.abs(response) ** 2.
96
+
97
+ if qc:
98
+ import matplotlib.pyplot as plt
99
+ data = np.random.randn(npts)
100
+ filtered_data = self.timecall(data=data, zerophase=zerophase)
101
+
102
+ if input_domain == "fft":
103
+ # freqs = fftfreq(npts, 1./self._sampling_rate)
104
+ tfdata = fft(data)
105
+ filtered_tfdata = fft(filtered_data)
106
+
107
+ elif input_domain == "rfft":
108
+ raise Exception(
109
+ 'warning : the behavior of scipy.fftpack.rfft '
110
+ 'differs from scipy.fft.rfft')
111
+ # freqs = fftfreq(npts, 1. / self._sampling_rate)
112
+ tfdata = rfft(data)
113
+ filtered_tfdata = rfft(filtered_data)
114
+
115
+ else:
116
+ raise NotImplementedError(input_domain)
117
+
118
+ expected_response = filtered_tfdata / tfdata
119
+
120
+ plt.figure()
121
+ plt.subplot(311, title=f"{zerophase}")
122
+ plt.plot(freqs, expected_response.real, linewidth=3)
123
+ plt.plot(freqs, response.real, linewidth=1)
124
+
125
+ plt.subplot(312)
126
+ plt.plot(freqs, expected_response.imag, linewidth=3)
127
+ plt.plot(freqs, response.imag, linewidth=1)
128
+
129
+ plt.subplot(313)
130
+ plt.loglog(np.abs(freqs), np.abs(expected_response), linewidth=3)
131
+ plt.loglog(np.abs(freqs), np.abs(response), linewidth=1)
132
+ plt.show()
133
+
134
+ return freqs, response
135
+
136
+ def __call__(self, data, zerophase=False, axis=-1, input_domain="time"):
137
+ """
138
+ Returns the filtered data
139
+ can be called on time domain (real or complex) data
140
+ fft or rfft transformed data (use input_domain)
141
+
142
+ :param data:
143
+ :param zerophase:
144
+ :param axis:
145
+ :param input_domain:
146
+ """
147
+ if input_domain == "time":
148
+ return self.timecall(data=data, zerophase=zerophase, axis=axis)
149
+
150
+ elif input_domain in ["fft", "rfft"]:
151
+
152
+ if input_domain == "rfft" and zerophase is False:
153
+ raise Exception(
154
+ 'warning : the behavior of scipy.fftpack.rfft '
155
+ 'differs from scipy.fft.rfft')
156
+ raise ValueError(f"{input_domain=}, {zerophase=} => complex => irfft not applicable")
157
+
158
+ _, response = self.response(
159
+ npts=len(data), zerophase=zerophase,
160
+ input_domain=input_domain, qc=False)
161
+
162
+ return data * response
163
+
164
+ else:
165
+ raise ValueError(input_domain)
166
+
167
+ def show(self, fig, freqs=None, zerophase=False, **kwargs):
168
+ freqs, response = sosfreqz(self._sos, worN=freqs, whole=False, fs=self._sampling_rate)
169
+
170
+ ax = fig.add_subplot(121)
171
+ bx = fig.add_subplot(122, sharex=ax)
172
+
173
+ if zerophase:
174
+ response = np.abs(response) ** 2.0
175
+
176
+ ax.loglog(freqs, np.abs(response), **kwargs)
177
+ bx.semilogx(freqs, np.angle(response), **kwargs)
178
+
179
+ ax.set_ylabel('response modulus')
180
+ bx.set_ylabel('response phase')
181
+
182
+ for cx in [ax, bx]:
183
+ ylim = cx.get_ylim()
184
+ cx.plot(self._freqmin * np.ones(2), ylim, 'r--')
185
+ cx.plot(self._freqmax * np.ones(2), ylim, 'r--')
186
+ cx.grid(True, linestyle="--")
187
+ cx.set_xlabel('frequency (Hz)')
188
+ fig.suptitle(
189
+ f'{self._freqmin},{self._freqmax},{self._order},{zerophase}')
190
+
191
+
192
+ class BandpassFilter(ButterworthFilter):
193
+ """
194
+ Shortcut for ButterworthFilter for band-pass filtering
195
+ """
196
+ def __init__(self, freqmin, freqmax, sampling_rate, order=4):
197
+ ButterworthFilter.__init__(
198
+ self, freqmin=freqmin, freqmax=freqmax,
199
+ sampling_rate=sampling_rate, order=order)
200
+
201
+
202
+ class LowpassFilter(ButterworthFilter):
203
+ """
204
+ Shortcut for ButterworthFilter for low-pass filtering
205
+ """
206
+
207
+ def __init__(self, freqmax, sampling_rate, order=4):
208
+ ButterworthFilter.__init__(
209
+ self, freqmin=None, freqmax=freqmax,
210
+ sampling_rate=sampling_rate, order=order)
211
+
212
+
213
+ class HighpassFilter(ButterworthFilter):
214
+ """
215
+ Shortcut for ButterworthFilter for high-pass filtering
216
+ """
217
+ def __init__(self, freqmin, sampling_rate, order=4):
218
+ ButterworthFilter.__init__(
219
+ self, freqmin=freqmin, freqmax=None,
220
+ sampling_rate=sampling_rate, order=order)
221
+
222
+
223
+ if __name__ == '__main__':
224
+ import matplotlib.pyplot as plt
225
+ from scipy.signal.windows import tukey
226
+
227
+ npts = 1200
228
+ sampling_rate = 1.0123456
229
+ data = 1.0 * np.random.randn(npts)
230
+ data *= tukey(len(data), 0.2)
231
+
232
+ freqmin = 0.03
233
+ freqmax = 0.08
234
+ fftfreqs = fftfreq(npts, 1. / sampling_rate)
235
+
236
+ bp = BandpassFilter(freqmin=freqmin, freqmax=freqmax, sampling_rate=sampling_rate, order=4)
237
+
238
+ bp.show(plt.figure(), zerophase=False)
239
+ bp.show(plt.figure(), zerophase=True)
240
+
241
+ ax = plt.gcf().axes[0]
242
+ ax.plot([freqmin, freqmin], ax.get_ylim(), 'r--')
243
+ ax.plot([freqmax, freqmax], ax.get_ylim(), 'r--')
244
+
245
+ plt.figure()
246
+ plt.plot(data, 'k')
247
+ plt.plot(bp(data, zerophase=True), "b", linewidth=3)
248
+ plt.plot(ifft(bp(fft(data), zerophase=True, input_domain="fft")).real, "g-")
249
+ # plt.plot(irfft(bp(rfft(data), zerophase=True, input_domain="rfft")).real, "m--")
250
+
251
+ plt.show()
@@ -0,0 +1,117 @@
1
+ """
2
+ Modified after sigy 1.5.3, M.L. 20/08/2023
3
+
4
+ Fourier domain oversampling, for the sake of simplicity,
5
+ this program can increase the number of samples only by 2**n, where n is an integer.
6
+
7
+ The oversampling is performed in the Fourier domain,
8
+ - for optimal use, it is preferred to use the fft_oversamp
9
+ if the signal is already in Fourier domain
10
+ - make sure the signal is properly detrended and tapered at its edges prior to FFT
11
+ otherwise, you might observe wiggles at the edges of the signal after oversampling.
12
+ (remember that fft assume a periodization of the signal in time)
13
+
14
+ """
15
+
16
+ import numpy as np
17
+ from scipy.fftpack import fft, ifft
18
+
19
+
20
+ def oversamp(t0: float, dt: float, data: np.ndarray,
21
+ npow2: int, axis: int = -1, demean: bool = False) \
22
+ -> (np.ndarray, np.ndarray):
23
+ """
24
+ Time domain version of fft_oversamp
25
+
26
+ :param t0: start time, sec
27
+ :param dt: sampling interval, sec
28
+ :param data: time domain data array (1d or more)
29
+ :param npow2: oversamp by 2 ** npow2
30
+ :param axis: axis along which to oversample the signal
31
+ :return to: the new time vector
32
+ :return datao: the oversample data
33
+
34
+ """
35
+ nt = data.shape[axis]
36
+ t_over = t0 + np.arange(nt * 2 ** npow2) * (dt / (2 ** npow2))
37
+ if demean:
38
+ m = data.mean(axis=axis)
39
+ else:
40
+ m = 0.
41
+
42
+ # factor = exp(npow2 * log(2))
43
+ # npow2 = log(factor) / log(2)
44
+ data_over = ifft(
45
+ fft_oversamp(
46
+ fft(data - m, axis=axis),
47
+ npow2=npow2,
48
+ axis=axis),
49
+ axis=axis).real + m
50
+ return t_over, data_over
51
+
52
+
53
+ def fft_oversamp(fft_data: np.ndarray, npow2: int = 1, axis: int = -1) -> np.ndarray:
54
+ """
55
+ Oversamp a signal by padding it with zeros in the FFT domain
56
+ the number of sample is multiplied by 2 ** npow2 (default 2**1)
57
+
58
+ :param fft_data: output of fft
59
+ :param npow2: oversampling rate expressed as a power of 2
60
+ :param axis: the axis along which to perform oversampling
61
+ :return: oversample data in fft domain
62
+ """
63
+
64
+ n = 2 ** npow2
65
+
66
+ npts = fft_data.shape[axis]
67
+
68
+ i_first_negative_freq = npts // 2 + npts % 2
69
+ n_positive_freqs = i_first_negative_freq
70
+ n_negative_freqs = npts - n_positive_freqs
71
+
72
+ new_shape = list(fft_data.shape)
73
+ new_shape[axis] = n * npts
74
+ new_fft_data = np.zeros(new_shape, complex)
75
+
76
+ view = fft_data.swapaxes(0, axis)
77
+ new_view = new_fft_data.swapaxes(0, axis)
78
+
79
+ new_view[:n_positive_freqs, ...] = n * view[:i_first_negative_freq, ...]
80
+ new_view[-n_negative_freqs:, ...] = n * view[i_first_negative_freq:, ...]
81
+
82
+ return new_fft_data
83
+
84
+
85
+ if __name__ == '__main__':
86
+
87
+ from scipy.signal import butter, sosfiltfilt
88
+ from scipy.signal.windows import tukey
89
+ import matplotlib.pyplot as plt
90
+
91
+ nt = 127 # number of samples
92
+ dt = 0.1 # sampling interval in sec
93
+ t0 = -10.012351503 # starttime in sec
94
+
95
+ t = t0 + np.arange(nt) * dt
96
+ # ny = 0.5 / dt # nyquist, Hz
97
+
98
+ # prepare bandpass filter and time taper
99
+ sos = butter(4.0, # order
100
+ [0.1, 0.5], # fmin, fmax relative to nyquist
101
+ output="sos", btype="band")
102
+ taper = tukey(nt, 0.1)
103
+
104
+ # generate random signal
105
+ y = np.random.randn(nt)
106
+
107
+ # bandpass / taper
108
+ y = sosfiltfilt(sos=sos, x=y)
109
+ y *= taper
110
+
111
+ # oversamp
112
+ t1, y1 = oversamp(t0=t0, dt=dt, data=y, npow2=4, axis=-1, demean=True)
113
+
114
+ plt.figure()
115
+ plt.plot(t, y, 'ko-')
116
+ plt.plot(t1, y1, 'r')
117
+ plt.show()
coodddaaaa/hypermax.py ADDED
@@ -0,0 +1,60 @@
1
+ """
2
+ Copyright (c) 2023 maximilien.lehujeur
3
+
4
+ Finds the maximum of a random array with subsample precision
5
+ by looking for the zero crossing of the
6
+ first order finite difference derivative
7
+ """
8
+ from typing import Optional
9
+ import numpy as np
10
+ from matplotlib.axes import Axes
11
+
12
+
13
+ def hypermax(time_array: np.ndarray, function_array: np.ndarray, axqc: Optional[Axes] = None, assume_t_growing: bool = False):
14
+ """
15
+ :param time_array: time array
16
+ :param function_array: function array
17
+ :param axqc: matplotlib ax or None for visual qc
18
+ :param assume_t_growing:
19
+ """
20
+ if not assume_t_growing:
21
+ assert (time_array[1:] > time_array[:-1]).all(), "t must be strictly growing"
22
+
23
+ imax = np.argmax(function_array)
24
+ tmax, fmax = time_array[imax], function_array[imax]
25
+
26
+ if imax == 0:
27
+ return time_array[0]
28
+
29
+ elif imax == len(time_array) - 1:
30
+ return time_array[-1]
31
+
32
+ tt = (0.5 * (time_array[imax: imax + 2] + time_array[imax - 1: imax + 1]))
33
+ ff = (function_array[imax: imax + 2] - function_array[imax - 1: imax + 1]) / (time_array[imax: imax + 2] - time_array[imax - 1: imax + 1])
34
+
35
+ ip = np.argsort(ff)
36
+ thypermax = np.interp(0., xp=ff[ip], fp=tt[ip])
37
+
38
+ if axqc is not None:
39
+ axqc.plot(time_array, function_array, "k+-", alpha = 0.4)
40
+ axqc.plot(tmax, fmax, 'ko')
41
+ # axqc.plot(dt, df, "r+-", alpha = 0.4)
42
+ axqc.plot(tt, ff, "r", alpha = 1.0)
43
+ axqc.plot(thypermax, 0, "r*", alpha = 1.0)
44
+ axqc.plot(thypermax * np.ones(2), axqc.get_ylim(), 'r')
45
+ axqc.grid(True)
46
+
47
+ return thypermax
48
+
49
+
50
+ if __name__ == "__main__":
51
+ import matplotlib.pyplot as plt
52
+ t = np.unique(np.random.randn(50))
53
+ f = np.sinc(t)
54
+
55
+ tmax = hypermax(t, f, axqc=plt.gca())
56
+
57
+ print('true max : ', 0.)
58
+ print('max : ', t[np.argmax(f)])
59
+ print('hypermax : ', tmax)
60
+ plt.show()
coodddaaaa/interp1d.py ADDED
@@ -0,0 +1,346 @@
1
+ """
2
+ Copyright (c) 2023 maximilien.lehujeur
3
+
4
+ Linear and cubic interpolation in 1d using fixed grids
5
+ and sparse operators for cases where one need
6
+ to interpolate functions on the same grids many times
7
+
8
+ note I do not use the scipy interpolator
9
+ because I need an interpolator that can be created from the grids only
10
+ and called later on with the function to interpolate
11
+
12
+ 2023.04.07 : P. Mora : Speed up the construction of the sparse matrixes => x20 to x50
13
+ """
14
+
15
+ import numpy as np
16
+ import matplotlib.pyplot as plt
17
+ from scipy import sparse as sp
18
+ from scipy.sparse import linalg as splinalg
19
+ from scipy.fftpack import rfft # NOT scipy.fft.rfft !!!
20
+
21
+ SPMATRIXFORMAT = {"csc": sp.csc_matrix, "csr": sp.csr_matrix}
22
+
23
+
24
+ class LinearInterpolator1d:
25
+ """
26
+ Linear interpolation operator
27
+
28
+ :param x0: x coordinate of the first sample
29
+ :param nx: number of samples
30
+ :param dx: sampling interval
31
+ :param xi: the points where we need the interpolated values
32
+ => f(xi) is computed by self.__call__
33
+ :param format: format to use for the linear operator
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ x0: float, nx: int, dx: float,
39
+ xi: np.ndarray,
40
+ format: str ="csc"):
41
+ """
42
+ x is the grid at which the function will be defined (nodes)
43
+ xi are the points where the function will be interpolated
44
+ """
45
+
46
+ self.x0 = x0
47
+ self.nx = nx
48
+ self.dx = dx
49
+ self.xi = xi
50
+
51
+ # nodes
52
+ x = np.arange(nx) * dx + x0
53
+
54
+ self._idx_xi_to_interp, self._idx_x_after_interp_points = \
55
+ self.find_interp_points_in_grid(x0, nx, dx, xi)
56
+
57
+ # nodes before the interpolation points
58
+ rows1 = self._idx_xi_to_interp
59
+ cols1 = self._idx_x_after_interp_points - 1
60
+ dxafter = (x[self._idx_x_after_interp_points] - xi[self._idx_xi_to_interp])
61
+ vals1 = dxafter / self.dx
62
+
63
+ # nodes after the interpolation points
64
+ rows2 = rows1 # self._idx_xi_to_interp
65
+ cols2 = self._idx_x_after_interp_points
66
+ vals2 = 1. - vals1 # dxbefore / self.dx
67
+
68
+ rows = np.concatenate((rows1, rows2))
69
+ cols = np.concatenate((cols1, cols2))
70
+ vals = np.concatenate((vals1, vals2))
71
+
72
+ # assemble
73
+ self.lininterp_operator = \
74
+ SPMATRIXFORMAT[format](
75
+ (vals, (rows, cols)),
76
+ shape=(len(self.xi), self.nx))
77
+
78
+ def find_interp_points_in_grid(self, x0: float, nx: int, dx: float, xi: np.ndarray):
79
+
80
+ # find indexs of x of nodes located after the interp points xi
81
+ k = np.ceil((xi - x0) / dx).astype(int)
82
+
83
+ # find the interp points that occur within the interp bounds
84
+ m = (k > 0) & (k < nx) # mask
85
+ idx_xi_to_interp = np.arange(len(xi))[m] # to indexs
86
+
87
+ # eliminate the interp points out of bounds
88
+ # => interp will return 0 for these points
89
+ idx_x_after_interp_points = k[idx_xi_to_interp]
90
+
91
+ return idx_xi_to_interp, idx_x_after_interp_points
92
+
93
+ def __call__(self, f: np.ndarray):
94
+ """
95
+ affect function values at x and return the interpolated values at xi
96
+ """
97
+ # assert isinstance(f, np.ndarray)
98
+ # assert f.ndim == 1
99
+ # assert len(f) == len(self.x)
100
+
101
+ return self.lininterp_operator * f
102
+
103
+
104
+ class SecondDerivativeOperatorTypeII:
105
+ """
106
+ Second derivative operator order 3 in the internal domain,
107
+ Implement type II boundary condition after https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation
108
+ For a regular grid only
109
+ x is the grid at which the function will be defined (nodes)
110
+ xi are the points where the function will be interpolated
111
+
112
+ :param nx: number of nodes
113
+ :param dx: sampling interval between nodes
114
+ :param format: format of the sparse operator
115
+ """
116
+
117
+ def __init__(self, nx: int, dx: float, format: str ="csc"):
118
+
119
+ idx2 = dx ** -2.
120
+ self.operator = sp.diags([1, -2, 1], [-1, 0, 1], shape=(nx, nx), format=format) * idx2
121
+ # for type II boundary condition => d[0] = 2 * f''0 = 0
122
+ self.operator[0, 0] = 0
123
+ self.operator[0, 1] = 0
124
+ # for type II boundary condition => d[-1] = 2 * f''[-1] = 0
125
+ self.operator[-1, -1] = 0
126
+ self.operator[-1, -2] = 0
127
+
128
+ def __call__(self, f: np.ndarray):
129
+ """
130
+ compute the derivative of f on x
131
+ """
132
+ # assert isinstance(f, np.ndarray)
133
+ # assert f.ndim == 1
134
+ # assert len(f) == len(self.x)
135
+
136
+ return self.operator * f
137
+
138
+
139
+ class CubicInterpolator1d(LinearInterpolator1d):
140
+ """
141
+ Lagrange Cubic interpolation with boundary type II from https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation
142
+ Works only on a regular grid for now
143
+ x is the grid at which the function will be defined (nodes)
144
+ xi are the points where the function will be interpolated
145
+ :param x0: x of first sample
146
+ :param nx: number of samples
147
+ :param dx: sampling interval
148
+ :param xi: array of points where to interpolate the function
149
+ :param format: format of the sparse operator
150
+ """
151
+
152
+ def __init__(
153
+ self,
154
+ x0: float, nx: int, dx: float,
155
+ xi: np.ndarray,
156
+ format: str ="csc"):
157
+
158
+ LinearInterpolator1d.__init__(self, x0=x0, nx=nx, dx=dx, xi=xi, format=format)
159
+ _sp_matrix = SPMATRIXFORMAT[format]
160
+
161
+ # ==== add more internal operators to move to cubic interpolation
162
+ # multiply by 3 because 6*f[xi-1,xi,xi+1] means 3 * [f(xi+1) - 2 * f(xi) + f(xi-1)] / (hi**2)
163
+ self.derivator = 3. * SecondDerivativeOperatorTypeII(nx=nx, dx=dx, format=format).operator
164
+
165
+ # left term in eq (6) from https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation
166
+ upper_diag = .5 * np.ones(nx-1, float) # lambda terms
167
+ diag = 2 * np.ones(nx, float) # diagonal terms
168
+ lower_diag = 0.5 * np.ones(nx-1, float) # mu terms
169
+ lower_diag[-1] = upper_diag[0] = 0. # type II boundary condition
170
+ a = sp.diags((lower_diag, diag, upper_diag), offsets=(-1, 0, 1), format=format)
171
+ # inverse_of_a = splinalg.inv(a) #=> dense
172
+ self.solver = splinalg.splu(a) # => time cost is negligible (0.4%)
173
+
174
+ # ==== Implement the operator for equation (1), with respect to Mis coefficients
175
+ # the missing terms for yi and yi+1 are included in the self.lininterp_operator term
176
+ x = np.arange(nx) * dx + x0
177
+
178
+ # nodes before the interpolation points
179
+ rows1 = self._idx_xi_to_interp
180
+ cols1 = self._idx_x_after_interp_points - 1
181
+ dxafter = (x[self._idx_x_after_interp_points] - xi[self._idx_xi_to_interp])
182
+ vals1 = dxafter ** 3. / (6. * self.dx) - self.dx * dxafter / 6.
183
+
184
+ # nodes after the interpolation points
185
+ rows2 = self._idx_xi_to_interp
186
+ cols2 = self._idx_x_after_interp_points
187
+ dxbefore = (xi[self._idx_xi_to_interp] - x[self._idx_x_after_interp_points-1])
188
+ vals2 = dxbefore ** 3. / (6. * self.dx) - self.dx * dxbefore / 6.
189
+
190
+ rows = np.concatenate((rows1, rows2)) # np.repeat(r_, 2)
191
+ cols = np.concatenate((cols1, cols2)) # np.array([k_-1, k_]).T.flatten()
192
+ vals = np.concatenate((vals1, vals2)) # np.array([v1, 1-v1]).T.flatten()
193
+
194
+ # assemble
195
+ self.cubinterp_operator = \
196
+ SPMATRIXFORMAT[format](
197
+ (vals, (rows, cols)),
198
+ shape=(len(self.xi), self.nx))
199
+
200
+ def __call__(self, f):
201
+
202
+ # assert isinstance(f, np.ndarray)
203
+ # assert f.ndim == 1
204
+ # assert len(f) == self.nx
205
+
206
+ # 3 * f[xi-1, xi, xi+1], d0=d-1=0 for type II boundary condition
207
+ d = self.derivator * f
208
+
209
+ # solve equation (6) for M
210
+ m = self.solver.solve(d)
211
+
212
+ return self.cubinterp_operator * m + self.lininterp_operator * f
213
+
214
+ # TODO : add method to export/reload from npz file
215
+
216
+
217
+ class RFFTInterpolator1d:
218
+ """
219
+ Fourier Interpolator based on rfft
220
+
221
+ :param x0: x of first sample
222
+ :param nx: number of samples
223
+ :param dx: sampling interval
224
+ :param xi: array of points where to interpolate the function
225
+ :param format: format of the sparse operator
226
+
227
+ """
228
+ def __init__(
229
+ self,
230
+ x0: float, nx: int, dx: float,
231
+ xi: np.ndarray):
232
+
233
+ self.x0 = x0
234
+ self.nx = nx
235
+ self.dx = dx
236
+ self.xi = xi
237
+
238
+ npts = nx
239
+ fnfft = npts
240
+ two_pi = 2.0 * np.pi
241
+
242
+ n = (xi - x0) / ((nx - 1) * dx) * (npts - 1)
243
+
244
+ k = np.arange(npts)[:, np.newaxis]
245
+ b = np.zeros((npts, len(xi)))
246
+ b[0, :] = 1.0
247
+
248
+ if npts % 2:
249
+ b[1:npts:2] = +2.0 * np.cos(two_pi * n * k[1:(npts - 1) // 2 + 1, :] / fnfft)
250
+ b[2:npts:2] = -2.0 * np.sin(two_pi * n * k[1:(npts - 1) // 2 + 1, :] / fnfft)
251
+
252
+ else:
253
+ b[1:npts - 1:2] = +2.0 * np.cos(two_pi * n * k[1:npts // 2, :] / fnfft)
254
+ b[2:npts - 1:2] = -2.0 * np.sin(two_pi * n * k[1:npts // 2, :] / fnfft)
255
+ b[-1, :] = +1.0 * np.cos(two_pi * n * npts / fnfft / 2.)
256
+
257
+ b *= 1.0 / fnfft
258
+ self._irfft_basis = b
259
+
260
+ def __call__(self, f: np.ndarray):
261
+ """
262
+ affect function values at x and return the interpolated values at xi
263
+ """
264
+ # assert isinstance(f, np.ndarray)
265
+ # assert f.ndim == 1
266
+ # assert len(f) == len(self.x)
267
+
268
+ return rfft(f, axis=-1).dot(self._irfft_basis)
269
+
270
+
271
+ if __name__ == "__main__":
272
+
273
+ # ========================= simple interpolation test
274
+ # build the grids
275
+ x = np.linspace(0.1, 0.9, 10) # the nodes
276
+ xi = np.linspace(-0.1, 1.1, 1000) # the interpolation points
277
+
278
+ # build the linear operators
279
+ P = LinearInterpolator1d(x0=x[0], nx=len(x), dx=x[1] - x[0], xi=xi)
280
+ C = CubicInterpolator1d(x0=x[0], nx=len(x), dx=x[1] - x[0], xi=xi)
281
+ F = RFFTInterpolator1d(x0=x[0], nx=len(x), dx=x[1] - x[0], xi=xi)
282
+ # pick some random values to attach to the nodes
283
+ f = np.random.randn(len(x))
284
+
285
+ # call the operators for these values
286
+ pi = P(f)
287
+ ci = C(f)
288
+ fi = F(f)
289
+
290
+ # compare the output
291
+ if True:
292
+ plt.figure()
293
+ plt.plot(x, f, "ko")
294
+ plt.plot(xi, pi, 'r-', label='linear')
295
+ plt.plot(xi, ci, 'g-', label='cubic')
296
+ plt.plot(xi, fi, 'm-', label='Fourier')
297
+ plt.gca().legend()
298
+ plt.show()
299
+
300
+ # ========================= interpolation for stretching
301
+ x = np.linspace(0.1, 0.9, 100)
302
+ xi = x * (1. + 0.01)
303
+
304
+ P = LinearInterpolator1d(x0=x[0], nx=len(x), dx=x[1] - x[0], xi=xi)
305
+ C = CubicInterpolator1d(x0=x[0], nx=len(x), dx=x[1] - x[0], xi=xi)
306
+ F = RFFTInterpolator1d(x0=x[0], nx=len(x), dx=x[1] - x[0], xi=xi)
307
+
308
+ f = np.sin(2. * np.pi * x / 0.1)
309
+
310
+ pi = P(f)
311
+ ci = C(f)
312
+ fi = F(f)
313
+
314
+ if True:
315
+ plt.figure()
316
+ plt.plot(x, f, "k")
317
+ plt.plot(x, pi, 'r-', label='linear')
318
+ plt.plot(x, ci, 'g-', label='cubic')
319
+ plt.plot(x, fi, 'm-', label='Fourier')
320
+ plt.gca().legend()
321
+ plt.show()
322
+
323
+ # ========================= interpolation of many signals at ones
324
+ x = np.linspace(0.1, 0.9, 10) # the nodes
325
+ xi = np.linspace(-0.1, 1.1, 1000) # the interpolation points
326
+
327
+ P = LinearInterpolator1d(x0=x[0], nx=len(x), dx=x[1] - x[0], xi=xi)
328
+ C = CubicInterpolator1d(x0=x[0], nx=len(x), dx=x[1] - x[0], xi=xi)
329
+ F = RFFTInterpolator1d(x0=x[0], nx=len(x), dx=x[1] - x[0], xi=xi)
330
+
331
+ f = 0.2 * np.random.randn(10, len(x))
332
+ pi = P(f.T).T
333
+ ci = C(f.T).T
334
+ fi = F(f) # WARNING : Fourier interp is not built the same way as the two other interpolators
335
+ print(ci.shape)
336
+ print(fi.shape)
337
+
338
+ if True:
339
+ plt.figure()
340
+ for n in range(f.shape[0]):
341
+ plt.plot(x, f[n, :] + n, "ko")
342
+ plt.plot(xi, pi[n, :] + n, 'r-', label='linear' if not n else None)
343
+ plt.plot(xi, ci[n, :] + n, 'g-', label='cubic' if not n else None)
344
+ plt.plot(xi, fi[n, :] + n, 'm-', label='Fourier' if not n else None)
345
+ plt.gca().legend()
346
+ plt.show()
@@ -0,0 +1,376 @@
1
+ """
2
+ Copyright (c) 2023 maximilien.lehujeur
3
+ """
4
+
5
+ from typing import Union, Optional, Literal
6
+ import numpy as np
7
+ from coodddaaaa.interp1d import LinearInterpolator1d, CubicInterpolator1d, RFFTInterpolator1d
8
+ from coodddaaaa.hypermax import hypermax
9
+ from scipy.sparse import block_diag
10
+
11
+
12
+ class Stretcher:
13
+ r"""
14
+ An object to compute stretched signals and to perform stretching correlation as defined in Weaver et al., 2011.
15
+
16
+ .. math:: X(\varepsilon) =
17
+ \frac
18
+ {\int{y^{ref}(t \times (1 + \varepsilon)) \cdot y(t) dt}}
19
+ {\sqrt{
20
+ \int{y^{ref}(t \times (1 + \varepsilon))^2 dt}
21
+ \int{y(t)^2 dt}
22
+ }}
23
+
24
+
25
+ The object pre-computes the interpolation operator.
26
+ The user can pre-compute and store the stretched basis of the reference signal.
27
+ This basis can then be provided for stretching correlation with a new signal.
28
+ This object can also compute the stretching between all pairs of signals in a b-scan.
29
+
30
+ :param t0: time of first sample
31
+ :param dt: sampling interval
32
+ :param nt: number of samples
33
+ :param eps: epsilon array
34
+ :param norm: use it to compute normalized correlation.
35
+ warning : for stretching only, use norm = False
36
+ :param interp_kind: which interpolator to use for stretching, among 'linear', "cubic", 'fourier'
37
+ """
38
+ def __init__(self,
39
+ t0: float, dt: float, nt: int,
40
+ eps: np.ndarray,
41
+ norm: bool = False,
42
+ interp_kind: Literal['linear', "cubic", 'fourier'] = "cubic"):
43
+ """
44
+ Initiate the stretcher and the interpolator on a fixed interpolation grid.
45
+
46
+ """
47
+ assert interp_kind in ['linear', 'cubic', 'fourier']
48
+
49
+ self.t0, self.nt, self.dt = t0, nt, dt
50
+ self.eps = eps # 1d, shape (len(eps), )
51
+ self.norm = norm
52
+ self.interp_kind = interp_kind
53
+
54
+ # Time array = nodes at which the function to stretch is defined
55
+ self.t = t0 + np.arange(nt) * dt # 1d, shape (nt, )
56
+
57
+ # Compute the stretching time grid for all values in eps
58
+ # = points at which to evaluate the function for stretching
59
+ self.stretch_time = self.t * (1. + self.eps[:, np.newaxis]) # 2d array, shape (len(eps), nt)
60
+
61
+ # compute the interpolation operator once for all (based on scipy.sparse matrices)
62
+ if interp_kind == "linear":
63
+ self.interpolator = LinearInterpolator1d(
64
+ x0=t0, nx=nt, dx=dt, # nodes
65
+ xi=self.stretch_time.flat[:], # interpolation points
66
+ )
67
+
68
+ elif interp_kind == "cubic":
69
+ t0 = self.t[0]
70
+ nt = len(self.t)
71
+ dt = self.t[1] - self.t[0]
72
+ assert ((self.t - (np.arange(nt) * dt + t0)) / dt <= 1e-6) .all()
73
+
74
+ self.interpolator = CubicInterpolator1d(
75
+ x0=t0, nx=nt, dx=dt, # nodes
76
+ xi=self.stretch_time.flat[:], # interpolation points
77
+ )
78
+
79
+ elif interp_kind == "fourier":
80
+ t0 = self.t[0]
81
+ nt = len(self.t)
82
+ dt = self.t[1] - self.t[0]
83
+ assert ((self.t - (np.arange(nt) * dt + t0)) / dt <= 1e-6) .all()
84
+
85
+ self.interpolator = RFFTInterpolator1d(
86
+ x0=t0, nx=nt, dx=dt, # nodes
87
+ xi=self.stretch_time.flat[:], # interpolation points
88
+ )
89
+
90
+ else:
91
+ raise ValueError(interp_kind)
92
+
93
+ def stretch(self, x: np.ndarray) -> np.ndarray:
94
+ """
95
+ Compute the stretched basis functions from a signal x
96
+
97
+ :param x: the input signal (reference), np.ndarray, 1d, shape (nt, )
98
+ :return x_stretched: the stretched version of x for all values in self.eps, np.ndarray 2d, shape (neps, nt)
99
+ """
100
+
101
+ x_stretched = np.zeros_like(self.stretch_time)
102
+ x_stretched.flat[:] = self.interpolator(x)
103
+
104
+ if self.norm:
105
+ # normalize the stretched function now for efficiency
106
+ # instead of doing it in the correlation
107
+ # => WARNING : this will affect the amplitudes of the stretched data
108
+ # for stretching only, user must use norm=False
109
+
110
+ # norm = np.linalg.norm(x_stretched, axis=1)
111
+ norm = (x_stretched ** 2).sum(axis=1) ** -0.5
112
+
113
+ # NB : I've tried Numba (this ref) => no gain at all
114
+ # https://stackoverflow.com/questions/30437947/
115
+ # most-memory-efficient-way-to-compute-abs2-of-complex-numpy-ndarray
116
+
117
+ x_stretched *= norm[:, np.newaxis]
118
+
119
+ return x_stretched
120
+
121
+ def corr(self, x: np.ndarray, x_stretched: np.ndarray) -> np.ndarray:
122
+ """
123
+ Stretching correlation of x with a basis of stretched versions of the reference signal
124
+
125
+ :param x: signal(s) to be correlated to the reference, np.ndarray,
126
+ either one single signal, 1d, shape (nt, )
127
+ or a bscan, 2d, shape (ntraces, nt)
128
+ :param x_stretched: stretched reference from self.stretch, np.ndarray, 2d, shape (neps, nt, )
129
+ :return c: correlation function np.ndarray,
130
+ either 1d, shape (neps, ) if x is 1d
131
+ or 2d, shape (neps, ntraces) if x is 2d
132
+ """
133
+ if x.ndim == 1:
134
+ assert x.shape == (self.nt, ), \
135
+ f'Shape Error : x must be 1 trace of shape (nt={self.nt}, )'
136
+
137
+ elif x.ndim == 2:
138
+ assert x.shape[1] == self.nt, \
139
+ f'Shape Error : x must be a bscan of shape (ntraces, nt={self.nt})'
140
+
141
+ else:
142
+ raise ValueError('Shape Error, x must be 1d (for single signal) or 2d (for a bscan)')
143
+
144
+ c = x_stretched.dot(x.T) * self.dt # np.ndarray, shape (neps, ntraces)
145
+
146
+ if self.norm:
147
+ # the normalization relative to x_stretched
148
+ # is already done
149
+ if x.ndim == 1:
150
+ # faster?
151
+ c /= x.dot(x) ** 0.5 * self.dt
152
+ elif x.ndim == 2:
153
+ c /= (x * x).sum(axis=-1) ** 0.5 * self.dt # shape (ntraces, )
154
+ else:
155
+ raise Exception('programming error')
156
+ return c
157
+
158
+ def corrmax(self, c: np.ndarray) -> (Union[float, np.ndarray], Union[float, np.ndarray]):
159
+ """
160
+ Find the maximum of the correlation function with subsample precision
161
+
162
+ :param c: correlation function(s) from self.corr
163
+ 1d for a single signal, shape (neps, )
164
+ 2d for a bscan, shape (neps, ntraces)
165
+ :return emax: best epsilon value, dimensionless, it corresponds to dt/t
166
+ float if c is 1d
167
+ 1d array, shape (ntraces, ) if c is 2d
168
+ :return cmax: max correlation, dimensionless, normalized if norm was True in __init__
169
+ float if c is 1d
170
+ 1d array, shape (ntraces, ) if c is 2d
171
+ """
172
+ if c.ndim == 1:
173
+ epsmax = hypermax(
174
+ time_array=self.eps,
175
+ function_array=c,
176
+ assume_t_growing=True)
177
+ cmax = c.max()
178
+
179
+ elif c.ndim == 2:
180
+ neps, ntraces = c.shape
181
+ assert neps == len(self.eps), f"Shape Error, c must be of shape (neps={len(self.eps)}, ntraces)"
182
+
183
+ epsmax = np.zeros(ntraces, float)
184
+ for i in range(ntraces):
185
+ # TODO : implement 2d version of hypermax to avoid the loop ?
186
+ epsmax[i] = hypermax(
187
+ time_array=self.eps,
188
+ function_array=c[:, i],
189
+ assume_t_growing=True)
190
+ cmax = c.max(axis=0) # one max per trace
191
+
192
+ else:
193
+ raise ValueError(
194
+ f'Shape Error, c must be 1d (single trace) or 2d (bscan)')
195
+
196
+ return epsmax, cmax
197
+
198
+ def corr_all_with_all(self, data: np.ndarray) -> (np.ndarray, np.ndarray):
199
+ """
200
+ Correlate all possible pairs of signals in a bscan
201
+
202
+ :param data: the bscan, one trace per row, same sampling (=self.t), 2d, shape (ntraces, nt)
203
+ :return c_triu: the max correlation coefficients for all pairs (upper triangle only)
204
+ :return e_triu: the best stretching coefficients for all pairs (upper triangle only)
205
+ use self.triu2dence to get the full matrices
206
+
207
+ c = Stretcher.triu2dense(c_triu, symetric=True, diag=1.0)
208
+ e = Stretcher.triu2dense(e_triu, symetric=False, diag=0.0)
209
+ """
210
+
211
+ ntraces, nsamps = data.shape
212
+
213
+ c_triu = np.zeros(ntraces * (ntraces - 1) // 2)
214
+ e_triu = np.zeros(ntraces * (ntraces - 1) // 2)
215
+ # itriu, jtriu = np.triu_indices(ntraces, 1)
216
+
217
+ n = 0
218
+ for i in range(ntraces - 1):
219
+ # print(f'{i+1}/{ntraces - 1}')
220
+ # stretch new reference
221
+ y = data[i, :]
222
+ y_stretched = self.stretch(y)
223
+
224
+ # correlate all remaining traces to this new reference
225
+ m = ntraces - i - 1
226
+ cijs = self.corr(x=data[i + 1:, :], x_stretched=y_stretched)
227
+ e_triu[n: n+m], c_triu[n: n+m] = self.corrmax(cijs)
228
+ n += m
229
+
230
+ return c_triu, e_triu
231
+
232
+ @staticmethod
233
+ def triu2dense(x_triu: np.ndarray, symetric: bool, diag: float) -> np.ndarray:
234
+ """
235
+ Convert upper triangle matrix to square matrix
236
+
237
+ :param x_triu: a flat upper triangle without diagonal, 1d, np.ndarray, shape (ntraces * (ntraces - 1) / 2, )
238
+ :param symetric: to impose symetry (True) or anti-symetry (False)
239
+ :param diag: the value to put on the diagonal
240
+ :return x:
241
+ a square matrix with x_triu on its upper triangle, shape (ntraces, ntraces)
242
+ diag on its diagonal
243
+ +-x_triu on its lower triangle
244
+ """
245
+ # 2n = (x * (x -1))
246
+ # 2n = x ** 2 - x
247
+ # x ** 2 - x - 2n = 0
248
+ # d = 2 + 8 * n
249
+ #
250
+ n = int((1 + np.sqrt(8 * len(x_triu) + 2)) / 2)
251
+ x = np.eye(n, dtype=float) * diag
252
+
253
+ ij = np.triu_indices(n, 1)
254
+ x[ij] = x_triu
255
+ if symetric:
256
+ x.T[ij] = x[ij]
257
+ else:
258
+ x.T[ij] = -x[ij]
259
+ return x
260
+
261
+ @staticmethod
262
+ def stretching_uncertainty(
263
+ cmax: Union[float, np.ndarray], fmin: float, fmax: float, tmin: float, tmax: float) \
264
+ -> Union[float, np.ndarray]:
265
+ """
266
+ Stretching uncertainty after Weaver et al 2011.
267
+
268
+ :param cmax: max correlation coefficient from self.corrmax, either a float or a np.ndarray
269
+ :param fmin: lower freq Hz, float
270
+ :param fmax: upper freq Hz, float
271
+ :param tmin: start coda time in s, float
272
+ :param tmax: end coda time in s, float
273
+ :return rmse: uncertainty on epsilon, same type as cmax
274
+ """
275
+
276
+ wc = 2. * np.pi * np.sqrt(fmin * fmax)
277
+ T = 1. / (fmax - fmin) / (np.pi * np.sqrt(2.))
278
+ X = cmax
279
+ rmse = np.sqrt(1 - X ** 2.) / (2 * X)
280
+ rmse *= np.sqrt((6 * T * np.sqrt(np.pi / 2.)) / (wc ** 2. * (tmax ** 3 - tmin ** 3)))
281
+
282
+ return rmse
283
+
284
+
285
+ class InverseStretcher:
286
+ """
287
+ An object to cancel the effect of the stretching on each trace of a bscan
288
+ This can be used to align the traces with the reference, and then to refine the reference.
289
+
290
+ For example:
291
+ you have a bscan of 256 traces with n samples each, bscan is a 2d array shapped (256, n)
292
+ you have an estimate of the stretching history, i.e. 256 epsilon values in an 1D array
293
+ this object returns the bscan corrected from the estimated stretching values
294
+ positive epsilon values (i.e. positive dv/v) mean that the trace was compressed relative to its ref, so this operator stretch it
295
+ negative epsilon values will tend to compress the waveform
296
+
297
+ :param t0: time of first sample
298
+ :param nt: number of samples
299
+ :param dt: sampling interval
300
+
301
+ :param eps_history: epsilon array, one item per trace in the bscan
302
+ :param interp_kind: which interpolator to use for inverse stretching, among 'linear',
303
+
304
+ """
305
+ def __init__(self, t0: float, nt: int, dt: float, eps_history: np.ndarray, interp_kind: Literal['linear'] = "linear"):
306
+
307
+ self.t0 = t0
308
+ self.nt = nt
309
+ self.dt = dt
310
+ self.eps_history = eps_history
311
+
312
+ self.time_array = self.t0 + np.arange(self.nt) * self.dt
313
+ if interp_kind == "linear":
314
+ block_diagonals = []
315
+ for eps in eps_history:
316
+
317
+ op = LinearInterpolator1d(
318
+ x0=self.t0, nx=self.nt, dx=self.dt, xi=self.time_array * (1. + eps))
319
+
320
+ op = op.lininterp_operator.T # transpose to get the inverser interpolator
321
+
322
+ block_diagonals.append(op)
323
+ else:
324
+ raise NotImplementedError(interp_kind)
325
+
326
+ self.interpolator = block_diag(block_diagonals, format="csr")
327
+
328
+ def __call__(self, bscan: np.ndarray) -> np.ndarray:
329
+ if not bscan.shape == (len(self.eps_history), self.nt):
330
+ raise ValueError(
331
+ f'shape error, bscan is {bscan.shape}'
332
+ f'and should be ({len(self.eps_history)=}, {self.nt=})'
333
+ )
334
+ ans = np.zeros_like(bscan)
335
+ ans.flat[:] = self.interpolator * bscan.flat[:]
336
+ return ans
337
+
338
+
339
+ if __name__ == "__main__":
340
+ from coodddaaaa.utils import Timer, polyspace
341
+
342
+ with Timer('constructor'):
343
+ st = Stretcher(
344
+ nt=4096,
345
+ dt=1e-8,
346
+ t0=0.,
347
+ eps=polyspace(-0.01, 0.01, 200, pwr=2.0),
348
+ interp_kind="cubic",
349
+ )
350
+
351
+ x = np.random.randn(st.nt)
352
+ with Timer('stretch'):
353
+ x_stretched = st.stretch(x)
354
+
355
+ with Timer('corr'):
356
+ c = st.corr(x, x_stretched)
357
+
358
+ with Timer('hypermax'):
359
+ epsmax, cmax = st.corrmax(c)
360
+
361
+ x = np.random.randn(123, st.nt)
362
+ with Timer('corr_all_with_all'):
363
+ c_triu, e_triu = st.corr_all_with_all(data=x)
364
+
365
+ with Timer('triu2dense x2'):
366
+ c = st.triu2dense(c_triu, True, 1.0)
367
+ e = st.triu2dense(e_triu, False, 0.0)
368
+
369
+ """
370
+ Timer[constructor]: 312.93 ms
371
+ Timer[stretch]: 12.13 ms
372
+ Timer[corr]: 0.33 ms
373
+ Timer[hypermax]: 0.10 ms
374
+ Timer[corr_all_with_all]: 2904.68 ms
375
+ Timer[triu2dense x2]: 7.41 ms
376
+ """
@@ -0,0 +1,25 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from coodddaaaa.butter import BandpassFilter
4
+
5
+ npts = 200
6
+ dt = 1e-6
7
+ nyquist = 0.5 / dt
8
+
9
+ bp = BandpassFilter(
10
+ freqmin=0.05 * nyquist,
11
+ freqmax=0.2 * nyquist,
12
+ sampling_rate=1./dt,
13
+ order=4)
14
+
15
+ data = np.zeros(npts)
16
+ data[npts // 2] = 1.0 # Dirac
17
+
18
+ plt.figure()
19
+ data = np.zeros(npts)
20
+ data[npts//2] = 1.0
21
+ plt.plot(data)
22
+ plt.plot(bp(data, zerophase=False), label="zerophase=False")
23
+ plt.plot(bp(data, zerophase=True), label="zerophase=True")
24
+ plt.gca().legend()
25
+ plt.show()
coodddaaaa/utils.py ADDED
@@ -0,0 +1,102 @@
1
+ """
2
+ Copyright (c) 2023 maximilien.lehujeur
3
+ """
4
+
5
+
6
+ import time
7
+ import numpy as np
8
+
9
+
10
+ class Timer:
11
+ """Counts the execution time under the "with" statement
12
+
13
+ :param message:
14
+ """
15
+
16
+ def __init__(self, message: str):
17
+ self.message = message
18
+
19
+ def __enter__(self):
20
+ self.start = time.perf_counter()
21
+ return self
22
+
23
+ def __exit__(self, *args, **kwargs):
24
+ end = time.perf_counter()
25
+ print(f'Timer[{self.message}]: {(end - self.start) * 1000.:.2f} ms')
26
+
27
+
28
+ def polyspace(xmin: float, xmax: float, nx: int, pwr: float):
29
+ """A power-law to refine resolution of a stretching grid search near zero
30
+
31
+ :param xmin: min value
32
+ :param xmax: max value
33
+ :param nx: number of points
34
+ :param pwr: power coefficient, increase pwr to refine the resolution near 0
35
+
36
+ """
37
+ assert pwr > 0, ValueError(pwr)
38
+
39
+ tmin = np.sign(xmin) * np.abs(xmin) ** (1. / pwr)
40
+ tmax = np.sign(xmax) * np.abs(xmax) ** (1. / pwr)
41
+ t = np.linspace(tmin, tmax, nx)
42
+ return np.sign(t) * np.abs(t) ** pwr
43
+
44
+
45
+ class TukeyWindow:
46
+ """
47
+ A parameterizable 4 points Tukey function
48
+ :param t0 ... t3: times of the corners of the Tukey window
49
+ """
50
+ def __init__(self, t0:float, t1:float, t2:float, t3:float):
51
+ """
52
+
53
+ """
54
+ assert t0 <= t1 <= t2 <= t3
55
+ self.t0 = t0
56
+ self.t1 = t1
57
+ self.t2 = t2
58
+ self.t3 = t3
59
+
60
+ def _growing(self, t):
61
+ return (1. - np.cos(np.pi * (t - self.t0) / (self.t1 - self.t0))) / 2.0
62
+
63
+ def _decreasing(self, t):
64
+ return (1. + np.cos(np.pi * (t - self.t2) / (self.t3 - self.t2))) / 2.0
65
+
66
+ def __call__(self, t: np.ndarray):
67
+ """
68
+ Evaluate the taper function at t
69
+
70
+ :param t: does not need to be sorted or regularly spaced
71
+ """
72
+ i = np.argsort(t)
73
+
74
+ j0, j1, j2, j3 = \
75
+ np.searchsorted(
76
+ a=t[i], v=[self.t0, self.t1, self.t2, self.t3])
77
+
78
+ y = np.zeros_like(t)
79
+ y[i[:j0]] = 0.
80
+ y[i[j0:j1]] = self._growing(t[i[j0:j1]])
81
+ y[i[j1:j2]] = 1.
82
+ y[i[j2:j3]] = self._decreasing(t[i[j2:j3]])
83
+ y[i[j3:]] = 0.
84
+ return y
85
+
86
+
87
+ if __name__ == "__main__":
88
+ import matplotlib.pyplot as plt
89
+
90
+ x = np.random.randn(10000)
91
+ with Timer('TukeyWindow'):
92
+ taper = TukeyWindow(-0.5, -0.25, 0.25, 0.33)
93
+ y = taper(x)
94
+
95
+ plt.figure()
96
+ plt.plot(x, y, '.')
97
+
98
+ with Timer('Polyspace'):
99
+ x = polyspace(-0.01, 0.03, 100, 2)
100
+ plt.figure()
101
+ plt.plot(x)
102
+ plt.show()
coodddaaaa/version.py ADDED
@@ -0,0 +1 @@
1
+ __version__="1.4.2"
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: coodddaaaa
3
+ Version: 1.4.2
4
+ Summary: Coda stretching
5
+ Home-page:
6
+ Author: Maximilien Lehujeur / Pierric Mora
7
+ Author-email: maximilien.lehujeur@univ-eiffel.fr
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: POSIX :: Linux
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy
15
+ Requires-Dist: scipy
16
+ Requires-Dist: matplotlib
17
+ Requires-Dist: jupyter
18
+ Requires-Dist: notebook
19
+ Requires-Dist: sphinx
20
+ Requires-Dist: sphinx-rtd-theme
21
+ Requires-Dist: myst-parser
22
+ Requires-Dist: nbsphinx
23
+ Dynamic: author
24
+ Dynamic: author-email
25
+ Dynamic: classifier
26
+ Dynamic: description-content-type
27
+ Dynamic: license-file
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
@@ -0,0 +1,15 @@
1
+ coodddaaaa/__init__.py,sha256=ztjGTDSWRCSB0JSuhbk_8eFQhwHXjw8v5ADSq6D6EUo,43
2
+ coodddaaaa/butter.py,sha256=el8Cc646zvKuQXEtOH38z-4G8PjbnOwDQG39A_vAGEQ,8525
3
+ coodddaaaa/fftoversamp.py,sha256=RYOGAzY7Hmyv3efVMymXXYavayNL91-lyU3hcdVXqgA,3458
4
+ coodddaaaa/hypermax.py,sha256=ER0Kklm0Gi5OIA9x-iz8wHRobfSUIHZQolQVLF417_U,1864
5
+ coodddaaaa/interp1d.py,sha256=MXZc6eqUkMBcIS9LyfFciAU9jDMb9P4_OEeUzrYZLis,11984
6
+ coodddaaaa/stretching.py,sha256=a_gk-NrmADa6BXIaxpHHSipshwpU1rBsDyegQIksqHk,14100
7
+ coodddaaaa/test_bandpass.py,sha256=pWAKQ54mFytzYMA8GLgS2P9dAG7LutaqqbMM6E37j0c,530
8
+ coodddaaaa/utils.py,sha256=4iVZ9O5SzKDPCkKglZMj30tw7ersY99MhnyWP5gy_i0,2510
9
+ coodddaaaa/version.py,sha256=EMuS0GKgrSjPsGgisChc21Z9T2CQcN7yYulyL1FRLVY,20
10
+ coodddaaaa-1.4.2.dist-info/licenses/LICENSE,sha256=x2VmhAerMbPYCjhZdVF3G581EhOW3R95KswAHn-VlZA,1076
11
+ examples/__init__.py,sha256=iCdfsReTYFyMKAl86iIo8KR90Syb6mv71om0v2yWASo,44
12
+ coodddaaaa-1.4.2.dist-info/METADATA,sha256=Rl_Sbyaxl7UNGiJoOed6181FdJVDCe9RXhnI_UzhlsQ,815
13
+ coodddaaaa-1.4.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ coodddaaaa-1.4.2.dist-info/top_level.txt,sha256=2z0RQvnwWKgYoJk5LDOR8KM77oGuMZ8G5slQxenfEiY,20
15
+ coodddaaaa-1.4.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 maximilien.lehujeur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ coodddaaaa
2
+ examples
examples/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from coodddaaaa.version import __version__
2
+