paradigma 0.3.2__py3-none-any.whl → 0.4.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/assets/gait_detection_clf_package.pkl +0 -0
- paradigma/assets/gait_filtering_clf_package.pkl +0 -0
- paradigma/assets/ppg_quality_clf_package.pkl +0 -0
- paradigma/assets/tremor_detection_clf_package.pkl +0 -0
- paradigma/classification.py +115 -0
- paradigma/config.py +314 -0
- paradigma/constants.py +48 -7
- paradigma/feature_extraction.py +811 -547
- paradigma/pipelines/__init__.py +0 -0
- paradigma/pipelines/gait_pipeline.py +727 -0
- paradigma/pipelines/heart_rate_pipeline.py +426 -0
- paradigma/pipelines/heart_rate_utils.py +780 -0
- paradigma/pipelines/tremor_pipeline.py +299 -0
- paradigma/preprocessing.py +363 -0
- paradigma/segmenting.py +396 -0
- paradigma/testing.py +416 -0
- paradigma/util.py +393 -16
- {paradigma-0.3.2.dist-info → paradigma-0.4.0.dist-info}/METADATA +58 -14
- paradigma-0.4.0.dist-info/RECORD +22 -0
- {paradigma-0.3.2.dist-info → paradigma-0.4.0.dist-info}/WHEEL +1 -1
- paradigma/gait_analysis.py +0 -415
- paradigma/gait_analysis_config.py +0 -266
- paradigma/heart_rate_analysis.py +0 -127
- paradigma/heart_rate_analysis_config.py +0 -9
- paradigma/heart_rate_util.py +0 -173
- paradigma/imu_preprocessing.py +0 -232
- paradigma/ppg/classifier/LR_PPG_quality.pkl +0 -0
- paradigma/ppg/classifier/LR_model.mat +0 -0
- paradigma/ppg/feat_extraction/acc_feature.m +0 -20
- paradigma/ppg/feat_extraction/peakdet.m +0 -64
- paradigma/ppg/feat_extraction/ppg_features.m +0 -53
- paradigma/ppg/glob_functions/extract_hr_segments.m +0 -37
- paradigma/ppg/glob_functions/extract_overlapping_segments.m +0 -23
- paradigma/ppg/glob_functions/jsonlab/AUTHORS.txt +0 -41
- paradigma/ppg/glob_functions/jsonlab/ChangeLog.txt +0 -74
- paradigma/ppg/glob_functions/jsonlab/LICENSE_BSD.txt +0 -25
- paradigma/ppg/glob_functions/jsonlab/LICENSE_GPLv3.txt +0 -699
- paradigma/ppg/glob_functions/jsonlab/README.txt +0 -394
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/entries +0 -368
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_jsonlab_basic.m.svn-base +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_ubjson_basic.m.svn-base +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example1.json.svn-base +0 -23
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example2.json.svn-base +0 -22
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example3.json.svn-base +0 -11
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example4.json.svn-base +0 -34
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_basictest.matlab.svn-base +0 -662
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.m.svn-base +0 -27
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.matlab.svn-base +0 -144
- paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_speedtest.m.svn-base +0 -21
- paradigma/ppg/glob_functions/jsonlab/examples/demo_jsonlab_basic.m +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/demo_ubjson_basic.m +0 -180
- paradigma/ppg/glob_functions/jsonlab/examples/example1.json +0 -23
- paradigma/ppg/glob_functions/jsonlab/examples/example2.json +0 -22
- paradigma/ppg/glob_functions/jsonlab/examples/example3.json +0 -11
- paradigma/ppg/glob_functions/jsonlab/examples/example4.json +0 -34
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_basictest.matlab +0 -662
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.m +0 -27
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.matlab +0 -144
- paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_speedtest.m +0 -21
- paradigma/ppg/glob_functions/jsonlab/jsonopt.m +0 -32
- paradigma/ppg/glob_functions/jsonlab/loadjson.m +0 -566
- paradigma/ppg/glob_functions/jsonlab/loadubjson.m +0 -528
- paradigma/ppg/glob_functions/jsonlab/mergestruct.m +0 -33
- paradigma/ppg/glob_functions/jsonlab/savejson.m +0 -475
- paradigma/ppg/glob_functions/jsonlab/saveubjson.m +0 -504
- paradigma/ppg/glob_functions/jsonlab/varargin2struct.m +0 -40
- paradigma/ppg/glob_functions/sample_prob_final.m +0 -49
- paradigma/ppg/glob_functions/synchronization.m +0 -76
- paradigma/ppg/glob_functions/tsdf_scan_meta.m +0 -22
- paradigma/ppg/hr_functions/Long_TFD_JOT.m +0 -37
- paradigma/ppg/hr_functions/PPG_TFD_HR.m +0 -59
- paradigma/ppg/hr_functions/TFD toolbox JOT/.gitignore +0 -4
- paradigma/ppg/hr_functions/TFD toolbox JOT/CHANGELOG.md +0 -23
- paradigma/ppg/hr_functions/TFD toolbox JOT/LICENCE.md +0 -27
- paradigma/ppg/hr_functions/TFD toolbox JOT/README.md +0 -251
- paradigma/ppg/hr_functions/TFD toolbox JOT/README.pdf +0 -0
- paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_kern.m +0 -142
- paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_lag_kern.m +0 -314
- paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_lag_kern.m +0 -123
- paradigma/ppg/hr_functions/TFD toolbox JOT/dec_tfd.m +0 -154
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_di_gdtfd.m +0 -194
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_li_gdtfd.m +0 -200
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_nonsep_gdtfd.m +0 -229
- paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_sep_gdtfd.m +0 -241
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/di_gdtfd.m +0 -157
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/li_gdtfd.m +0 -190
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/nonsep_gdtfd.m +0 -196
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/sep_gdtfd.m +0 -199
- paradigma/ppg/hr_functions/TFD toolbox JOT/full_tfd.m +0 -144
- paradigma/ppg/hr_functions/TFD toolbox JOT/load_curdir.m +0 -13
- paradigma/ppg/hr_functions/TFD toolbox JOT/pics/decimated_TFDs_examples.png +0 -0
- paradigma/ppg/hr_functions/TFD toolbox JOT/pics/full_TFDs_examples.png +0 -0
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/check_dec_params_seq.m +0 -79
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispEE.m +0 -9
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispVars.m +0 -26
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/disp_bytes.m +0 -25
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_full.m +0 -40
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_half.m +0 -34
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/gen_LFM.m +0 -29
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_analytic_signal.m +0 -76
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_window.m +0 -176
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/isreal_fn.m +0 -11
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/padWin.m +0 -97
- paradigma/ppg/hr_functions/TFD toolbox JOT/utils/vtfd.m +0 -149
- paradigma/ppg/preprocessing/preprocessing_imu.m +0 -15
- paradigma/ppg/preprocessing/preprocessing_ppg.m +0 -13
- paradigma/ppg_preprocessing.py +0 -313
- paradigma/preprocessing_config.py +0 -69
- paradigma/quantification.py +0 -58
- paradigma/tremor/TremorFeaturesAndClassification.m +0 -345
- paradigma/tremor/feat_extraction/DerivativesExtract.m +0 -22
- paradigma/tremor/feat_extraction/ExtractBandSignalsRMS.m +0 -72
- paradigma/tremor/feat_extraction/MFCCExtract.m +0 -100
- paradigma/tremor/feat_extraction/PSDBandPower.m +0 -52
- paradigma/tremor/feat_extraction/PSDEst.m +0 -63
- paradigma/tremor/feat_extraction/PSDExtrAxis.m +0 -88
- paradigma/tremor/feat_extraction/PSDExtrOpt.m +0 -95
- paradigma/tremor/preprocessing/InterpData.m +0 -32
- paradigma/tremor/weekly_aggregates/WeeklyAggregates.m +0 -295
- paradigma/windowing.py +0 -219
- paradigma-0.3.2.dist-info/RECORD +0 -108
- {paradigma-0.3.2.dist-info → paradigma-0.4.0.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import signal
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
from paradigma.config import HeartRateConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def assign_sqa_label(
|
|
9
|
+
ppg_prob: np.ndarray,
|
|
10
|
+
config: HeartRateConfig,
|
|
11
|
+
acc_label=None
|
|
12
|
+
) -> np.ndarray:
|
|
13
|
+
"""
|
|
14
|
+
Assigns a signal quality label to every individual data point.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
ppg_prob : np.ndarray
|
|
19
|
+
The probabilities for PPG.
|
|
20
|
+
config : HeartRateConfig
|
|
21
|
+
The configuration parameters.
|
|
22
|
+
acc_label : np.ndarray, optional
|
|
23
|
+
The labels for the accelerometer.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
np.ndarray
|
|
28
|
+
The signal quality assessment labels.
|
|
29
|
+
"""
|
|
30
|
+
# Default _label to ones if not provided
|
|
31
|
+
if acc_label is None:
|
|
32
|
+
acc_label = np.ones(len(ppg_prob))
|
|
33
|
+
|
|
34
|
+
# Number of samples in an epoch
|
|
35
|
+
fs = config.sampling_frequency
|
|
36
|
+
samples_per_epoch = config.window_length_s * fs
|
|
37
|
+
|
|
38
|
+
# Calculate number of samples to shift for each epoch
|
|
39
|
+
samples_shift = config.window_step_length_s * fs
|
|
40
|
+
n_samples = int(np.round(len(ppg_prob) + config.window_overlap_s) * fs)
|
|
41
|
+
data_prob = np.zeros(n_samples)
|
|
42
|
+
data_label_imu = np.zeros(n_samples, dtype=np.int8)
|
|
43
|
+
|
|
44
|
+
for i in range(n_samples):
|
|
45
|
+
# Start and end indices for current epoch
|
|
46
|
+
start_idx = max(0, int((i - (samples_per_epoch - samples_shift)) // fs)) # max to handle first epochs
|
|
47
|
+
end_idx = min(int(i // fs), len(ppg_prob)) # min to handle last epochs
|
|
48
|
+
|
|
49
|
+
# Extract probabilities and labels for the current epoch
|
|
50
|
+
prob = ppg_prob[start_idx:end_idx+1]
|
|
51
|
+
label_imu = acc_label[start_idx:end_idx+1]
|
|
52
|
+
|
|
53
|
+
# Calculate mean probability and majority voting for labels
|
|
54
|
+
data_prob[i] = np.mean(prob)
|
|
55
|
+
data_label_imu[i] = int(np.mean(label_imu) >= 0.5)
|
|
56
|
+
|
|
57
|
+
# Set probability to zero if majority IMU label is 0
|
|
58
|
+
data_prob[data_label_imu == 0] = 0
|
|
59
|
+
sqa_label = data_prob >= config.threshold_sqa
|
|
60
|
+
|
|
61
|
+
return sqa_label
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def extract_hr_segments(sqa_label: np.ndarray, min_hr_samples: int) -> Tuple[np.ndarray, np.ndarray]:
|
|
65
|
+
"""
|
|
66
|
+
Extracts heart rate segments based on the SQA label.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
sqa_label : np.ndarray
|
|
71
|
+
The signal quality assessment label.
|
|
72
|
+
min_hr_samples : int
|
|
73
|
+
The minimum number of samples required for a heart rate segment.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
Tuple[v_start_idx_long, v_end_idx_long]
|
|
78
|
+
The start and end indices of the heart rate segments.
|
|
79
|
+
"""
|
|
80
|
+
# Find the start and end indices of the heart rate segments
|
|
81
|
+
v_start_idx = np.where(np.diff(sqa_label.astype(int)) == 1)[0] + 1
|
|
82
|
+
v_end_idx = np.where(np.diff(sqa_label.astype(int)) == -1)[0] + 1
|
|
83
|
+
|
|
84
|
+
# Check if the first segment is a start or end
|
|
85
|
+
if sqa_label[0] == 1:
|
|
86
|
+
v_start_idx = np.insert(v_start_idx, 0, 0)
|
|
87
|
+
if sqa_label[-1] == 1:
|
|
88
|
+
v_end_idx = np.append(v_end_idx, len(sqa_label))
|
|
89
|
+
|
|
90
|
+
# Check if the segments are long enough
|
|
91
|
+
v_start_idx_long = v_start_idx[(v_end_idx - v_start_idx) >= min_hr_samples]
|
|
92
|
+
v_end_idx_long = v_end_idx[(v_end_idx - v_start_idx) >= min_hr_samples]
|
|
93
|
+
|
|
94
|
+
return v_start_idx_long, v_end_idx_long
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_hr_from_segment(
|
|
98
|
+
ppg: np.ndarray,
|
|
99
|
+
tfd_length: int,
|
|
100
|
+
fs: int,
|
|
101
|
+
kern_type: str,
|
|
102
|
+
kern_params: dict
|
|
103
|
+
) -> np.ndarray:
|
|
104
|
+
"""
|
|
105
|
+
Extracts heart rate from the time-frequency distribution of the PPG signal.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
ppg : np.ndarray
|
|
110
|
+
The preprocessed PPG segment with 2 seconds of padding on both sides to reduce boundary effects.
|
|
111
|
+
tfd_length : int
|
|
112
|
+
Length of each segment (in seconds) to calculate the time-frequency distribution.
|
|
113
|
+
fs : int
|
|
114
|
+
The sampling frequency of the PPG signal.
|
|
115
|
+
kern_type : str
|
|
116
|
+
Type of TFD kernel to use (e.g., 'wvd' for Wigner-Ville distribution).
|
|
117
|
+
kern_params : dict
|
|
118
|
+
Parameters for the specified kernel. Not required for 'wvd', but relevant for other
|
|
119
|
+
kernels like 'spwvd' or 'swvd'. Default is None.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
np.ndarray
|
|
124
|
+
The estimated heart rate.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
# Constants to handle boundary effects
|
|
128
|
+
edge_padding = 4 * fs # Additional 4 seconds (2 seconds on both sides)
|
|
129
|
+
tfd_length = tfd_length * fs # Convert tfd_length to samples
|
|
130
|
+
|
|
131
|
+
# Calculate the actual segment length without padding
|
|
132
|
+
original_segment_length = len(ppg) - edge_padding
|
|
133
|
+
|
|
134
|
+
# Determine the number of tfd_length-second segments
|
|
135
|
+
num_segments = max(1, int(original_segment_length // tfd_length))
|
|
136
|
+
|
|
137
|
+
# Split the PPG signal into segments
|
|
138
|
+
ppg_segments = []
|
|
139
|
+
for i in range(num_segments):
|
|
140
|
+
if i != num_segments - 1: # For all segments except the last
|
|
141
|
+
start_idx = int(i * tfd_length)
|
|
142
|
+
end_idx = int((i + 1) * tfd_length + edge_padding)
|
|
143
|
+
else: # For the last segment
|
|
144
|
+
start_idx = int(i * tfd_length)
|
|
145
|
+
end_idx = len(ppg)
|
|
146
|
+
ppg_segments.append(ppg[start_idx:end_idx])
|
|
147
|
+
|
|
148
|
+
hr_est_from_ppg = np.array([])
|
|
149
|
+
for segment in ppg_segments:
|
|
150
|
+
# Calculate the time-frequency distribution
|
|
151
|
+
hr_tfd = extract_hr_with_tfd(segment, fs, kern_type, kern_params)
|
|
152
|
+
hr_est_from_ppg = np.concatenate((hr_est_from_ppg, hr_tfd))
|
|
153
|
+
|
|
154
|
+
return hr_est_from_ppg
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def extract_hr_with_tfd(
|
|
158
|
+
ppg: np.ndarray,
|
|
159
|
+
fs: int,
|
|
160
|
+
kern_type: str,
|
|
161
|
+
kern_params: dict
|
|
162
|
+
) -> np.ndarray:
|
|
163
|
+
"""
|
|
164
|
+
Estimate heart rate (HR) from a PPG segment using a TFD method with optional
|
|
165
|
+
moving average filtering.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
ppg_segment : array-like
|
|
170
|
+
Segment of the PPG signal to process.
|
|
171
|
+
fs : int
|
|
172
|
+
Sampling frequency in Hz.
|
|
173
|
+
kern_type : str
|
|
174
|
+
Type of TFD kernel to use.
|
|
175
|
+
kern_params : dict
|
|
176
|
+
Parameters for the specified kernel.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
hr_smooth_tfd : np.ndarray
|
|
181
|
+
Estimated HR values (in beats per minute) for each 2-second segment of the PPG signal.
|
|
182
|
+
"""
|
|
183
|
+
# Generate the TFD matrix using the specified kernel
|
|
184
|
+
tfd_obj = TimeFreqDistr()
|
|
185
|
+
tfd = tfd_obj.nonsep_gdtfd(ppg, kern_type, kern_params) # Returns an NxN matrix
|
|
186
|
+
|
|
187
|
+
# Get time and frequency axes for the TFD
|
|
188
|
+
num_time_samples, num_freq_bins = tfd.shape
|
|
189
|
+
time_axis = np.arange(num_time_samples) / fs
|
|
190
|
+
freq_axis = np.linspace(0, 0.5, num_freq_bins) * fs
|
|
191
|
+
|
|
192
|
+
# Estimate HR by identifying the max frequency in the TFD
|
|
193
|
+
max_freq_indices = np.argmax(tfd, axis=0)
|
|
194
|
+
|
|
195
|
+
hr_smooth_tfd = np.array([])
|
|
196
|
+
for i in range(2, int(len(ppg) / fs) - 4 + 1, 2): # Skip the first and last 2 seconds, add 1 to include the last segment
|
|
197
|
+
relevant_indices = (time_axis >= i) & (time_axis < i + 2)
|
|
198
|
+
avg_frequency = np.mean(freq_axis[max_freq_indices[relevant_indices]])
|
|
199
|
+
hr_smooth_tfd = np.concatenate((hr_smooth_tfd, [60 * avg_frequency])) # Convert frequency to BPM
|
|
200
|
+
|
|
201
|
+
return hr_smooth_tfd
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TimeFreqDistr:
|
|
205
|
+
def __init__(self):
|
|
206
|
+
"""
|
|
207
|
+
This module contains the implementation of the Generalized Time-Frequency Distribution (TFD) computation using non-separable kernels.
|
|
208
|
+
This is a Python implementation of the MATLAB code provided by John O Toole in the following repository: https://github.com/otoolej/memeff_TFDs
|
|
209
|
+
|
|
210
|
+
The following functions are implemented for the computation of the TFD:
|
|
211
|
+
- nonsep_gdtfd: Computes the generalized time-frequency distribution using a non-separable kernel.
|
|
212
|
+
- get_analytic_signal: Generates the analytic signal of the input signal.
|
|
213
|
+
- gen_analytic: Generates the analytic signal by zero-padding and performing FFT.
|
|
214
|
+
- gen_time_lag: Generates the time-lag distribution of the analytic signal.
|
|
215
|
+
- multiply_kernel_signal: Multiplies the TFD by the Doppler-lag kernel.
|
|
216
|
+
- gen_doppler_lag_kern: Generates the Doppler-lag kernel based on kernel type and parameters.
|
|
217
|
+
- get_kern: Gets the kernel based on the provided kernel type.
|
|
218
|
+
- get_window: General function to calculate a window function.
|
|
219
|
+
- get_win: Helper function to create the specified window type.
|
|
220
|
+
- shift_window: Shifts the window so that positive indices appear first.
|
|
221
|
+
- pad_window: Zero-pads the window to a specified length.
|
|
222
|
+
- compute_tfd: Finalizes the time-frequency distribution computation.
|
|
223
|
+
"""
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
def nonsep_gdtfd(
|
|
227
|
+
self,
|
|
228
|
+
x: np.ndarray,
|
|
229
|
+
kern_type: None | str = None,
|
|
230
|
+
kern_params: None | dict = None
|
|
231
|
+
):
|
|
232
|
+
"""
|
|
233
|
+
Computes the generalized time-frequency distribution (TFD) using a non-separable kernel.
|
|
234
|
+
|
|
235
|
+
Parameters:
|
|
236
|
+
-----------
|
|
237
|
+
x : ndarray
|
|
238
|
+
Input signal to be analyzed.
|
|
239
|
+
kern_type : str, optional
|
|
240
|
+
Type of kernel to be used for TFD computation. Default is None.
|
|
241
|
+
Currently supported kernels are:
|
|
242
|
+
wvd - kernel for Wigner-Ville distribution
|
|
243
|
+
swvd - kernel for smoothed Wigner-Ville distribution
|
|
244
|
+
(lag-independent kernel)
|
|
245
|
+
pwvd - kernel for pseudo Wigner-Ville distribution
|
|
246
|
+
(Doppler-independent kernel)
|
|
247
|
+
sep - kernel for separable kernel (combintation of SWVD and PWVD)
|
|
248
|
+
|
|
249
|
+
kern_params : dict, optional
|
|
250
|
+
Dictionary of parameters specific to the kernel type. Default is None.
|
|
251
|
+
The structure of the dictionary depends on the selected kernel type:
|
|
252
|
+
- wvd:
|
|
253
|
+
An empty dictionary, as no additional parameters are required.
|
|
254
|
+
- swvd:
|
|
255
|
+
Dictionary with the following keys:
|
|
256
|
+
'win_length': Length of the smoothing window.
|
|
257
|
+
'win_type': Type of window function (e.g., 'hamm', 'hann').
|
|
258
|
+
'win_param' (optional): Additional parameters for the window.
|
|
259
|
+
'win_param2' (optional): 0 for time-domain window or 1 for Doppler-domain window.
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
```python
|
|
263
|
+
kern_params = {
|
|
264
|
+
'win_length': 11,
|
|
265
|
+
'win_type': 'hamm',
|
|
266
|
+
'win_param': 0,
|
|
267
|
+
'domain': 1
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
- pwvd:
|
|
271
|
+
Dictionary with the following keys:
|
|
272
|
+
'win_length': Length of the smoothing window.
|
|
273
|
+
'win_type': Type of window function (e.g., 'cosh').
|
|
274
|
+
'win_param' (optional): Additional parameters for the window.
|
|
275
|
+
'win_param2' (optional): 0 for time-domain window or 1 for Doppler-domain window.
|
|
276
|
+
Example:
|
|
277
|
+
```python
|
|
278
|
+
kern_params = {
|
|
279
|
+
'win_length': 200,
|
|
280
|
+
'win_type': 'cosh',
|
|
281
|
+
'win_param': 0.1
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
- sep:
|
|
285
|
+
Dictionary containing two nested dictionaries, one for the Doppler window and one for the lag window:
|
|
286
|
+
'doppler': {
|
|
287
|
+
'win_length': Length of the Doppler-domain window.
|
|
288
|
+
'win_type': Type of Doppler-domain window function.
|
|
289
|
+
'win_param' (optional): Additional parameters for the Doppler window.
|
|
290
|
+
'win_param2' (optional): 0 for time-domain window or 1 for Doppler-domain window.
|
|
291
|
+
}
|
|
292
|
+
'lag': {
|
|
293
|
+
'win_length': Length of the lag-domain window.
|
|
294
|
+
'win_type': Type of lag-domain window function.
|
|
295
|
+
'win_param' (optional): Additional parameters for the lag window.
|
|
296
|
+
'win_param2' (optional): 0 for time-domain window or 1 for Doppler-domain window.
|
|
297
|
+
}
|
|
298
|
+
Example:
|
|
299
|
+
```python
|
|
300
|
+
kern_params = {
|
|
301
|
+
'doppler': {
|
|
302
|
+
'win_length': doppler_samples,
|
|
303
|
+
'win_type': win_type_doppler,
|
|
304
|
+
},
|
|
305
|
+
'lag': {
|
|
306
|
+
'win_length': lag_samples,
|
|
307
|
+
'win_type': win_type_lag,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
--------
|
|
314
|
+
tfd : ndarray
|
|
315
|
+
The computed time-frequency distribution.
|
|
316
|
+
"""
|
|
317
|
+
z = self.get_analytic_signal(x)
|
|
318
|
+
N = len(z) // 2 # Since z is a signal of length 2N
|
|
319
|
+
Nh = int(np.ceil(N / 2))
|
|
320
|
+
|
|
321
|
+
# Generate the time-lag distribution of the analytic signal
|
|
322
|
+
tfd = self.gen_time_lag(z)
|
|
323
|
+
|
|
324
|
+
# Multiply the TFD by the Doppler-lag kernel
|
|
325
|
+
tfd = self.multiply_kernel_signal(tfd, kern_type, kern_params, N, Nh)
|
|
326
|
+
|
|
327
|
+
# Finalize the TFD computation
|
|
328
|
+
tfd = self.compute_tfd(N, Nh, tfd)
|
|
329
|
+
|
|
330
|
+
return tfd
|
|
331
|
+
|
|
332
|
+
def get_analytic_signal(self, x: np.ndarray) -> np.ndarray:
|
|
333
|
+
"""
|
|
334
|
+
Generates the signals analytic version.
|
|
335
|
+
|
|
336
|
+
Parameters:
|
|
337
|
+
-----------
|
|
338
|
+
z : ndarray
|
|
339
|
+
Input real-valued signal.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
--------
|
|
343
|
+
z : ndarray
|
|
344
|
+
Analytic signal with zero-padded imaginary part.
|
|
345
|
+
"""
|
|
346
|
+
N = len(x)
|
|
347
|
+
|
|
348
|
+
# Ensure the signal length is even by trimming one sample if odd, since the gen_time_lag function requires an even-length signal
|
|
349
|
+
if N % 2 != 0:
|
|
350
|
+
x = x[:-1]
|
|
351
|
+
|
|
352
|
+
# Make the analytical signal of the real-valued signal z (preprocessed PPG signal)
|
|
353
|
+
# doesn't work for input of complex numbers
|
|
354
|
+
z = self.gen_analytic(x)
|
|
355
|
+
|
|
356
|
+
return z
|
|
357
|
+
|
|
358
|
+
def gen_analytic(self, x: np.ndarray) -> np.ndarray:
|
|
359
|
+
"""
|
|
360
|
+
Generates an analytic signal by zero-padding and performing FFT.
|
|
361
|
+
|
|
362
|
+
Parameters:
|
|
363
|
+
-----------
|
|
364
|
+
x : ndarray
|
|
365
|
+
Input real-valued signal.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
--------
|
|
369
|
+
z : ndarray
|
|
370
|
+
Analytic signal in the time domain with zeroed second half.
|
|
371
|
+
"""
|
|
372
|
+
N = len(x)
|
|
373
|
+
|
|
374
|
+
# Zero-pad the signal to double its length
|
|
375
|
+
x = np.concatenate((np.real(x), np.zeros(N)))
|
|
376
|
+
x_fft = np.fft.fft(x)
|
|
377
|
+
|
|
378
|
+
# Generate the analytic signal in the frequency domain
|
|
379
|
+
H = np.empty(2 * N) # Preallocate an array of size 2*N
|
|
380
|
+
H[0] = 1 # First element
|
|
381
|
+
H[1:N] = 2 # Next N-1 elements
|
|
382
|
+
H[N] = 1 # Middle element
|
|
383
|
+
H[N+1:] = 0 # Last N-1 elements
|
|
384
|
+
z_cb = np.fft.ifft(x_fft * H)
|
|
385
|
+
|
|
386
|
+
# Force the second half of the time-domain signal to zero
|
|
387
|
+
z = np.concatenate((z_cb[:N], np.zeros(N)))
|
|
388
|
+
|
|
389
|
+
return z
|
|
390
|
+
|
|
391
|
+
def gen_time_lag(self, z: np.ndarray) -> np.ndarray:
|
|
392
|
+
"""
|
|
393
|
+
Generate the time-lag distribution of the analytic signal z.
|
|
394
|
+
|
|
395
|
+
Parameters:
|
|
396
|
+
-----------
|
|
397
|
+
z : ndarray
|
|
398
|
+
Analytic signal of the input signal x.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
--------
|
|
402
|
+
tfd : ndarray
|
|
403
|
+
Time-lag distribution of the analytic signal z.
|
|
404
|
+
|
|
405
|
+
"""
|
|
406
|
+
N = len(z) // 2 # Assuming z is a signal of length 2N
|
|
407
|
+
Nh = int(np.ceil(N / 2))
|
|
408
|
+
|
|
409
|
+
# Initialize the time-frequency distribution (TFD) matrix
|
|
410
|
+
tfd = np.zeros((N, N), dtype=complex)
|
|
411
|
+
|
|
412
|
+
m = np.arange(Nh)
|
|
413
|
+
|
|
414
|
+
# Loop over time indices
|
|
415
|
+
for n in range(N):
|
|
416
|
+
inp = np.mod(n + m, 2 * N)
|
|
417
|
+
inn = np.mod(n - m, 2 * N)
|
|
418
|
+
|
|
419
|
+
# Extract the time slice from the analytic signal
|
|
420
|
+
K_time_slice = z[inp] * np.conj(z[inn])
|
|
421
|
+
|
|
422
|
+
# Store real and imaginary parts
|
|
423
|
+
tfd[n, :Nh] = np.real(K_time_slice)
|
|
424
|
+
tfd[n, Nh:] = np.imag(K_time_slice)
|
|
425
|
+
|
|
426
|
+
return tfd
|
|
427
|
+
|
|
428
|
+
def multiply_kernel_signal(
|
|
429
|
+
self,
|
|
430
|
+
tfd: np.ndarray,
|
|
431
|
+
kern_type: str,
|
|
432
|
+
kern_params: dict,
|
|
433
|
+
N: int,
|
|
434
|
+
Nh: int
|
|
435
|
+
) -> np.ndarray:
|
|
436
|
+
"""
|
|
437
|
+
Multiplies the TFD by the Doppler-lag kernel.
|
|
438
|
+
|
|
439
|
+
Parameters:
|
|
440
|
+
-----------
|
|
441
|
+
tfd : ndarray
|
|
442
|
+
Time-frequency distribution.
|
|
443
|
+
kern_type : str
|
|
444
|
+
Kernel type to be applied.
|
|
445
|
+
kern_params : dict
|
|
446
|
+
Kernel parameters specific to the kernel type.
|
|
447
|
+
N : int
|
|
448
|
+
Length of the signal.
|
|
449
|
+
Nh : int
|
|
450
|
+
Half length of the signal.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
--------
|
|
454
|
+
tfd : ndarray
|
|
455
|
+
Modified TFD after kernel multiplication.
|
|
456
|
+
"""
|
|
457
|
+
# Loop over lag indices
|
|
458
|
+
for m in range(Nh):
|
|
459
|
+
# Generate the Doppler-lag kernel for each lag index
|
|
460
|
+
g_lag_slice = self.gen_doppler_lag_kern(kern_type, kern_params, N, m)
|
|
461
|
+
|
|
462
|
+
# Extract and transform the TFD slice for this lag
|
|
463
|
+
tfd_slice = np.fft.fft(tfd[:, m]) + 1j * np.fft.fft(tfd[:, Nh + m])
|
|
464
|
+
|
|
465
|
+
# Multiply by the kernel and perform inverse FFT
|
|
466
|
+
R_lag_slice = np.fft.ifft(tfd_slice * g_lag_slice)
|
|
467
|
+
|
|
468
|
+
# Store real and imaginary parts back into the TFD
|
|
469
|
+
tfd[:, m] = np.real(R_lag_slice)
|
|
470
|
+
tfd[:, Nh + m] = np.imag(R_lag_slice)
|
|
471
|
+
|
|
472
|
+
return tfd
|
|
473
|
+
|
|
474
|
+
def gen_doppler_lag_kern(
|
|
475
|
+
self,
|
|
476
|
+
kern_type: str,
|
|
477
|
+
kern_params: dict,
|
|
478
|
+
N: int,
|
|
479
|
+
lag_index: int
|
|
480
|
+
):
|
|
481
|
+
"""
|
|
482
|
+
Generate the Doppler-lag kernel based on kernel type and parameters.
|
|
483
|
+
|
|
484
|
+
Parameters:
|
|
485
|
+
-----------
|
|
486
|
+
kern_type : str
|
|
487
|
+
Type of kernel (e.g., 'wvd', 'swvd', 'pwvd', etc.).
|
|
488
|
+
kern_params : dict
|
|
489
|
+
Parameters for the kernel.
|
|
490
|
+
N : int
|
|
491
|
+
Signal length.
|
|
492
|
+
lag_index : int
|
|
493
|
+
Current lag index.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
--------
|
|
497
|
+
g : ndarray
|
|
498
|
+
Doppler-lag kernel for the given lag.
|
|
499
|
+
"""
|
|
500
|
+
g = np.zeros(N, dtype=complex) # Initialize the kernel
|
|
501
|
+
|
|
502
|
+
# Get kernel based on the type
|
|
503
|
+
g = self.get_kern(g, lag_index, kern_type, kern_params, N)
|
|
504
|
+
|
|
505
|
+
return np.real(g) # All kernels are real valued
|
|
506
|
+
|
|
507
|
+
def get_kern(
|
|
508
|
+
self,
|
|
509
|
+
g: np.ndarray,
|
|
510
|
+
lag_index: int,
|
|
511
|
+
kern_type: str,
|
|
512
|
+
kern_params: dict,
|
|
513
|
+
N: int
|
|
514
|
+
) -> np.ndarray:
|
|
515
|
+
"""
|
|
516
|
+
Get the kernel based on the provided kernel type.
|
|
517
|
+
|
|
518
|
+
Parameters:
|
|
519
|
+
-----------
|
|
520
|
+
g : ndarray
|
|
521
|
+
Kernel to be filled.
|
|
522
|
+
lag_index : int
|
|
523
|
+
Lag index for the kernel.
|
|
524
|
+
kern_type : str
|
|
525
|
+
Type of kernel to use (now included: 'wvd', 'swvd', 'pwvd', 'sep').
|
|
526
|
+
kern_params : dict
|
|
527
|
+
Parameters for the specified kernel.
|
|
528
|
+
N : int
|
|
529
|
+
Signal length.
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
--------
|
|
533
|
+
g : ndarray
|
|
534
|
+
Kernel function at the current lag.
|
|
535
|
+
"""
|
|
536
|
+
# Validate kern_type
|
|
537
|
+
valid_kern_types = ['wvd', 'sep', 'swvd', 'pwvd'] # List of valid kernel types which are currently supported
|
|
538
|
+
if kern_type not in valid_kern_types:
|
|
539
|
+
raise ValueError(f"Unknown kernel type: {kern_type}. Expected one of {valid_kern_types}")
|
|
540
|
+
|
|
541
|
+
num_params = len(kern_params)
|
|
542
|
+
|
|
543
|
+
if kern_type == 'wvd':
|
|
544
|
+
g[:] = 1 # WVD kernel is the equal to 1 for all lags
|
|
545
|
+
|
|
546
|
+
elif kern_type == 'sep':
|
|
547
|
+
# Separable Kernel
|
|
548
|
+
g1 = np.copy(g) # Create a new array for g1
|
|
549
|
+
g2 = np.copy(g) # Create a new array for g2
|
|
550
|
+
|
|
551
|
+
# Call recursively to obtain g1 and g2 kernels (no in-place modification of g)
|
|
552
|
+
g1 = self.get_kern(g1, lag_index, 'swvd', kern_params['lag'], N) # Generate the first kernel
|
|
553
|
+
g2 = self.get_kern(g2, lag_index, 'pwvd', kern_params['doppler'], N) # Generate the second kernel
|
|
554
|
+
g = g1 * g2 # Multiply the two kernels to obtain the separable kernel
|
|
555
|
+
|
|
556
|
+
else:
|
|
557
|
+
if num_params < 2:
|
|
558
|
+
raise ValueError("Missing required kernel parameters: 'win_length' and 'win_type'")
|
|
559
|
+
|
|
560
|
+
win_length = kern_params['win_length']
|
|
561
|
+
win_type = kern_params['win_type']
|
|
562
|
+
win_param = kern_params['win_param'] if 'win_param' in kern_params else 0
|
|
563
|
+
win_param2 = kern_params['win_param2'] if 'win_param2' in kern_params else 1
|
|
564
|
+
|
|
565
|
+
G = self.get_window(win_length, win_type, win_param)
|
|
566
|
+
G = self.pad_window(G, N)
|
|
567
|
+
|
|
568
|
+
if kern_type == 'swvd' and win_param2 == 0:
|
|
569
|
+
G = np.fft.fft(G)
|
|
570
|
+
if G[0] != 0: # add this check to avoid division by zero
|
|
571
|
+
G /= G[0]
|
|
572
|
+
G = G[lag_index]
|
|
573
|
+
|
|
574
|
+
g[:] = G
|
|
575
|
+
|
|
576
|
+
return g
|
|
577
|
+
|
|
578
|
+
def get_window(
|
|
579
|
+
self,
|
|
580
|
+
win_length: int,
|
|
581
|
+
win_type: str,
|
|
582
|
+
win_param: float | None = None,
|
|
583
|
+
dft_window: bool = False,
|
|
584
|
+
Npad: int = 0
|
|
585
|
+
) -> np.ndarray:
|
|
586
|
+
"""
|
|
587
|
+
General function to calculate a window function.
|
|
588
|
+
|
|
589
|
+
Parameters:
|
|
590
|
+
-----------
|
|
591
|
+
win_length : int
|
|
592
|
+
Length of the window.
|
|
593
|
+
win_type : str
|
|
594
|
+
Type of window. Options are:
|
|
595
|
+
{'delta', 'rect', 'hamm'|'hamming', 'hann'|'hanning', 'gauss', 'cosh'}.
|
|
596
|
+
win_param : float, optional
|
|
597
|
+
Window parameter (e.g., alpha for Gaussian window). Default is None.
|
|
598
|
+
dft_window : bool, optional
|
|
599
|
+
If True, returns the DFT of the window. Default is False.
|
|
600
|
+
Npad : int, optional
|
|
601
|
+
If greater than 0, zero-pads the window to length Npad. Default is 0.
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
--------
|
|
605
|
+
win : ndarray
|
|
606
|
+
The calculated window (or its DFT if dft_window is True).
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
# Get the window
|
|
610
|
+
win = self.get_win(win_length, win_type, win_param, dft_window)
|
|
611
|
+
|
|
612
|
+
# Shift the window so that positive indices are first
|
|
613
|
+
win = self.shift_window(win)
|
|
614
|
+
|
|
615
|
+
# Zero-pad the window to length Npad if necessary
|
|
616
|
+
if Npad > 0:
|
|
617
|
+
win = self.pad_window(win, Npad)
|
|
618
|
+
|
|
619
|
+
return win
|
|
620
|
+
|
|
621
|
+
def get_win(
|
|
622
|
+
self,
|
|
623
|
+
win_length: int,
|
|
624
|
+
win_type: str,
|
|
625
|
+
win_param: float | None = None,
|
|
626
|
+
dft_window: bool = False
|
|
627
|
+
) -> np.ndarray:
|
|
628
|
+
"""
|
|
629
|
+
Helper function to create the specified window type.
|
|
630
|
+
|
|
631
|
+
Parameters:
|
|
632
|
+
-----------
|
|
633
|
+
win_length : int
|
|
634
|
+
Length of the window.
|
|
635
|
+
win_type : str
|
|
636
|
+
Type of window.
|
|
637
|
+
win_param : float, optional
|
|
638
|
+
Additional parameter for certain window types (e.g., Gaussian alpha). Default is None.
|
|
639
|
+
dft_window : bool, optional
|
|
640
|
+
If True, returns the DFT of the window. Default is False.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
--------
|
|
644
|
+
win : ndarray
|
|
645
|
+
The created window (or its DFT if dft_window is True).
|
|
646
|
+
"""
|
|
647
|
+
if win_type == 'delta':
|
|
648
|
+
win = np.zeros(win_length)
|
|
649
|
+
win[win_length // 2] = 1
|
|
650
|
+
elif win_type == 'rect':
|
|
651
|
+
win = np.ones(win_length)
|
|
652
|
+
elif win_type in ['hamm', 'hamming']:
|
|
653
|
+
win = signal.windows.hamming(win_length)
|
|
654
|
+
elif win_type in ['hann', 'hanning']:
|
|
655
|
+
win = signal.windows.hann(win_length)
|
|
656
|
+
elif win_type == 'gauss':
|
|
657
|
+
win = signal.windows.gaussian(win_length, std=win_param if win_param else 0.4)
|
|
658
|
+
elif win_type == 'cosh':
|
|
659
|
+
win_hlf = win_length // 2
|
|
660
|
+
if not win_param:
|
|
661
|
+
win_param = 0.01
|
|
662
|
+
win = np.array([np.cosh(m) ** (-2 * win_param) for m in range(-win_hlf, win_hlf+1)])
|
|
663
|
+
win = np.fft.fftshift(win)
|
|
664
|
+
else:
|
|
665
|
+
raise ValueError(f"Unknown window type {win_type}")
|
|
666
|
+
|
|
667
|
+
# If dft_window is True, return the DFT of the window
|
|
668
|
+
if dft_window:
|
|
669
|
+
win = np.fft.fft(np.roll(win, win_length // 2))
|
|
670
|
+
win = np.roll(win, -win_length // 2)
|
|
671
|
+
|
|
672
|
+
return win
|
|
673
|
+
|
|
674
|
+
def shift_window(self, w: np.ndarray) -> np.ndarray:
|
|
675
|
+
"""
|
|
676
|
+
Shift the window so that positive indices appear first.
|
|
677
|
+
|
|
678
|
+
Parameters:
|
|
679
|
+
-----------
|
|
680
|
+
w : ndarray
|
|
681
|
+
Window to be shifted.
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
--------
|
|
685
|
+
w_shifted : ndarray
|
|
686
|
+
Shifted window with positive indices first.
|
|
687
|
+
"""
|
|
688
|
+
N = len(w)
|
|
689
|
+
return np.roll(w, N // 2)
|
|
690
|
+
|
|
691
|
+
def pad_window(self, w: np.ndarray, Npad: int) -> np.ndarray:
|
|
692
|
+
"""
|
|
693
|
+
Zero-pad the window to a specified length.
|
|
694
|
+
|
|
695
|
+
Parameters:
|
|
696
|
+
-----------
|
|
697
|
+
w : ndarray
|
|
698
|
+
The original window.
|
|
699
|
+
Npad : int
|
|
700
|
+
Length to zero-pad the window to.
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
--------
|
|
704
|
+
w_pad : ndarray
|
|
705
|
+
Zero-padded window of length Npad.
|
|
706
|
+
|
|
707
|
+
Raises:
|
|
708
|
+
-------
|
|
709
|
+
ValueError:
|
|
710
|
+
If Npad is less than the original window length.
|
|
711
|
+
"""
|
|
712
|
+
N = len(w)
|
|
713
|
+
w_pad = np.zeros(Npad)
|
|
714
|
+
Nh = N // 2
|
|
715
|
+
|
|
716
|
+
if Npad < N:
|
|
717
|
+
raise ValueError("Npad must be greater than or equal to the window length")
|
|
718
|
+
|
|
719
|
+
if N == Npad:
|
|
720
|
+
return w
|
|
721
|
+
|
|
722
|
+
if N % 2 == 1: # For odd N
|
|
723
|
+
w_pad[:Nh+1] = w[:Nh+1]
|
|
724
|
+
w_pad[-Nh:] = w[-Nh:]
|
|
725
|
+
else: # For even N
|
|
726
|
+
w_pad[:Nh] = w[:Nh]
|
|
727
|
+
w_pad[Nh] = w[Nh] / 2
|
|
728
|
+
w_pad[-Nh:] = w[-Nh:]
|
|
729
|
+
w_pad[-Nh] = w[Nh] / 2
|
|
730
|
+
|
|
731
|
+
return w_pad
|
|
732
|
+
|
|
733
|
+
def compute_tfd(
|
|
734
|
+
self,
|
|
735
|
+
N: int,
|
|
736
|
+
Nh: int,
|
|
737
|
+
tfd: np.ndarray
|
|
738
|
+
):
|
|
739
|
+
"""
|
|
740
|
+
Finalizes the time-frequency distribution computation.
|
|
741
|
+
|
|
742
|
+
Parameters:
|
|
743
|
+
-----------
|
|
744
|
+
N : int
|
|
745
|
+
Size of the TFD.
|
|
746
|
+
Nh : int
|
|
747
|
+
Half-length parameter.
|
|
748
|
+
tfd : np.ndarray
|
|
749
|
+
Time-frequency distribution to be finalized.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
--------
|
|
753
|
+
tfd : np.ndarray
|
|
754
|
+
Final computed TFD (N,N).
|
|
755
|
+
"""
|
|
756
|
+
m = np.arange(0, Nh) # m = 0:(Nh-1)
|
|
757
|
+
mb = np.arange(1, Nh) # mb = 1:(Nh-1)
|
|
758
|
+
|
|
759
|
+
for n in range(0, N-1, 2): # for n=0:2:N-2
|
|
760
|
+
R_even_half = np.complex128(tfd[n, :Nh]) + 1j * np.complex128(tfd[n, Nh:])
|
|
761
|
+
R_odd_half = np.complex128(tfd[n+1, :Nh]) + 1j * np.complex128(tfd[n+1, Nh:])
|
|
762
|
+
|
|
763
|
+
R_tslice_even = np.zeros(N, dtype=np.complex128)
|
|
764
|
+
R_tslice_odd = np.zeros(N, dtype=np.complex128)
|
|
765
|
+
|
|
766
|
+
R_tslice_even[m] = R_even_half
|
|
767
|
+
R_tslice_odd[m] = R_odd_half
|
|
768
|
+
|
|
769
|
+
R_tslice_even[N-mb] = np.conj(R_even_half[mb])
|
|
770
|
+
R_tslice_odd[N-mb] = np.conj(R_odd_half[mb])
|
|
771
|
+
|
|
772
|
+
# Perform FFT to compute time slices
|
|
773
|
+
tfd_time_slice = np.fft.fft(R_tslice_even + 1j * R_tslice_odd)
|
|
774
|
+
|
|
775
|
+
tfd[n, :] = np.real(tfd_time_slice)
|
|
776
|
+
tfd[n+1, :] = np.imag(tfd_time_slice)
|
|
777
|
+
|
|
778
|
+
tfd = tfd / N # Normalize the TFD
|
|
779
|
+
tfd = tfd.transpose() # Transpose the TFD to have the time on the x-axis and frequency on the y-axis
|
|
780
|
+
return tfd
|