prpy 0.2.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.
- prpy/__init__.py +19 -0
- prpy/constants.py +24 -0
- prpy/ffmpeg/__init__.py +19 -0
- prpy/ffmpeg/probe.py +86 -0
- prpy/ffmpeg/readwrite.py +412 -0
- prpy/ffmpeg/utils.py +81 -0
- prpy/numpy/__init__.py +19 -0
- prpy/numpy/face.py +213 -0
- prpy/numpy/image.py +141 -0
- prpy/numpy/metric.py +179 -0
- prpy/numpy/signal.py +649 -0
- prpy/numpy/stride_tricks.py +182 -0
- prpy/tensorflow/__init__.py +19 -0
- prpy/tensorflow/image.py +203 -0
- prpy/tensorflow/loss.py +104 -0
- prpy/tensorflow/lr_schedule.py +74 -0
- prpy/tensorflow/model_saver.py +203 -0
- prpy/tensorflow/nan.py +226 -0
- prpy/tensorflow/optimizer.py +233 -0
- prpy/tensorflow/signal.py +103 -0
- prpy/torch/__init__.py +19 -0
- prpy/torch/model_saver.py +208 -0
- prpy-0.2.2.dist-info/LICENSE +19 -0
- prpy-0.2.2.dist-info/METADATA +75 -0
- prpy-0.2.2.dist-info/RECORD +27 -0
- prpy-0.2.2.dist-info/WHEEL +5 -0
- prpy-0.2.2.dist-info/top_level.txt +1 -0
prpy/numpy/signal.py
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
# Copyright (c) 2024 Philipp Rouast
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
# copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import math
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scipy import signal, interpolate, fft, stats
|
|
25
|
+
from scipy.sparse import spdiags
|
|
26
|
+
from scipy.ndimage import uniform_filter1d
|
|
27
|
+
from typing import Union, Tuple, Callable
|
|
28
|
+
|
|
29
|
+
from prpy.numpy.stride_tricks import window_view, resolve_1d_window_view
|
|
30
|
+
|
|
31
|
+
def div0(
|
|
32
|
+
a: Union[np.ndarray, float, int],
|
|
33
|
+
b: Union[np.ndarray, float, int],
|
|
34
|
+
fill: Union[float, int] = np.nan
|
|
35
|
+
) -> np.ndarray:
|
|
36
|
+
"""Divide after accounting for zeros in divisor, e.g.:
|
|
37
|
+
|
|
38
|
+
- div0( [-1, 0, 1], 0, fill=np.nan) -> [nan nan nan]
|
|
39
|
+
- div0( 1, 0, fill=np.inf ) -> inf
|
|
40
|
+
|
|
41
|
+
Source: https://stackoverflow.com/a/35696047/3595278
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
a: Dividend
|
|
45
|
+
b: Divisor
|
|
46
|
+
fill: Use this value to fill where b == 0.
|
|
47
|
+
Returns:
|
|
48
|
+
c: safe a/b
|
|
49
|
+
"""
|
|
50
|
+
assert isinstance(fill, (int, float))
|
|
51
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
52
|
+
c = np.true_divide(a, b)
|
|
53
|
+
if np.isscalar(c):
|
|
54
|
+
return c if np.isfinite(c) else fill
|
|
55
|
+
else:
|
|
56
|
+
c[~np.isfinite(c)] = fill
|
|
57
|
+
return c
|
|
58
|
+
|
|
59
|
+
def normalize(
|
|
60
|
+
x: np.ndarray,
|
|
61
|
+
axis: Union[int, tuple, None] = -1
|
|
62
|
+
) -> np.ndarray:
|
|
63
|
+
"""Perform normalization
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
x: The input data
|
|
67
|
+
axis: Axis or axes over which to normalize
|
|
68
|
+
Returns:
|
|
69
|
+
x: The normalized data
|
|
70
|
+
"""
|
|
71
|
+
assert axis is None or isinstance(axis, int) or (isinstance(axis, tuple) and all(isinstance(i, int) for i in axis))
|
|
72
|
+
x = np.asarray(x)
|
|
73
|
+
x -= np.mean(x, axis=axis, keepdims=x.ndim>0)
|
|
74
|
+
return x
|
|
75
|
+
|
|
76
|
+
def standardize(
|
|
77
|
+
x: np.ndarray,
|
|
78
|
+
axis: Union[int, None] = -1
|
|
79
|
+
) -> np.ndarray:
|
|
80
|
+
"""Perform standardization
|
|
81
|
+
|
|
82
|
+
- Note: Returns zero if std == 0
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
x: The input data
|
|
86
|
+
axis: Axis over which to standardize
|
|
87
|
+
Returns:
|
|
88
|
+
x: The standardized data
|
|
89
|
+
"""
|
|
90
|
+
assert axis is None or isinstance(axis, int) or (isinstance(axis, tuple) and all(isinstance(i, int) for i in axis))
|
|
91
|
+
x = np.asarray(x)
|
|
92
|
+
x -= np.mean(x, axis=axis, keepdims=x.ndim>0)
|
|
93
|
+
std = np.std(x, axis=axis, keepdims=x.ndim>0)
|
|
94
|
+
x = div0(x, std, fill=0)
|
|
95
|
+
return x
|
|
96
|
+
|
|
97
|
+
def moving_average(
|
|
98
|
+
x: np.ndarray,
|
|
99
|
+
size: int,
|
|
100
|
+
axis: Union[int, None] = -1,
|
|
101
|
+
pad_method: str = 'reflect',
|
|
102
|
+
scale: bool = False,
|
|
103
|
+
scale_factor: float = 1000000000.
|
|
104
|
+
) -> np.ndarray:
|
|
105
|
+
"""Perform moving average
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
x: The input data
|
|
109
|
+
size: The size of the moving average window
|
|
110
|
+
axis: Axis over which to calculate moving average
|
|
111
|
+
pad_method: Method for padding ends to keep same dims
|
|
112
|
+
scale: Internally scale the input data up before applying filter
|
|
113
|
+
scale_factor: Factor to use for scaling
|
|
114
|
+
Returns:
|
|
115
|
+
x: The averaged data
|
|
116
|
+
"""
|
|
117
|
+
assert axis is None or isinstance(axis, int) or (isinstance(axis, tuple) and all(isinstance(i, int) for i in axis))
|
|
118
|
+
assert isinstance(size, int) and size > 0
|
|
119
|
+
assert isinstance(scale, bool)
|
|
120
|
+
assert isinstance(scale_factor, float)
|
|
121
|
+
x = np.asarray(x)
|
|
122
|
+
if np.isnan(x).any():
|
|
123
|
+
return x
|
|
124
|
+
if scale:
|
|
125
|
+
x *= scale_factor
|
|
126
|
+
y = uniform_filter1d(x, size, mode=pad_method, origin=0, axis=axis)
|
|
127
|
+
if scale:
|
|
128
|
+
y /= scale_factor
|
|
129
|
+
return y
|
|
130
|
+
|
|
131
|
+
def moving_average_size_for_response(
|
|
132
|
+
sampling_freq: Union[float, int],
|
|
133
|
+
cutoff_freq: Union[float, int]
|
|
134
|
+
) -> int:
|
|
135
|
+
"""Estimate the required moving average size to achieve a given response
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
sampling_freq: The sampling frequency [Hz]
|
|
139
|
+
cutoff_freq: The desired cutoff frequency [Hz]
|
|
140
|
+
Returns:
|
|
141
|
+
size: The estimated moving average size
|
|
142
|
+
"""
|
|
143
|
+
assert isinstance(sampling_freq, (float, int))
|
|
144
|
+
assert isinstance(cutoff_freq, (float, int))
|
|
145
|
+
assert cutoff_freq > 0, "Cutoff frequency needs to be greater than zero"
|
|
146
|
+
# Adapted from https://dsp.stackexchange.com/a/14648
|
|
147
|
+
# cutoff freq in Hz
|
|
148
|
+
F = cutoff_freq / sampling_freq
|
|
149
|
+
size = int(math.sqrt(0.196202 + F * F) / F)
|
|
150
|
+
return max(size, 1)
|
|
151
|
+
|
|
152
|
+
def moving_std(
|
|
153
|
+
x: np.ndarray,
|
|
154
|
+
size: int,
|
|
155
|
+
overlap: int,
|
|
156
|
+
fill_method: str = 'mean'
|
|
157
|
+
) -> np.ndarray:
|
|
158
|
+
"""Compute moving standard deviation
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
x: The data to be computed. Shape (n,)
|
|
162
|
+
size: The size of the moving window
|
|
163
|
+
overlap: The overlap of the moving windows
|
|
164
|
+
fill_method: Method to fill the edges.
|
|
165
|
+
Options: 'zero', 'mean' (default), or 'start'
|
|
166
|
+
Returns:
|
|
167
|
+
std: The moving standard deviations
|
|
168
|
+
"""
|
|
169
|
+
x = np.asarray(x)
|
|
170
|
+
assert len(x.shape) == 1, "Only 1-D arrays supported"
|
|
171
|
+
assert isinstance(size, int)
|
|
172
|
+
assert isinstance(overlap, int)
|
|
173
|
+
x_view, _, pad_end = window_view(
|
|
174
|
+
x=x,
|
|
175
|
+
min_window_size=size,
|
|
176
|
+
max_window_size=size,
|
|
177
|
+
overlap=overlap)
|
|
178
|
+
y_view = np.std(x_view, axis=-1)
|
|
179
|
+
y = resolve_1d_window_view(
|
|
180
|
+
x=y_view,
|
|
181
|
+
window_size=size,
|
|
182
|
+
overlap=overlap,
|
|
183
|
+
pad_end=pad_end,
|
|
184
|
+
fill_method=fill_method)
|
|
185
|
+
return y
|
|
186
|
+
|
|
187
|
+
def detrend(
|
|
188
|
+
z: np.ndarray,
|
|
189
|
+
Lambda: int,
|
|
190
|
+
axis: int = -1
|
|
191
|
+
) -> np.ndarray:
|
|
192
|
+
"""Detrend signal(s)
|
|
193
|
+
|
|
194
|
+
Vectorized implementation of the detrending method by
|
|
195
|
+
Tarvainen et al. (2002). Based on code listing in the Appendix.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
z: The input signal. Shape (b, n) or (n,)
|
|
199
|
+
Lambda: The lambda parameter
|
|
200
|
+
axis: The axis along which should be detrended
|
|
201
|
+
Returns:
|
|
202
|
+
proc_z: The detrended signal
|
|
203
|
+
"""
|
|
204
|
+
assert isinstance(Lambda, int)
|
|
205
|
+
assert isinstance(axis, int) and (axis == 0 or axis == 1 or axis == -1)
|
|
206
|
+
axis = 1 if axis == -1 else axis
|
|
207
|
+
z = np.asarray(z) # Make sure z is np array
|
|
208
|
+
z = np.nan_to_num(z) # Replace NAs with 0
|
|
209
|
+
if len(z.shape) == 1:
|
|
210
|
+
z = np.expand_dims(z, axis=1-axis)
|
|
211
|
+
assert z.ndim == 2, "z.ndim must equal 2"
|
|
212
|
+
T = z.shape[axis]
|
|
213
|
+
if T < 3:
|
|
214
|
+
return z
|
|
215
|
+
# Identity matrix
|
|
216
|
+
I = np.identity(T)
|
|
217
|
+
# Regularization matrix
|
|
218
|
+
D2 = spdiags(
|
|
219
|
+
[np.ones(T), -2*np.ones(T), np.ones(T)],
|
|
220
|
+
[0, 1, 2], (T-2), T).toarray()
|
|
221
|
+
# Inverse of I+lambda^2*D2’*D2
|
|
222
|
+
inv = np.linalg.inv(I + (Lambda**2) * np.dot(D2.T, D2))
|
|
223
|
+
# Compute the detrending operation (vectorized)
|
|
224
|
+
if axis == 0:
|
|
225
|
+
z = np.transpose(z)
|
|
226
|
+
proc_z = np.matmul((I - inv), z.T)
|
|
227
|
+
if axis == 1:
|
|
228
|
+
proc_z = np.transpose(proc_z)
|
|
229
|
+
# Squeeze if necessary
|
|
230
|
+
proc_z = np.squeeze(proc_z)
|
|
231
|
+
# Return
|
|
232
|
+
return proc_z
|
|
233
|
+
|
|
234
|
+
# TODO write tests
|
|
235
|
+
def windowed_standardize(
|
|
236
|
+
x: np.ndarray,
|
|
237
|
+
window_size: int,
|
|
238
|
+
windowed_mean: bool = True,
|
|
239
|
+
windowed_std: bool = True
|
|
240
|
+
) -> np.ndarray:
|
|
241
|
+
"""Perform dynamic standardization based on windowed mean and std
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
x: The input data
|
|
245
|
+
window_size: The size of the moving window
|
|
246
|
+
windowed_mean: Boolean indicating whether mean should be windowed
|
|
247
|
+
windowed_std: Boolean indicating whether std should be windowed
|
|
248
|
+
Returns:
|
|
249
|
+
y: The standardized data
|
|
250
|
+
"""
|
|
251
|
+
x = np.asarray(x)
|
|
252
|
+
if windowed_mean:
|
|
253
|
+
mean = moving_average(x, size=window_size, scale=True)
|
|
254
|
+
else:
|
|
255
|
+
mean = np.mean(x)
|
|
256
|
+
if windowed_std:
|
|
257
|
+
std = moving_std(x, size=window_size, overlap=window_size-1)
|
|
258
|
+
else:
|
|
259
|
+
std = np.std(x)
|
|
260
|
+
x -= mean
|
|
261
|
+
x /= std
|
|
262
|
+
return x
|
|
263
|
+
|
|
264
|
+
# TODO write tests
|
|
265
|
+
def butter_bandpass(
|
|
266
|
+
x: np.ndarray,
|
|
267
|
+
lowcut: Union[int, float],
|
|
268
|
+
highcut: Union[int, float],
|
|
269
|
+
fs: Union[int, float],
|
|
270
|
+
axis: Union[int, tuple, None] = -1,
|
|
271
|
+
order: int = 5
|
|
272
|
+
):
|
|
273
|
+
"""Apply a butterworth bandpass filter.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
x: The signal data
|
|
277
|
+
lowcut: The lower cutoff frequency
|
|
278
|
+
highcut: The higher cutoff frequency
|
|
279
|
+
fs: The sampling frequency
|
|
280
|
+
axis: The axis along which to apply the filter
|
|
281
|
+
order: The order of the filter
|
|
282
|
+
Returns:
|
|
283
|
+
y: The filtered signal data
|
|
284
|
+
"""
|
|
285
|
+
assert axis is None or isinstance(axis, int) or (isinstance(axis, tuple) and all(isinstance(i, int) for i in axis))
|
|
286
|
+
assert isinstance(lowcut, (int, float))
|
|
287
|
+
assert isinstance(highcut, (int, float))
|
|
288
|
+
assert isinstance(fs, (int, float))
|
|
289
|
+
assert isinstance(order, int)
|
|
290
|
+
def butter_bandpass_filter(
|
|
291
|
+
lowcut: Union[int, float],
|
|
292
|
+
highcut: Union[int, float],
|
|
293
|
+
fs: Union[int, float],
|
|
294
|
+
order: int = 5
|
|
295
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
296
|
+
nyq = 0.5 * fs
|
|
297
|
+
low = lowcut / nyq
|
|
298
|
+
high = highcut / nyq
|
|
299
|
+
return signal.butter(order, [low, high], btype='band')
|
|
300
|
+
b, a = butter_bandpass_filter(
|
|
301
|
+
lowcut=lowcut, highcut=highcut, fs=fs, order=order)
|
|
302
|
+
y = signal.lfilter(
|
|
303
|
+
b=b, a=a, x=x, axis=axis)
|
|
304
|
+
return y
|
|
305
|
+
|
|
306
|
+
def estimate_freq(
|
|
307
|
+
x: np.ndarray,
|
|
308
|
+
f_s: Union[float, int],
|
|
309
|
+
f_range: tuple = None,
|
|
310
|
+
f_res: float = None,
|
|
311
|
+
method: str = 'fft',
|
|
312
|
+
max_periodicity_deviation: float = 0.5,
|
|
313
|
+
axis: int = -1
|
|
314
|
+
) -> np.ndarray:
|
|
315
|
+
"""Determine maximum frequencies in x.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
x: The signal data. Shape: (n_data,) or (n_sig, n_data)
|
|
319
|
+
f_s: The sampling frequency [Hz]
|
|
320
|
+
f_range: Optional expected range of freqs [Hz] - (min, max)
|
|
321
|
+
f_res: Optional frequency resolution for analysis [Hz]
|
|
322
|
+
(useful if signal small; applies only to periodogram)
|
|
323
|
+
method: The method to be used [fft, peak, or periodogram]
|
|
324
|
+
max_periodicity_deviation: Maximum relative deviation of peaks from regular periodicity
|
|
325
|
+
axis: The axis along which to estimate frequencies
|
|
326
|
+
Returns:
|
|
327
|
+
f_out: The maximum frequencies [Hz]. Shape: (n_sig,)
|
|
328
|
+
"""
|
|
329
|
+
assert isinstance(method, str)
|
|
330
|
+
if method == 'fft':
|
|
331
|
+
return estimate_freq_fft(x, f_s=f_s, f_range=f_range, axis=axis)
|
|
332
|
+
elif method == 'peak':
|
|
333
|
+
return estimate_freq_peak(x, f_s=f_s, f_range=f_range, max_periodicity_deviation=max_periodicity_deviation, axis=axis)
|
|
334
|
+
elif method == 'periodogram':
|
|
335
|
+
return estimate_freq_periodogram(x, f_s=f_s, f_range=f_range, f_res=f_res, axis=axis)
|
|
336
|
+
else:
|
|
337
|
+
return ValueError("method should be 'peak', 'fft', or 'periodogram' but was {}".format(method))
|
|
338
|
+
|
|
339
|
+
def estimate_freq_fft(
|
|
340
|
+
x: np.ndarray,
|
|
341
|
+
f_s: Union[float, int],
|
|
342
|
+
f_range: Union[tuple, None] = None,
|
|
343
|
+
axis: int = -1
|
|
344
|
+
) -> np.ndarray:
|
|
345
|
+
"""Use a fourier transform to determine maximum frequencies.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
x: The signal data. Shape: (n_data,) or (n_sig, n_data)
|
|
349
|
+
f_s: The sampling frequency [Hz]
|
|
350
|
+
f_range: Optional expected range of freqs [Hz] - (min, max)
|
|
351
|
+
axis: The axis along which to estimate frequencies
|
|
352
|
+
Returns:
|
|
353
|
+
f_out: The maximum frequencies [Hz]. Shape: (n_sig,)
|
|
354
|
+
"""
|
|
355
|
+
assert isinstance(f_s, (float, int)) and f_s > 0
|
|
356
|
+
assert f_range is None or (isinstance(f_range, tuple) and len(f_range) == 2 and all(isinstance(i, (int, float)) for i in f_range))
|
|
357
|
+
assert isinstance(axis, int) and (axis == 0 or axis == 1 or axis == -1)
|
|
358
|
+
x = np.asarray(x)
|
|
359
|
+
# Change to 2-dim array if necessary
|
|
360
|
+
if len(x.shape) == 1:
|
|
361
|
+
x = np.expand_dims(x, axis=0)
|
|
362
|
+
# Run the fourier transform
|
|
363
|
+
w = fft.rfft(x, axis=axis)
|
|
364
|
+
f = fft.rfftfreq(x.shape[axis], 1/f_s)
|
|
365
|
+
# Restrict by range if necessary
|
|
366
|
+
if f_range is not None:
|
|
367
|
+
# Bandpass: Set w outside of range to zero
|
|
368
|
+
f_min = min(np.amax(f), f_range[0])
|
|
369
|
+
f_max = max(np.amin(f), f_range[1])
|
|
370
|
+
w = np.where(np.logical_or(f < f_min, f > f_max), 0, w)
|
|
371
|
+
# Determine maximum frequency component
|
|
372
|
+
idx = np.argmax(np.abs(w), axis=axis)
|
|
373
|
+
# Derive frequency in Hz
|
|
374
|
+
f_out = abs(f[idx])
|
|
375
|
+
# Squeeze if necessary
|
|
376
|
+
f_out = np.squeeze(f_out)
|
|
377
|
+
# Return
|
|
378
|
+
return f_out
|
|
379
|
+
|
|
380
|
+
def estimate_freq_peak(
|
|
381
|
+
x: np.ndarray,
|
|
382
|
+
f_s: Union[float, int],
|
|
383
|
+
f_range: Union[tuple, None] = None,
|
|
384
|
+
max_periodicity_deviation: float = 0.5,
|
|
385
|
+
axis: int = -1
|
|
386
|
+
) -> np.ndarray:
|
|
387
|
+
"""Use peak detection to determine maximum frequencies in x.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
x: The signal data. Shape: (n_data,) or (n_sig, n_data)
|
|
391
|
+
f_s: The sampling frequency [Hz]
|
|
392
|
+
f_range: Optional expected range of freqs [Hz] - (min, max)
|
|
393
|
+
max_periodicity_deviation: Maximum relative deviation of peaks from regular periodicity
|
|
394
|
+
axis: The axis along which to estimate frequencies
|
|
395
|
+
Returns:
|
|
396
|
+
f_out: The maximum frequencies [Hz]. Shape: (n_sig,)
|
|
397
|
+
"""
|
|
398
|
+
assert isinstance(f_s, (float, int)) and f_s > 0
|
|
399
|
+
assert f_range is None or (isinstance(f_range, tuple) and len(f_range) == 2 and all(isinstance(i, (int, float)) for i in f_range))
|
|
400
|
+
assert isinstance(max_periodicity_deviation, float)
|
|
401
|
+
assert isinstance(axis, int) and (axis == 0 or axis == 1 or axis == -1)
|
|
402
|
+
x = np.asarray(x)
|
|
403
|
+
# Change to 2-dim array if necessary
|
|
404
|
+
if len(x.shape) == 1:
|
|
405
|
+
x = np.expand_dims(x, axis=0)
|
|
406
|
+
# Derive minimum distance between peaks if necessary
|
|
407
|
+
min_dist = max(1/f_range[1]*f_s*(1-max_periodicity_deviation), 0) if f_range is not None else 0
|
|
408
|
+
# Peak detection is only available for 1-D tensors
|
|
409
|
+
def estimate_freq_peak_for_single_axis(x):
|
|
410
|
+
# Find peaks in the signal
|
|
411
|
+
det_idxs, _ = signal.find_peaks(x, height=0, distance=min_dist)
|
|
412
|
+
# Calculate mean distance between peaks
|
|
413
|
+
mean_idx_dist = np.mean(np.diff(det_idxs), axis=-1)
|
|
414
|
+
# Derive the frequency
|
|
415
|
+
return f_s/mean_idx_dist
|
|
416
|
+
# Apply function
|
|
417
|
+
f_out = np.apply_along_axis(estimate_freq_peak_for_single_axis, axis=axis, arr=x)
|
|
418
|
+
# Squeeze if necessary
|
|
419
|
+
f_out = np.squeeze(f_out)
|
|
420
|
+
# Return
|
|
421
|
+
return f_out
|
|
422
|
+
|
|
423
|
+
def estimate_freq_periodogram(
|
|
424
|
+
x: np.ndarray,
|
|
425
|
+
f_s: Union[float, int],
|
|
426
|
+
f_range: Union[tuple, None] = None,
|
|
427
|
+
f_res: Union[float, None] = None,
|
|
428
|
+
axis: int = -1
|
|
429
|
+
) -> np.ndarray:
|
|
430
|
+
"""Use a periodigram to estimate maximum frequencies at f_res.
|
|
431
|
+
|
|
432
|
+
When signal is sampled at a lower frequency than f_res, this is essentially done
|
|
433
|
+
by interpolating in the frequency domain.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
x: The signal data. Shape: (n_data,) or (n_sig, n_data)
|
|
437
|
+
f_s: The sampling frequency [Hz]
|
|
438
|
+
f_range: Optional expected range of freqs [Hz] - (min, max)
|
|
439
|
+
f_res: Optional frequency resolution for analysis [Hz]
|
|
440
|
+
axis: The axis along which to estimate frequencies
|
|
441
|
+
Returns:
|
|
442
|
+
f_out: The maximum frequencies [Hz]. Shape: (n_sig,)
|
|
443
|
+
"""
|
|
444
|
+
assert isinstance(f_s, (float, int)) and f_s > 0
|
|
445
|
+
assert f_range is None or (isinstance(f_range, tuple) and len(f_range) == 2 and all(isinstance(i, (int, float)) for i in f_range))
|
|
446
|
+
assert f_res is None or (isinstance(f_res, (float, int)) and f_res > 0)
|
|
447
|
+
assert isinstance(axis, int) and (axis == 0 or axis == 1 or axis == -1)
|
|
448
|
+
x = np.asarray(x)
|
|
449
|
+
# Change to 2-dim array if necessary
|
|
450
|
+
if len(x.shape) == 1:
|
|
451
|
+
x = np.expand_dims(x, axis=0)
|
|
452
|
+
# Determine the length of the fft if f_res specified
|
|
453
|
+
# Large nfft > x.length leads to zero padding of x before fft (like interpolating frequency domain)
|
|
454
|
+
nfft = None if f_res is None else int(f_s // f_res)
|
|
455
|
+
# Compute
|
|
456
|
+
f, pxx = signal.periodogram(x, fs=f_s, nfft=nfft, detrend=False, axis=axis)
|
|
457
|
+
# Restrict by range if necessary
|
|
458
|
+
if f_range is not None:
|
|
459
|
+
# Bandpass: Set w outside of range to zero
|
|
460
|
+
f_min = min(np.amax(f), f_range[0])
|
|
461
|
+
f_max = max(np.amin(f), f_range[1])
|
|
462
|
+
pxx = np.where(np.logical_or(f < f_min, f > f_max), 0, pxx)
|
|
463
|
+
# Determine maximum frequency component
|
|
464
|
+
idx = np.argmax(pxx, axis=axis)
|
|
465
|
+
# Maximum frequency in Hz
|
|
466
|
+
f_out = f[idx]
|
|
467
|
+
# Squeeze if necessary
|
|
468
|
+
f_out = np.squeeze(f_out)
|
|
469
|
+
# Return
|
|
470
|
+
return f_out
|
|
471
|
+
|
|
472
|
+
def interpolate_vals(
|
|
473
|
+
x: np.ndarray,
|
|
474
|
+
val_fn: Callable[[np.ndarray], np.ndarray] = lambda x: np.isnan(x)
|
|
475
|
+
) -> np.ndarray:
|
|
476
|
+
"""Linearly interpolate vals matching val_fn
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
x: The values, shape (n_vals,)
|
|
480
|
+
val_fn: The function, values matching which will be interpolated
|
|
481
|
+
Returns:
|
|
482
|
+
x: The interpolated values, shape (n_vals,)
|
|
483
|
+
"""
|
|
484
|
+
assert callable(val_fn)
|
|
485
|
+
x = np.asarray(x)
|
|
486
|
+
assert len(x.shape) == 1, "Only 1-D arrays supported"
|
|
487
|
+
if val_fn(x).all():
|
|
488
|
+
logging.debug("All elements in x fulfilled val_fn. Not doing anything.")
|
|
489
|
+
return x
|
|
490
|
+
mask = val_fn(x)
|
|
491
|
+
x[mask] = np.interp(np.flatnonzero(mask), np.flatnonzero(~mask), x[~mask])
|
|
492
|
+
return x
|
|
493
|
+
|
|
494
|
+
def interpolate_cubic_spline(
|
|
495
|
+
x: np.ndarray,
|
|
496
|
+
y: np.ndarray,
|
|
497
|
+
xs: np.ndarray,
|
|
498
|
+
axis: int = 0
|
|
499
|
+
) -> np.ndarray:
|
|
500
|
+
"""Interpolate data with a cubic spline.
|
|
501
|
+
Args:
|
|
502
|
+
x: The x values of the data we want to interpolate. 1-dim.
|
|
503
|
+
y: The y values of the data we want to interpolate. Along the given axis,
|
|
504
|
+
shape of y must match shape of x.
|
|
505
|
+
xs: The x values at which we want to interpolate. 1-dim.
|
|
506
|
+
Returns:
|
|
507
|
+
ys: The interpolated y values
|
|
508
|
+
"""
|
|
509
|
+
assert isinstance(axis, int)
|
|
510
|
+
x = np.asarray(x)
|
|
511
|
+
y = np.asarray(y)
|
|
512
|
+
xs = np.asarray(xs)
|
|
513
|
+
x = np.nan_to_num(x) # Replace NAs with 0
|
|
514
|
+
y = np.nan_to_num(y) # Replace NAs with 0
|
|
515
|
+
if np.array_equal(x, xs):
|
|
516
|
+
return y
|
|
517
|
+
cs = interpolate.CubicSpline(x, y, axis=axis)
|
|
518
|
+
return cs(xs)
|
|
519
|
+
|
|
520
|
+
def interpolate_linear_sequence_outliers(
|
|
521
|
+
t: np.ndarray,
|
|
522
|
+
max_diff_rel: float = 1.0,
|
|
523
|
+
max_diff_abs: Union[float, None] = None
|
|
524
|
+
) -> np.ndarray:
|
|
525
|
+
"""Interpolate outliers in an otherwise linear sequence.
|
|
526
|
+
|
|
527
|
+
- For example: Measurement timestamps
|
|
528
|
+
- I.e., goal is to make the sequence strictly increasing with approx. constant diff.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
t: The sequence vals to fix. 1-dim.
|
|
532
|
+
max_diff_rel: Maximum relative difference from regular linear increasing value [%]
|
|
533
|
+
Used if `max_diff_abs` is None
|
|
534
|
+
max_diff_abs: Maximum absolute difference from regular linear increasing value
|
|
535
|
+
Used if not None
|
|
536
|
+
Returns:
|
|
537
|
+
t: The interpolated sequence of strictly increasing vals. 1-dim.
|
|
538
|
+
"""
|
|
539
|
+
assert isinstance(max_diff_rel, float)
|
|
540
|
+
assert max_diff_abs is None or isinstance(max_diff_abs, float)
|
|
541
|
+
from sklearn.linear_model import RANSACRegressor
|
|
542
|
+
t = np.asarray(t)
|
|
543
|
+
assert len(t.shape) == 1
|
|
544
|
+
size = len(t)
|
|
545
|
+
indices = np.arange(size)
|
|
546
|
+
# Calculate max diff
|
|
547
|
+
max_diff = max_diff_abs if max_diff_abs is not None else np.abs(np.median(np.diff(t)) * max_diff_rel)
|
|
548
|
+
# Stage 1
|
|
549
|
+
def interpolate_regression_outliers(t, max_diff):
|
|
550
|
+
# Stage 1: Fit robust regression model
|
|
551
|
+
reg = RANSACRegressor(random_state=0).fit(indices[:,np.newaxis], t)
|
|
552
|
+
# Stage 1: Identify idxs for regression outliers
|
|
553
|
+
t_preds = reg.predict(indices[:,np.newaxis])
|
|
554
|
+
not_reg_outlier = np.abs(t_preds - t) < max_diff
|
|
555
|
+
# Stage 1: Fix regression outliers
|
|
556
|
+
f = interpolate.interp1d(indices[not_reg_outlier], t[not_reg_outlier],
|
|
557
|
+
kind='linear', fill_value="extrapolate")
|
|
558
|
+
return f(indices)
|
|
559
|
+
t = interpolate_regression_outliers(t, max_diff=max_diff)
|
|
560
|
+
# Stage 2
|
|
561
|
+
def interpolate_non_strictly_increasing(t):
|
|
562
|
+
not_si_outlier = np.concatenate([[True], t[1:] - t[:-1] > 0])
|
|
563
|
+
f = interpolate.interp1d(indices[not_si_outlier], t[not_si_outlier],
|
|
564
|
+
kind='linear', fill_value="extrapolate")
|
|
565
|
+
return f(indices)
|
|
566
|
+
while not np.logical_and.reduce(np.diff(t) > 0):
|
|
567
|
+
t = interpolate_non_strictly_increasing(t)
|
|
568
|
+
# Assert strictly increasing
|
|
569
|
+
assert np.logical_and.reduce(np.diff(t) > 0)
|
|
570
|
+
return t
|
|
571
|
+
|
|
572
|
+
def interpolate_data_outliers(
|
|
573
|
+
x: np.ndarray,
|
|
574
|
+
z_score: Union[int, float] = 3
|
|
575
|
+
) -> np.ndarray:
|
|
576
|
+
"""Recursively interpolate outliers in sensor data.
|
|
577
|
+
|
|
578
|
+
- Example: ECG signal
|
|
579
|
+
- Goal is to remove outliers in sensor data which may be caused by electrical interference etc.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
vals: The signal data to interpolate. 1-dim.
|
|
583
|
+
z_score: Significance score required for a data point to be interpolated
|
|
584
|
+
Returns:
|
|
585
|
+
vals: The interpolated signal data. 1-dim.
|
|
586
|
+
"""
|
|
587
|
+
def interpolation_step(
|
|
588
|
+
x: np.ndarray,
|
|
589
|
+
z_score: Union[int, float]
|
|
590
|
+
) -> np.ndarray:
|
|
591
|
+
x_z_score = stats.zscore(x)
|
|
592
|
+
if np.isnan(x_z_score).all():
|
|
593
|
+
return x
|
|
594
|
+
not_outlier = np.abs(x_z_score) <= z_score
|
|
595
|
+
indices = np.arange(len(x))
|
|
596
|
+
interp = interpolate.interp1d(indices[not_outlier], x[not_outlier],
|
|
597
|
+
kind='linear', fill_value='extrapolate')
|
|
598
|
+
new_x = interp(indices)
|
|
599
|
+
new_outlier = np.abs(stats.zscore(new_x)) > z_score
|
|
600
|
+
if len(np.where(new_outlier)[0]) > 0:
|
|
601
|
+
if new_outlier[0]:
|
|
602
|
+
# Set first to mean if it is an outlier to avoid infinite recursion
|
|
603
|
+
new_x[0] = np.mean(new_x[~new_outlier])
|
|
604
|
+
if new_outlier[-1]:
|
|
605
|
+
# Set last to mean if it is an outlier to avoid infinite recursion
|
|
606
|
+
new_x[-1] = np.mean(new_x[~new_outlier])
|
|
607
|
+
return interpolation_step(new_x, z_score)
|
|
608
|
+
else:
|
|
609
|
+
return new_x
|
|
610
|
+
# Recursive interpolation of data outliers
|
|
611
|
+
return interpolation_step(x=x, z_score=z_score)
|
|
612
|
+
|
|
613
|
+
def _component_periodicity(x: np.ndarray) -> list:
|
|
614
|
+
"""Compute the periodicity of the maximum frequency components
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
x: The signal data. Shape (n_sig, n_data)
|
|
618
|
+
Returns:
|
|
619
|
+
result: The periodicities. Shape (n_sig,)
|
|
620
|
+
"""
|
|
621
|
+
x = np.asarray(x) # Make sure x is np array
|
|
622
|
+
x = np.nan_to_num(x) # Replace NAs with 0
|
|
623
|
+
assert x.ndim == 2, "x.ndim must equal 2"
|
|
624
|
+
# Perform FFT
|
|
625
|
+
w = fft.rfft(x, axis=1)
|
|
626
|
+
# Determine maximum frequency component of each dim
|
|
627
|
+
w_ = np.square(np.abs(w))
|
|
628
|
+
w_ = div0(w_, np.sum(w_, axis=1)[:, np.newaxis], fill=0)
|
|
629
|
+
idxs = np.argmax(w_, axis=1)
|
|
630
|
+
# Compute periodicity for maximum frequency component
|
|
631
|
+
return [w_[i,idx] for i, idx in enumerate(idxs)]
|
|
632
|
+
|
|
633
|
+
def select_most_periodic(x: np.ndarray) -> np.ndarray:
|
|
634
|
+
"""Select the most periodic signal
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
x: The signal data. Shape (n_sig, n_data)
|
|
638
|
+
Returns:
|
|
639
|
+
y: Signal with highest periodicity
|
|
640
|
+
"""
|
|
641
|
+
x = np.asarray(x) # Make sure x is np array
|
|
642
|
+
x = np.nan_to_num(x) # Replace NAs with 0
|
|
643
|
+
assert x.ndim == 2, "x.ndim must equal 2"
|
|
644
|
+
# Compute component periodicity
|
|
645
|
+
p = _component_periodicity(x)
|
|
646
|
+
idx = np.argmax(p)
|
|
647
|
+
y = x[idx]
|
|
648
|
+
assert x.shape[1] == y.shape[0]
|
|
649
|
+
return y
|