paradigma 1.0.4__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paradigma/__init__.py +10 -1
- paradigma/classification.py +14 -14
- paradigma/config.py +38 -29
- paradigma/constants.py +10 -2
- paradigma/feature_extraction.py +106 -75
- paradigma/load.py +476 -0
- paradigma/orchestrator.py +670 -0
- paradigma/pipelines/gait_pipeline.py +488 -97
- paradigma/pipelines/pulse_rate_pipeline.py +278 -46
- paradigma/pipelines/pulse_rate_utils.py +176 -137
- paradigma/pipelines/tremor_pipeline.py +292 -72
- paradigma/prepare_data.py +409 -0
- paradigma/preprocessing.py +345 -77
- paradigma/segmenting.py +57 -42
- paradigma/testing.py +14 -9
- paradigma/util.py +36 -22
- paradigma-1.1.0.dist-info/METADATA +229 -0
- paradigma-1.1.0.dist-info/RECORD +26 -0
- {paradigma-1.0.4.dist-info → paradigma-1.1.0.dist-info}/WHEEL +1 -1
- paradigma-1.0.4.dist-info/METADATA +0 -140
- paradigma-1.0.4.dist-info/RECORD +0 -23
- {paradigma-1.0.4.dist-info → paradigma-1.1.0.dist-info}/entry_points.txt +0 -0
- {paradigma-1.0.4.dist-info → paradigma-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from typing import Tuple
|
|
2
|
-
|
|
3
1
|
import numpy as np
|
|
4
2
|
from scipy import signal
|
|
5
3
|
|
|
@@ -42,9 +40,8 @@ def assign_sqa_label(
|
|
|
42
40
|
|
|
43
41
|
for i in range(n_samples):
|
|
44
42
|
# Start and end indices for current epoch
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
) # max to handle first epochs
|
|
43
|
+
# Max to handle first epochs
|
|
44
|
+
start_idx = max(0, int((i - (samples_per_epoch - samples_shift)) // fs))
|
|
48
45
|
end_idx = min(int(i // fs), len(ppg_prob)) # min to handle last epochs
|
|
49
46
|
|
|
50
47
|
# Extract probabilities and labels for the current epoch
|
|
@@ -64,7 +61,7 @@ def assign_sqa_label(
|
|
|
64
61
|
|
|
65
62
|
def extract_pr_segments(
|
|
66
63
|
sqa_label: np.ndarray, min_pr_samples: int
|
|
67
|
-
) ->
|
|
64
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
68
65
|
"""
|
|
69
66
|
Extracts pulse rate segments based on the SQA label.
|
|
70
67
|
|
|
@@ -106,16 +103,19 @@ def extract_pr_from_segment(
|
|
|
106
103
|
Parameters
|
|
107
104
|
----------
|
|
108
105
|
ppg : np.ndarray
|
|
109
|
-
The preprocessed PPG segment with 2 seconds of padding on both sides
|
|
106
|
+
The preprocessed PPG segment with 2 seconds of padding on both sides
|
|
107
|
+
to reduce boundary effects.
|
|
110
108
|
tfd_length : int
|
|
111
|
-
Length of each segment (in seconds) to calculate the time-frequency
|
|
109
|
+
Length of each segment (in seconds) to calculate the time-frequency
|
|
110
|
+
distribution.
|
|
112
111
|
fs : int
|
|
113
112
|
The sampling frequency of the PPG signal.
|
|
114
113
|
kern_type : str
|
|
115
|
-
Type of TFD kernel to use (e.g., 'wvd' for Wigner-Ville
|
|
114
|
+
Type of TFD kernel to use (e.g., 'wvd' for Wigner-Ville
|
|
115
|
+
distribution).
|
|
116
116
|
kern_params : dict
|
|
117
|
-
Parameters for the specified kernel. Not required for 'wvd', but
|
|
118
|
-
kernels like 'spwvd' or 'swvd'. Default is None.
|
|
117
|
+
Parameters for the specified kernel. Not required for 'wvd', but
|
|
118
|
+
relevant for other kernels like 'spwvd' or 'swvd'. Default is None.
|
|
119
119
|
|
|
120
120
|
Returns
|
|
121
121
|
-------
|
|
@@ -157,8 +157,8 @@ def extract_pr_with_tfd(
|
|
|
157
157
|
ppg: np.ndarray, fs: int, kern_type: str, kern_params: dict
|
|
158
158
|
) -> np.ndarray:
|
|
159
159
|
"""
|
|
160
|
-
Estimate pulse rate (PR) from a PPG segment using a TFD method with
|
|
161
|
-
moving average filtering.
|
|
160
|
+
Estimate pulse rate (PR) from a PPG segment using a TFD method with
|
|
161
|
+
optional moving average filtering.
|
|
162
162
|
|
|
163
163
|
Parameters
|
|
164
164
|
----------
|
|
@@ -174,7 +174,8 @@ def extract_pr_with_tfd(
|
|
|
174
174
|
Returns
|
|
175
175
|
-------
|
|
176
176
|
pr_smooth_tfd : np.ndarray
|
|
177
|
-
Estimated pr values (in beats per minute) for each 2-second segment
|
|
177
|
+
Estimated pr values (in beats per minute) for each 2-second segment
|
|
178
|
+
of the PPG signal.
|
|
178
179
|
"""
|
|
179
180
|
# Generate the TFD matrix using the specified kernel
|
|
180
181
|
tfd_obj = TimeFreqDistr()
|
|
@@ -204,22 +205,34 @@ def extract_pr_with_tfd(
|
|
|
204
205
|
class TimeFreqDistr:
|
|
205
206
|
def __init__(self):
|
|
206
207
|
"""
|
|
207
|
-
This module contains the implementation of the Generalized
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
-
|
|
216
|
-
|
|
208
|
+
This module contains the implementation of the Generalized
|
|
209
|
+
Time-Frequency Distribution (TFD) computation using non-separable
|
|
210
|
+
kernels. This is a Python implementation of the MATLAB code provided
|
|
211
|
+
by John O Toole in the following repository:
|
|
212
|
+
https://github.com/otoolej/memeff_TFDs
|
|
213
|
+
|
|
214
|
+
The following functions are implemented for the computation of the
|
|
215
|
+
TFD:
|
|
216
|
+
- nonsep_gdtfd: Computes the generalized time-frequency
|
|
217
|
+
distribution using a non-separable kernel.
|
|
218
|
+
- get_analytic_signal: Generates the analytic signal of the input
|
|
219
|
+
signal.
|
|
220
|
+
- gen_analytic: Generates the analytic signal by zero-padding and
|
|
221
|
+
performing FFT.
|
|
222
|
+
- gen_time_lag: Generates the time-lag distribution of the
|
|
223
|
+
analytic signal.
|
|
224
|
+
- multiply_kernel_signal: Multiplies the TFD by the Doppler-lag
|
|
225
|
+
kernel.
|
|
226
|
+
- gen_doppler_lag_kern: Generates the Doppler-lag kernel based on
|
|
227
|
+
kernel type and parameters.
|
|
217
228
|
- get_kern: Gets the kernel based on the provided kernel type.
|
|
218
229
|
- get_window: General function to calculate a window function.
|
|
219
230
|
- get_win: Helper function to create the specified window type.
|
|
220
|
-
- shift_window: Shifts the window so that positive indices appear
|
|
231
|
+
- shift_window: Shifts the window so that positive indices appear
|
|
232
|
+
first.
|
|
221
233
|
- pad_window: Zero-pads the window to a specified length.
|
|
222
|
-
- compute_tfd: Finalizes the time-frequency distribution
|
|
234
|
+
- compute_tfd: Finalizes the time-frequency distribution
|
|
235
|
+
computation.
|
|
223
236
|
"""
|
|
224
237
|
pass
|
|
225
238
|
|
|
@@ -230,7 +243,8 @@ class TimeFreqDistr:
|
|
|
230
243
|
kern_params: None | dict = None,
|
|
231
244
|
):
|
|
232
245
|
"""
|
|
233
|
-
Computes the generalized time-frequency distribution (TFD) using a
|
|
246
|
+
Computes the generalized time-frequency distribution (TFD) using a
|
|
247
|
+
non-separable kernel.
|
|
234
248
|
|
|
235
249
|
Parameters:
|
|
236
250
|
-----------
|
|
@@ -247,16 +261,20 @@ class TimeFreqDistr:
|
|
|
247
261
|
sep - kernel for separable kernel (combintation of SWVD and PWVD)
|
|
248
262
|
|
|
249
263
|
kern_params : dict, optional
|
|
250
|
-
Dictionary of parameters specific to the kernel type. Default is
|
|
251
|
-
The structure of the dictionary depends on the selected
|
|
264
|
+
Dictionary of parameters specific to the kernel type. Default is
|
|
265
|
+
None. The structure of the dictionary depends on the selected
|
|
266
|
+
kernel type:
|
|
252
267
|
- wvd:
|
|
253
268
|
An empty dictionary, as no additional parameters are required.
|
|
254
269
|
- swvd:
|
|
255
270
|
Dictionary with the following keys:
|
|
256
271
|
'win_length': Length of the smoothing window.
|
|
257
|
-
'win_type': Type of window function (e.g., 'hamm',
|
|
258
|
-
|
|
259
|
-
'
|
|
272
|
+
'win_type': Type of window function (e.g., 'hamm',
|
|
273
|
+
'hann').
|
|
274
|
+
'win_param' (optional): Additional parameters for the
|
|
275
|
+
window.
|
|
276
|
+
'win_param2' (optional): 0 for time-domain window or 1
|
|
277
|
+
for Doppler-domain window.
|
|
260
278
|
|
|
261
279
|
Example:
|
|
262
280
|
```python
|
|
@@ -272,7 +290,8 @@ class TimeFreqDistr:
|
|
|
272
290
|
'win_length': Length of the smoothing window.
|
|
273
291
|
'win_type': Type of window function (e.g., 'cosh').
|
|
274
292
|
'win_param' (optional): Additional parameters for the window.
|
|
275
|
-
'win_param2' (optional): 0 for time-domain window or 1 for
|
|
293
|
+
'win_param2' (optional): 0 for time-domain window or 1 for
|
|
294
|
+
Doppler-domain window.
|
|
276
295
|
Example:
|
|
277
296
|
```python
|
|
278
297
|
kern_params = {
|
|
@@ -282,18 +301,23 @@ class TimeFreqDistr:
|
|
|
282
301
|
}
|
|
283
302
|
```
|
|
284
303
|
- sep:
|
|
285
|
-
Dictionary containing two nested dictionaries, one for the Doppler
|
|
304
|
+
Dictionary containing two nested dictionaries, one for the Doppler
|
|
305
|
+
window and one for the lag window:
|
|
286
306
|
'doppler': {
|
|
287
307
|
'win_length': Length of the Doppler-domain window.
|
|
288
308
|
'win_type': Type of Doppler-domain window function.
|
|
289
|
-
'win_param' (optional): Additional parameters for the
|
|
290
|
-
|
|
309
|
+
'win_param' (optional): Additional parameters for the
|
|
310
|
+
Doppler window.
|
|
311
|
+
'win_param2' (optional): 0 for time-domain window or 1
|
|
312
|
+
for Doppler-domain window.
|
|
291
313
|
}
|
|
292
314
|
'lag': {
|
|
293
315
|
'win_length': Length of the lag-domain window.
|
|
294
316
|
'win_type': Type of lag-domain window function.
|
|
295
|
-
'win_param' (optional): Additional parameters for the lag
|
|
296
|
-
|
|
317
|
+
'win_param' (optional): Additional parameters for the lag
|
|
318
|
+
window.
|
|
319
|
+
'win_param2' (optional): 0 for time-domain window or 1
|
|
320
|
+
for Doppler-domain window.
|
|
297
321
|
}
|
|
298
322
|
Example:
|
|
299
323
|
```python
|
|
@@ -315,17 +339,17 @@ class TimeFreqDistr:
|
|
|
315
339
|
The computed time-frequency distribution.
|
|
316
340
|
"""
|
|
317
341
|
z = self.get_analytic_signal(x)
|
|
318
|
-
|
|
319
|
-
|
|
342
|
+
n_len = len(z) // 2 # Since z is a signal of length 2*n_len
|
|
343
|
+
n_half = int(np.ceil(n_len / 2))
|
|
320
344
|
|
|
321
345
|
# Generate the time-lag distribution of the analytic signal
|
|
322
346
|
tfd = self.gen_time_lag(z)
|
|
323
347
|
|
|
324
348
|
# Multiply the TFD by the Doppler-lag kernel
|
|
325
|
-
tfd = self.multiply_kernel_signal(tfd, kern_type, kern_params,
|
|
349
|
+
tfd = self.multiply_kernel_signal(tfd, kern_type, kern_params, n_len, n_half)
|
|
326
350
|
|
|
327
351
|
# Finalize the TFD computation
|
|
328
|
-
tfd = self.compute_tfd(
|
|
352
|
+
tfd = self.compute_tfd(n_len, n_half, tfd)
|
|
329
353
|
|
|
330
354
|
return tfd
|
|
331
355
|
|
|
@@ -343,14 +367,15 @@ class TimeFreqDistr:
|
|
|
343
367
|
z : ndarray
|
|
344
368
|
Analytic signal with zero-padded imaginary part.
|
|
345
369
|
"""
|
|
346
|
-
|
|
370
|
+
n_len = len(x)
|
|
347
371
|
|
|
348
|
-
# Ensure the signal length is even by trimming one sample if odd,
|
|
349
|
-
|
|
372
|
+
# Ensure the signal length is even by trimming one sample if odd,
|
|
373
|
+
# since the gen_time_lag function requires an even-length signal
|
|
374
|
+
if n_len % 2 != 0:
|
|
350
375
|
x = x[:-1]
|
|
351
376
|
|
|
352
|
-
# Make the analytical signal of the real-valued signal z
|
|
353
|
-
#
|
|
377
|
+
# Make the analytical signal of the real-valued signal z
|
|
378
|
+
# (preprocessed PPG signal). Doesn't work for input of complex numbers
|
|
354
379
|
z = self.gen_analytic(x)
|
|
355
380
|
|
|
356
381
|
return z
|
|
@@ -369,22 +394,22 @@ class TimeFreqDistr:
|
|
|
369
394
|
z : ndarray
|
|
370
395
|
Analytic signal in the time domain with zeroed second half.
|
|
371
396
|
"""
|
|
372
|
-
|
|
397
|
+
n_len = len(x)
|
|
373
398
|
|
|
374
399
|
# Zero-pad the signal to double its length
|
|
375
|
-
x = np.concatenate((np.real(x), np.zeros(
|
|
400
|
+
x = np.concatenate((np.real(x), np.zeros(n_len)))
|
|
376
401
|
x_fft = np.fft.fft(x)
|
|
377
402
|
|
|
378
403
|
# Generate the analytic signal in the frequency domain
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
z_cb = np.fft.ifft(x_fft *
|
|
404
|
+
h_analytic = np.empty(2 * n_len) # Preallocate an array of size 2*n_len
|
|
405
|
+
h_analytic[0] = 1 # First element
|
|
406
|
+
h_analytic[1:n_len] = 2 # Next n_len-1 elements
|
|
407
|
+
h_analytic[n_len] = 1 # Middle element
|
|
408
|
+
h_analytic[n_len + 1 :] = 0 # Last n_len-1 elements
|
|
409
|
+
z_cb = np.fft.ifft(x_fft * h_analytic)
|
|
385
410
|
|
|
386
411
|
# Force the second half of the time-domain signal to zero
|
|
387
|
-
z = np.concatenate((z_cb[:
|
|
412
|
+
z = np.concatenate((z_cb[:n_len], np.zeros(n_len)))
|
|
388
413
|
|
|
389
414
|
return z
|
|
390
415
|
|
|
@@ -403,30 +428,35 @@ class TimeFreqDistr:
|
|
|
403
428
|
Time-lag distribution of the analytic signal z.
|
|
404
429
|
|
|
405
430
|
"""
|
|
406
|
-
|
|
407
|
-
|
|
431
|
+
n_len = len(z) // 2 # Assuming z is a signal of length 2*n_len
|
|
432
|
+
n_half = int(np.ceil(n_len / 2))
|
|
408
433
|
|
|
409
434
|
# Initialize the time-frequency distribution (TFD) matrix
|
|
410
|
-
tfd = np.zeros((
|
|
435
|
+
tfd = np.zeros((n_len, n_len), dtype=complex)
|
|
411
436
|
|
|
412
|
-
m = np.arange(
|
|
437
|
+
m = np.arange(n_half)
|
|
413
438
|
|
|
414
439
|
# Loop over time indices
|
|
415
|
-
for n in range(
|
|
416
|
-
inp = np.mod(n + m, 2 *
|
|
417
|
-
inn = np.mod(n - m, 2 *
|
|
440
|
+
for n in range(n_len):
|
|
441
|
+
inp = np.mod(n + m, 2 * n_len)
|
|
442
|
+
inn = np.mod(n - m, 2 * n_len)
|
|
418
443
|
|
|
419
444
|
# Extract the time slice from the analytic signal
|
|
420
|
-
|
|
445
|
+
k_time_slice = z[inp] * np.conj(z[inn])
|
|
421
446
|
|
|
422
447
|
# Store real and imaginary parts
|
|
423
|
-
tfd[n, :
|
|
424
|
-
tfd[n,
|
|
448
|
+
tfd[n, :n_half] = np.real(k_time_slice)
|
|
449
|
+
tfd[n, n_half:] = np.imag(k_time_slice)
|
|
425
450
|
|
|
426
451
|
return tfd
|
|
427
452
|
|
|
428
453
|
def multiply_kernel_signal(
|
|
429
|
-
self,
|
|
454
|
+
self,
|
|
455
|
+
tfd: np.ndarray,
|
|
456
|
+
kern_type: str,
|
|
457
|
+
kern_params: dict,
|
|
458
|
+
n_len: int,
|
|
459
|
+
n_half: int,
|
|
430
460
|
) -> np.ndarray:
|
|
431
461
|
"""
|
|
432
462
|
Multiplies the TFD by the Doppler-lag kernel.
|
|
@@ -450,24 +480,24 @@ class TimeFreqDistr:
|
|
|
450
480
|
Modified TFD after kernel multiplication.
|
|
451
481
|
"""
|
|
452
482
|
# Loop over lag indices
|
|
453
|
-
for m in range(
|
|
483
|
+
for m in range(n_half):
|
|
454
484
|
# Generate the Doppler-lag kernel for each lag index
|
|
455
|
-
g_lag_slice = self.gen_doppler_lag_kern(kern_type, kern_params,
|
|
485
|
+
g_lag_slice = self.gen_doppler_lag_kern(kern_type, kern_params, n_len, m)
|
|
456
486
|
|
|
457
487
|
# Extract and transform the TFD slice for this lag
|
|
458
|
-
tfd_slice = np.fft.fft(tfd[:, m]) + 1j * np.fft.fft(tfd[:,
|
|
488
|
+
tfd_slice = np.fft.fft(tfd[:, m]) + 1j * np.fft.fft(tfd[:, n_half + m])
|
|
459
489
|
|
|
460
490
|
# Multiply by the kernel and perform inverse FFT
|
|
461
|
-
|
|
491
|
+
r_lag_slice = np.fft.ifft(tfd_slice * g_lag_slice)
|
|
462
492
|
|
|
463
493
|
# Store real and imaginary parts back into the TFD
|
|
464
|
-
tfd[:, m] = np.real(
|
|
465
|
-
tfd[:,
|
|
494
|
+
tfd[:, m] = np.real(r_lag_slice)
|
|
495
|
+
tfd[:, n_half + m] = np.imag(r_lag_slice)
|
|
466
496
|
|
|
467
497
|
return tfd
|
|
468
498
|
|
|
469
499
|
def gen_doppler_lag_kern(
|
|
470
|
-
self, kern_type: str, kern_params: dict,
|
|
500
|
+
self, kern_type: str, kern_params: dict, n_len: int, lag_index: int
|
|
471
501
|
):
|
|
472
502
|
"""
|
|
473
503
|
Generate the Doppler-lag kernel based on kernel type and parameters.
|
|
@@ -478,7 +508,7 @@ class TimeFreqDistr:
|
|
|
478
508
|
Type of kernel (e.g., 'wvd', 'swvd', 'pwvd', etc.).
|
|
479
509
|
kern_params : dict
|
|
480
510
|
Parameters for the kernel.
|
|
481
|
-
|
|
511
|
+
n_len : int
|
|
482
512
|
Signal length.
|
|
483
513
|
lag_index : int
|
|
484
514
|
Current lag index.
|
|
@@ -488,15 +518,20 @@ class TimeFreqDistr:
|
|
|
488
518
|
g : ndarray
|
|
489
519
|
Doppler-lag kernel for the given lag.
|
|
490
520
|
"""
|
|
491
|
-
g = np.zeros(
|
|
521
|
+
g = np.zeros(n_len, dtype=complex) # Initialize the kernel
|
|
492
522
|
|
|
493
523
|
# Get kernel based on the type
|
|
494
|
-
g = self.get_kern(g, lag_index, kern_type, kern_params,
|
|
524
|
+
g = self.get_kern(g, lag_index, kern_type, kern_params, n_len)
|
|
495
525
|
|
|
496
526
|
return np.real(g) # All kernels are real valued
|
|
497
527
|
|
|
498
528
|
def get_kern(
|
|
499
|
-
self,
|
|
529
|
+
self,
|
|
530
|
+
g: np.ndarray,
|
|
531
|
+
lag_index: int,
|
|
532
|
+
kern_type: str,
|
|
533
|
+
kern_params: dict,
|
|
534
|
+
n_len: int,
|
|
500
535
|
) -> np.ndarray:
|
|
501
536
|
"""
|
|
502
537
|
Get the kernel based on the provided kernel type.
|
|
@@ -511,7 +546,7 @@ class TimeFreqDistr:
|
|
|
511
546
|
Type of kernel to use (now included: 'wvd', 'swvd', 'pwvd', 'sep').
|
|
512
547
|
kern_params : dict
|
|
513
548
|
Parameters for the specified kernel.
|
|
514
|
-
|
|
549
|
+
n_len : int
|
|
515
550
|
Signal length.
|
|
516
551
|
|
|
517
552
|
Returns:
|
|
@@ -541,12 +576,13 @@ class TimeFreqDistr:
|
|
|
541
576
|
g1 = np.copy(g) # Create a new array for g1
|
|
542
577
|
g2 = np.copy(g) # Create a new array for g2
|
|
543
578
|
|
|
544
|
-
# Call recursively to obtain g1 and g2 kernels (no in-place
|
|
579
|
+
# Call recursively to obtain g1 and g2 kernels (no in-place
|
|
580
|
+
# modification of g)
|
|
545
581
|
g1 = self.get_kern(
|
|
546
|
-
g1, lag_index, "swvd", kern_params["lag"],
|
|
582
|
+
g1, lag_index, "swvd", kern_params["lag"], n_len
|
|
547
583
|
) # Generate the first kernel
|
|
548
584
|
g2 = self.get_kern(
|
|
549
|
-
g2, lag_index, "pwvd", kern_params["doppler"],
|
|
585
|
+
g2, lag_index, "pwvd", kern_params["doppler"], n_len
|
|
550
586
|
) # Generate the second kernel
|
|
551
587
|
g = g1 * g2 # Multiply the two kernels to obtain the separable kernel
|
|
552
588
|
|
|
@@ -561,16 +597,16 @@ class TimeFreqDistr:
|
|
|
561
597
|
win_param = kern_params["win_param"] if "win_param" in kern_params else 0
|
|
562
598
|
win_param2 = kern_params["win_param2"] if "win_param2" in kern_params else 1
|
|
563
599
|
|
|
564
|
-
|
|
565
|
-
|
|
600
|
+
g_window = self.get_window(win_length, win_type, win_param)
|
|
601
|
+
g_window = self.pad_window(g_window, n_len)
|
|
566
602
|
|
|
567
603
|
if kern_type == "swvd" and win_param2 == 0:
|
|
568
|
-
|
|
569
|
-
if
|
|
570
|
-
|
|
571
|
-
|
|
604
|
+
g_window = np.fft.fft(g_window)
|
|
605
|
+
if g_window[0] != 0: # add this check to avoid division by zero
|
|
606
|
+
g_window /= g_window[0]
|
|
607
|
+
g_window = g_window[lag_index]
|
|
572
608
|
|
|
573
|
-
g[:] =
|
|
609
|
+
g[:] = g_window
|
|
574
610
|
|
|
575
611
|
return g
|
|
576
612
|
|
|
@@ -580,7 +616,7 @@ class TimeFreqDistr:
|
|
|
580
616
|
win_type: str,
|
|
581
617
|
win_param: float | None = None,
|
|
582
618
|
dft_window: bool = False,
|
|
583
|
-
|
|
619
|
+
n_pad: int = 0,
|
|
584
620
|
) -> np.ndarray:
|
|
585
621
|
"""
|
|
586
622
|
General function to calculate a window function.
|
|
@@ -596,8 +632,8 @@ class TimeFreqDistr:
|
|
|
596
632
|
Window parameter (e.g., alpha for Gaussian window). Default is None.
|
|
597
633
|
dft_window : bool, optional
|
|
598
634
|
If True, returns the DFT of the window. Default is False.
|
|
599
|
-
|
|
600
|
-
If greater than 0, zero-pads the window to length
|
|
635
|
+
n_pad : int, optional
|
|
636
|
+
If greater than 0, zero-pads the window to length n_pad. Default is 0.
|
|
601
637
|
|
|
602
638
|
Returns:
|
|
603
639
|
--------
|
|
@@ -611,9 +647,9 @@ class TimeFreqDistr:
|
|
|
611
647
|
# Shift the window so that positive indices are first
|
|
612
648
|
win = self.shift_window(win)
|
|
613
649
|
|
|
614
|
-
# Zero-pad the window to length
|
|
615
|
-
if
|
|
616
|
-
win = self.pad_window(win,
|
|
650
|
+
# Zero-pad the window to length n_pad if necessary
|
|
651
|
+
if n_pad > 0:
|
|
652
|
+
win = self.pad_window(win, n_pad)
|
|
617
653
|
|
|
618
654
|
return win
|
|
619
655
|
|
|
@@ -634,7 +670,8 @@ class TimeFreqDistr:
|
|
|
634
670
|
win_type : str
|
|
635
671
|
Type of window.
|
|
636
672
|
win_param : float, optional
|
|
637
|
-
Additional parameter for certain window types (e.g., Gaussian
|
|
673
|
+
Additional parameter for certain window types (e.g., Gaussian
|
|
674
|
+
alpha). Default is None.
|
|
638
675
|
dft_window : bool, optional
|
|
639
676
|
If True, returns the DFT of the window. Default is False.
|
|
640
677
|
|
|
@@ -688,10 +725,10 @@ class TimeFreqDistr:
|
|
|
688
725
|
w_shifted : ndarray
|
|
689
726
|
Shifted window with positive indices first.
|
|
690
727
|
"""
|
|
691
|
-
|
|
692
|
-
return np.roll(w,
|
|
728
|
+
n_len = len(w)
|
|
729
|
+
return np.roll(w, n_len // 2)
|
|
693
730
|
|
|
694
|
-
def pad_window(self, w: np.ndarray,
|
|
731
|
+
def pad_window(self, w: np.ndarray, n_pad: int) -> np.ndarray:
|
|
695
732
|
"""
|
|
696
733
|
Zero-pad the window to a specified length.
|
|
697
734
|
|
|
@@ -699,49 +736,49 @@ class TimeFreqDistr:
|
|
|
699
736
|
-----------
|
|
700
737
|
w : ndarray
|
|
701
738
|
The original window.
|
|
702
|
-
|
|
739
|
+
n_pad : int
|
|
703
740
|
Length to zero-pad the window to.
|
|
704
741
|
|
|
705
742
|
Returns:
|
|
706
743
|
--------
|
|
707
744
|
w_pad : ndarray
|
|
708
|
-
Zero-padded window of length
|
|
745
|
+
Zero-padded window of length n_pad.
|
|
709
746
|
|
|
710
747
|
Raises:
|
|
711
748
|
-------
|
|
712
749
|
ValueError:
|
|
713
|
-
If
|
|
750
|
+
If n_pad is less than the original window length.
|
|
714
751
|
"""
|
|
715
|
-
|
|
716
|
-
w_pad = np.zeros(
|
|
717
|
-
|
|
752
|
+
n_len = len(w)
|
|
753
|
+
w_pad = np.zeros(n_pad)
|
|
754
|
+
n_half = n_len // 2
|
|
718
755
|
|
|
719
|
-
if
|
|
720
|
-
raise ValueError("
|
|
756
|
+
if n_pad < n_len:
|
|
757
|
+
raise ValueError("n_pad must be greater than or equal to the window length")
|
|
721
758
|
|
|
722
|
-
if
|
|
759
|
+
if n_len == n_pad:
|
|
723
760
|
return w
|
|
724
761
|
|
|
725
|
-
if
|
|
726
|
-
w_pad[:
|
|
727
|
-
w_pad[-
|
|
728
|
-
else: # For even
|
|
729
|
-
w_pad[:
|
|
730
|
-
w_pad[
|
|
731
|
-
w_pad[-
|
|
732
|
-
w_pad[-
|
|
762
|
+
if n_len % 2 == 1: # For odd n_len
|
|
763
|
+
w_pad[: n_half + 1] = w[: n_half + 1]
|
|
764
|
+
w_pad[-n_half:] = w[-n_half:]
|
|
765
|
+
else: # For even n_len
|
|
766
|
+
w_pad[:n_half] = w[:n_half]
|
|
767
|
+
w_pad[n_half] = w[n_half] / 2
|
|
768
|
+
w_pad[-n_half:] = w[-n_half:]
|
|
769
|
+
w_pad[-n_half] = w[n_half] / 2
|
|
733
770
|
|
|
734
771
|
return w_pad
|
|
735
772
|
|
|
736
|
-
def compute_tfd(self,
|
|
773
|
+
def compute_tfd(self, n_len: int, n_half: int, tfd: np.ndarray):
|
|
737
774
|
"""
|
|
738
775
|
Finalizes the time-frequency distribution computation.
|
|
739
776
|
|
|
740
777
|
Parameters:
|
|
741
778
|
-----------
|
|
742
|
-
|
|
779
|
+
n_len : int
|
|
743
780
|
Size of the TFD.
|
|
744
|
-
|
|
781
|
+
n_half : int
|
|
745
782
|
Half-length parameter.
|
|
746
783
|
tfd : np.ndarray
|
|
747
784
|
Time-frequency distribution to be finalized.
|
|
@@ -749,34 +786,36 @@ class TimeFreqDistr:
|
|
|
749
786
|
Returns:
|
|
750
787
|
--------
|
|
751
788
|
tfd : np.ndarray
|
|
752
|
-
Final computed TFD (
|
|
789
|
+
Final computed TFD (n_len, n_len).
|
|
753
790
|
"""
|
|
754
|
-
m = np.arange(0,
|
|
755
|
-
mb = np.arange(1,
|
|
791
|
+
m = np.arange(0, n_half) # m = 0:(n_half-1)
|
|
792
|
+
mb = np.arange(1, n_half) # mb = 1:(n_half-1)
|
|
756
793
|
|
|
757
|
-
for n in range(0,
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
794
|
+
for n in range(0, n_len - 1, 2): # for n=0:2:n_len-2
|
|
795
|
+
r_even_half = np.complex128(tfd[n, :n_half]) + 1j * np.complex128(
|
|
796
|
+
tfd[n, n_half:]
|
|
797
|
+
)
|
|
798
|
+
r_odd_half = np.complex128(tfd[n + 1, :n_half]) + 1j * np.complex128(
|
|
799
|
+
tfd[n + 1, n_half:]
|
|
761
800
|
)
|
|
762
801
|
|
|
763
|
-
|
|
764
|
-
|
|
802
|
+
r_tslice_even = np.zeros(n_len, dtype=np.complex128)
|
|
803
|
+
r_tslice_odd = np.zeros(n_len, dtype=np.complex128)
|
|
765
804
|
|
|
766
|
-
|
|
767
|
-
|
|
805
|
+
r_tslice_even[m] = r_even_half
|
|
806
|
+
r_tslice_odd[m] = r_odd_half
|
|
768
807
|
|
|
769
|
-
|
|
770
|
-
|
|
808
|
+
r_tslice_even[n_len - mb] = np.conj(r_even_half[mb])
|
|
809
|
+
r_tslice_odd[n_len - mb] = np.conj(r_odd_half[mb])
|
|
771
810
|
|
|
772
811
|
# Perform FFT to compute time slices
|
|
773
|
-
tfd_time_slice = np.fft.fft(
|
|
812
|
+
tfd_time_slice = np.fft.fft(r_tslice_even + 1j * r_tslice_odd)
|
|
774
813
|
|
|
775
814
|
tfd[n, :] = np.real(tfd_time_slice)
|
|
776
815
|
tfd[n + 1, :] = np.imag(tfd_time_slice)
|
|
777
816
|
|
|
778
|
-
tfd = tfd /
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
817
|
+
tfd = tfd / n_len # Normalize the TFD
|
|
818
|
+
# Transpose the TFD to have the time on the x-axis and frequency on
|
|
819
|
+
# the y-axis
|
|
820
|
+
tfd = tfd.transpose()
|
|
782
821
|
return tfd
|