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 +1 -0
- coodddaaaa/butter.py +251 -0
- coodddaaaa/fftoversamp.py +117 -0
- coodddaaaa/hypermax.py +60 -0
- coodddaaaa/interp1d.py +346 -0
- coodddaaaa/stretching.py +376 -0
- coodddaaaa/test_bandpass.py +25 -0
- coodddaaaa/utils.py +102 -0
- coodddaaaa/version.py +1 -0
- coodddaaaa-1.4.2.dist-info/METADATA +30 -0
- coodddaaaa-1.4.2.dist-info/RECORD +15 -0
- coodddaaaa-1.4.2.dist-info/WHEEL +5 -0
- coodddaaaa-1.4.2.dist-info/licenses/LICENSE +21 -0
- coodddaaaa-1.4.2.dist-info/top_level.txt +2 -0
- examples/__init__.py +2 -0
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()
|
coodddaaaa/stretching.py
ADDED
|
@@ -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,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.
|
examples/__init__.py
ADDED