paradigma 0.3.2__py3-none-any.whl → 0.4.1__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.
Files changed (123) hide show
  1. paradigma/assets/gait_detection_clf_package.pkl +0 -0
  2. paradigma/assets/gait_filtering_clf_package.pkl +0 -0
  3. paradigma/assets/ppg_quality_clf_package.pkl +0 -0
  4. paradigma/assets/tremor_detection_clf_package.pkl +0 -0
  5. paradigma/classification.py +115 -0
  6. paradigma/config.py +314 -0
  7. paradigma/constants.py +48 -7
  8. paradigma/feature_extraction.py +811 -547
  9. paradigma/pipelines/__init__.py +0 -0
  10. paradigma/pipelines/gait_pipeline.py +727 -0
  11. paradigma/pipelines/heart_rate_pipeline.py +426 -0
  12. paradigma/pipelines/heart_rate_utils.py +780 -0
  13. paradigma/pipelines/tremor_pipeline.py +299 -0
  14. paradigma/preprocessing.py +363 -0
  15. paradigma/segmenting.py +396 -0
  16. paradigma/testing.py +416 -0
  17. paradigma/util.py +393 -16
  18. paradigma-0.4.1.dist-info/METADATA +138 -0
  19. paradigma-0.4.1.dist-info/RECORD +22 -0
  20. {paradigma-0.3.2.dist-info → paradigma-0.4.1.dist-info}/WHEEL +1 -1
  21. paradigma/gait_analysis.py +0 -415
  22. paradigma/gait_analysis_config.py +0 -266
  23. paradigma/heart_rate_analysis.py +0 -127
  24. paradigma/heart_rate_analysis_config.py +0 -9
  25. paradigma/heart_rate_util.py +0 -173
  26. paradigma/imu_preprocessing.py +0 -232
  27. paradigma/ppg/classifier/LR_PPG_quality.pkl +0 -0
  28. paradigma/ppg/classifier/LR_model.mat +0 -0
  29. paradigma/ppg/feat_extraction/acc_feature.m +0 -20
  30. paradigma/ppg/feat_extraction/peakdet.m +0 -64
  31. paradigma/ppg/feat_extraction/ppg_features.m +0 -53
  32. paradigma/ppg/glob_functions/extract_hr_segments.m +0 -37
  33. paradigma/ppg/glob_functions/extract_overlapping_segments.m +0 -23
  34. paradigma/ppg/glob_functions/jsonlab/AUTHORS.txt +0 -41
  35. paradigma/ppg/glob_functions/jsonlab/ChangeLog.txt +0 -74
  36. paradigma/ppg/glob_functions/jsonlab/LICENSE_BSD.txt +0 -25
  37. paradigma/ppg/glob_functions/jsonlab/LICENSE_GPLv3.txt +0 -699
  38. paradigma/ppg/glob_functions/jsonlab/README.txt +0 -394
  39. paradigma/ppg/glob_functions/jsonlab/examples/.svn/entries +0 -368
  40. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_jsonlab_basic.m.svn-base +0 -180
  41. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_ubjson_basic.m.svn-base +0 -180
  42. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example1.json.svn-base +0 -23
  43. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example2.json.svn-base +0 -22
  44. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example3.json.svn-base +0 -11
  45. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example4.json.svn-base +0 -34
  46. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_basictest.matlab.svn-base +0 -662
  47. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.m.svn-base +0 -27
  48. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.matlab.svn-base +0 -144
  49. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_speedtest.m.svn-base +0 -21
  50. paradigma/ppg/glob_functions/jsonlab/examples/demo_jsonlab_basic.m +0 -180
  51. paradigma/ppg/glob_functions/jsonlab/examples/demo_ubjson_basic.m +0 -180
  52. paradigma/ppg/glob_functions/jsonlab/examples/example1.json +0 -23
  53. paradigma/ppg/glob_functions/jsonlab/examples/example2.json +0 -22
  54. paradigma/ppg/glob_functions/jsonlab/examples/example3.json +0 -11
  55. paradigma/ppg/glob_functions/jsonlab/examples/example4.json +0 -34
  56. paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_basictest.matlab +0 -662
  57. paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.m +0 -27
  58. paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.matlab +0 -144
  59. paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_speedtest.m +0 -21
  60. paradigma/ppg/glob_functions/jsonlab/jsonopt.m +0 -32
  61. paradigma/ppg/glob_functions/jsonlab/loadjson.m +0 -566
  62. paradigma/ppg/glob_functions/jsonlab/loadubjson.m +0 -528
  63. paradigma/ppg/glob_functions/jsonlab/mergestruct.m +0 -33
  64. paradigma/ppg/glob_functions/jsonlab/savejson.m +0 -475
  65. paradigma/ppg/glob_functions/jsonlab/saveubjson.m +0 -504
  66. paradigma/ppg/glob_functions/jsonlab/varargin2struct.m +0 -40
  67. paradigma/ppg/glob_functions/sample_prob_final.m +0 -49
  68. paradigma/ppg/glob_functions/synchronization.m +0 -76
  69. paradigma/ppg/glob_functions/tsdf_scan_meta.m +0 -22
  70. paradigma/ppg/hr_functions/Long_TFD_JOT.m +0 -37
  71. paradigma/ppg/hr_functions/PPG_TFD_HR.m +0 -59
  72. paradigma/ppg/hr_functions/TFD toolbox JOT/.gitignore +0 -4
  73. paradigma/ppg/hr_functions/TFD toolbox JOT/CHANGELOG.md +0 -23
  74. paradigma/ppg/hr_functions/TFD toolbox JOT/LICENCE.md +0 -27
  75. paradigma/ppg/hr_functions/TFD toolbox JOT/README.md +0 -251
  76. paradigma/ppg/hr_functions/TFD toolbox JOT/README.pdf +0 -0
  77. paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_kern.m +0 -142
  78. paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_lag_kern.m +0 -314
  79. paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_lag_kern.m +0 -123
  80. paradigma/ppg/hr_functions/TFD toolbox JOT/dec_tfd.m +0 -154
  81. paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_di_gdtfd.m +0 -194
  82. paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_li_gdtfd.m +0 -200
  83. paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_nonsep_gdtfd.m +0 -229
  84. paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_sep_gdtfd.m +0 -241
  85. paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/di_gdtfd.m +0 -157
  86. paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/li_gdtfd.m +0 -190
  87. paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/nonsep_gdtfd.m +0 -196
  88. paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/sep_gdtfd.m +0 -199
  89. paradigma/ppg/hr_functions/TFD toolbox JOT/full_tfd.m +0 -144
  90. paradigma/ppg/hr_functions/TFD toolbox JOT/load_curdir.m +0 -13
  91. paradigma/ppg/hr_functions/TFD toolbox JOT/pics/decimated_TFDs_examples.png +0 -0
  92. paradigma/ppg/hr_functions/TFD toolbox JOT/pics/full_TFDs_examples.png +0 -0
  93. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/check_dec_params_seq.m +0 -79
  94. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispEE.m +0 -9
  95. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispVars.m +0 -26
  96. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/disp_bytes.m +0 -25
  97. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_full.m +0 -40
  98. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_half.m +0 -34
  99. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/gen_LFM.m +0 -29
  100. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_analytic_signal.m +0 -76
  101. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_window.m +0 -176
  102. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/isreal_fn.m +0 -11
  103. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/padWin.m +0 -97
  104. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/vtfd.m +0 -149
  105. paradigma/ppg/preprocessing/preprocessing_imu.m +0 -15
  106. paradigma/ppg/preprocessing/preprocessing_ppg.m +0 -13
  107. paradigma/ppg_preprocessing.py +0 -313
  108. paradigma/preprocessing_config.py +0 -69
  109. paradigma/quantification.py +0 -58
  110. paradigma/tremor/TremorFeaturesAndClassification.m +0 -345
  111. paradigma/tremor/feat_extraction/DerivativesExtract.m +0 -22
  112. paradigma/tremor/feat_extraction/ExtractBandSignalsRMS.m +0 -72
  113. paradigma/tremor/feat_extraction/MFCCExtract.m +0 -100
  114. paradigma/tremor/feat_extraction/PSDBandPower.m +0 -52
  115. paradigma/tremor/feat_extraction/PSDEst.m +0 -63
  116. paradigma/tremor/feat_extraction/PSDExtrAxis.m +0 -88
  117. paradigma/tremor/feat_extraction/PSDExtrOpt.m +0 -95
  118. paradigma/tremor/preprocessing/InterpData.m +0 -32
  119. paradigma/tremor/weekly_aggregates/WeeklyAggregates.m +0 -295
  120. paradigma/windowing.py +0 -219
  121. paradigma-0.3.2.dist-info/METADATA +0 -79
  122. paradigma-0.3.2.dist-info/RECORD +0 -108
  123. {paradigma-0.3.2.dist-info → paradigma-0.4.1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,426 @@
1
+ import json
2
+ import numpy as np
3
+ import os
4
+ import pandas as pd
5
+ from pathlib import Path
6
+ from scipy.signal import welch
7
+ from scipy.signal.windows import hamming, hann
8
+ import tsdf
9
+ from typing import List
10
+
11
+ from paradigma.classification import ClassifierPackage
12
+ from paradigma.constants import DataColumns
13
+ from paradigma.config import HeartRateConfig
14
+ from paradigma.feature_extraction import compute_statistics, compute_signal_to_noise_ratio, compute_auto_correlation, \
15
+ compute_dominant_frequency, compute_relative_power, compute_spectral_entropy
16
+ from paradigma.pipelines.heart_rate_utils import assign_sqa_label, extract_hr_segments, extract_hr_from_segment
17
+ from paradigma.segmenting import tabulate_windows, WindowedDataExtractor
18
+ from paradigma.util import read_metadata, aggregate_parameter
19
+
20
+ def extract_signal_quality_features(df_ppg: pd.DataFrame, df_acc: pd.DataFrame, ppg_config: HeartRateConfig, acc_config: HeartRateConfig) -> pd.DataFrame:
21
+ """
22
+ Extract signal quality features from the PPG signal.
23
+ The features are extracted from the temporal and spectral domain of the PPG signal.
24
+ The temporal domain features include variance, mean, median, kurtosis, skewness, signal-to-noise ratio, and autocorrelation.
25
+ The spectral domain features include the dominant frequency, relative power, spectral entropy.
26
+
27
+ Parameters
28
+ ----------
29
+ df_ppg : pd.DataFrame
30
+ The DataFrame containing the PPG signal.
31
+ df_acc : pd.DataFrame
32
+ The DataFrame containing the accelerometer signal.
33
+ ppg_config: HeartRateConfig
34
+ The configuration for the signal quality feature extraction of the PPG signal.
35
+ acc_config: HeartRateConfig
36
+ The configuration for the signal quality feature extraction of the accelerometer signal.
37
+
38
+ Returns
39
+ -------
40
+ df_features : pd.DataFrame
41
+ The DataFrame containing the extracted signal quality features.
42
+
43
+ """
44
+ # Group sequences of timestamps into windows
45
+ ppg_windowed_cols = [DataColumns.TIME, ppg_config.ppg_colname]
46
+ ppg_windowed = tabulate_windows(
47
+ df=df_ppg,
48
+ columns=ppg_windowed_cols,
49
+ window_length_s=ppg_config.window_length_s,
50
+ window_step_length_s=ppg_config.window_step_length_s,
51
+ fs=ppg_config.sampling_frequency
52
+ )
53
+
54
+ # Extract data from the windowed PPG signal
55
+ extractor = WindowedDataExtractor(ppg_windowed_cols)
56
+ idx_time = extractor.get_index(DataColumns.TIME)
57
+ idx_ppg = extractor.get_index(ppg_config.ppg_colname)
58
+ start_time_ppg = np.min(ppg_windowed[:, :, idx_time], axis=1) # Start time of the window is relative to the first datapoint in the PPG data
59
+ ppg_values_windowed = ppg_windowed[:, :, idx_ppg]
60
+
61
+ acc_windowed_cols = [DataColumns.TIME] + acc_config.accelerometer_cols
62
+ acc_windowed = tabulate_windows(
63
+ df=df_acc,
64
+ columns=acc_windowed_cols,
65
+ window_length_s=acc_config.window_length_s,
66
+ window_step_length_s=acc_config.window_step_length_s,
67
+ fs=acc_config.sampling_frequency
68
+ )
69
+
70
+ # Extract data from the windowed accelerometer signal
71
+ extractor = WindowedDataExtractor(acc_windowed_cols)
72
+ idx_acc = extractor.get_slice(acc_config.accelerometer_cols)
73
+ acc_values_windowed = acc_windowed[:, :, idx_acc]
74
+
75
+ df_features = pd.DataFrame(start_time_ppg, columns=[DataColumns.TIME])
76
+ # Compute features of the temporal domain of the PPG signal
77
+ df_temporal_features = extract_temporal_domain_features(ppg_values_windowed, ppg_config, quality_stats=['var', 'mean', 'median', 'kurtosis', 'skewness'])
78
+
79
+ # Combine temporal features with the start time
80
+ df_features = pd.concat([df_features, df_temporal_features], axis=1)
81
+
82
+ # Compute features of the spectral domain of the PPG signal
83
+ df_spectral_features = extract_spectral_domain_features(ppg_values_windowed, ppg_config)
84
+
85
+ # Combine the spectral features with the previously computed temporal features
86
+ df_features = pd.concat([df_features, df_spectral_features], axis=1)
87
+
88
+ # Compute periodicity feature of the accelerometer signal
89
+ df_accelerometer_feature = extract_accelerometer_feature(acc_values_windowed, ppg_values_windowed, acc_config)
90
+
91
+ # Combine the accelerometer feature with the previously computed features
92
+ df_features = pd.concat([df_features, df_accelerometer_feature], axis=1)
93
+
94
+ return df_features
95
+
96
+
97
+ def signal_quality_classification(df: pd.DataFrame, config: HeartRateConfig, full_path_to_classifier_package: str | Path) -> pd.DataFrame:
98
+ """
99
+ Classify the signal quality of the PPG signal using a logistic regression classifier. A probability close to 1 indicates a high-quality signal, while a probability close to 0 indicates a low-quality signal.
100
+ The classifier is trained on features extracted from the PPG signal. The features are extracted using the extract_signal_quality_features function.
101
+ The accelerometer signal is used to determine the signal quality based on the power ratio of the accelerometer signal and returns a binary label based on a threshold.
102
+ A value of 1 on the indicates no/minor periodic motion influence of the accelerometer on the PPG signal, 0 indicates major periodic motion influence.
103
+
104
+ Parameters
105
+ ----------
106
+ df : pd.DataFrame
107
+ The DataFrame containing the PPG features and the accelerometer feature for signal quality classification.
108
+ config : HeartRateConfig
109
+ The configuration for the signal quality classification.
110
+ full_path_to_classifier_package : str | Path
111
+ The path to the directory containing the classifier.
112
+
113
+ Returns
114
+ -------
115
+ df_sqa pd.DataFrame
116
+ The DataFrame containing the PPG signal quality predictions (both probabilities of the PPG signal quality classification and the accelerometer label based on the threshold).
117
+ """
118
+ clf_package = ClassifierPackage.load(full_path_to_classifier_package) # Load the classifier package
119
+ clf = clf_package.classifier # Load the logistic regression classifier
120
+
121
+ # Apply scaling to relevant columns
122
+ scaled_features = clf_package.transform_features(df.loc[:, clf.feature_names_in]) # Apply scaling to the features
123
+
124
+ # Make predictions for PPG signal quality assessment, and assign the probabilities to the DataFrame and drop the features
125
+ df[DataColumns.PRED_SQA_PROBA] = clf.predict_proba(scaled_features)[:, 0]
126
+ df[DataColumns.PRED_SQA_ACC_LABEL] = (df[DataColumns.ACC_POWER_RATIO] < config.threshold_sqa_accelerometer).astype(int) # Assign accelerometer label to the DataFrame based on the threshold
127
+
128
+ return df[[DataColumns.TIME, DataColumns.PRED_SQA_PROBA, DataColumns.PRED_SQA_ACC_LABEL]] # Return only the relevant columns, namely the predicted probabilities for the PPG signal quality and the accelerometer label
129
+
130
+
131
+ def estimate_heart_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame, config: HeartRateConfig) -> pd.DataFrame:
132
+ """
133
+ Estimate the heart rate from the PPG signal using the time-frequency domain method.
134
+
135
+ Parameters
136
+ ----------
137
+ df_sqa : pd.DataFrame
138
+ The DataFrame containing the signal quality assessment predictions.
139
+ df_ppg_preprocessed : pd.DataFrame
140
+ The DataFrame containing the preprocessed PPG signal.
141
+ config : HeartRateConfig
142
+ The configuration for the heart rate estimation.
143
+
144
+ Returns
145
+ -------
146
+ df_hr : pd.DataFrame
147
+ The DataFrame containing the heart rate estimations.
148
+ """
149
+
150
+ # Extract NumPy arrays for faster operations
151
+ ppg_post_prob = df_sqa[DataColumns.PRED_SQA_PROBA].to_numpy()
152
+ acc_label = df_sqa.loc[:, DataColumns.PRED_SQA_ACC_LABEL].to_numpy() # Adjust later in data columns to get the correct label, should be first intergrated in feature extraction and classification
153
+ ppg_preprocessed = df_ppg_preprocessed.values
154
+ time_idx = df_ppg_preprocessed.columns.get_loc(DataColumns.TIME) # Get the index of the time column
155
+ ppg_idx = df_ppg_preprocessed.columns.get_loc(DataColumns.PPG) # Get the index of the PPG column
156
+
157
+ # Assign window-level probabilities to individual samples
158
+ sqa_label = assign_sqa_label(ppg_post_prob, config, acc_label) # assigns a signal quality label to every individual data point
159
+ v_start_idx, v_end_idx = extract_hr_segments(sqa_label, config.min_hr_samples) # extracts heart rate segments based on the SQA label
160
+
161
+ v_hr_rel = np.array([])
162
+ t_hr_rel = np.array([])
163
+
164
+ edge_add = 2 * config.sampling_frequency # Add 2s on both sides of the segment for HR estimation
165
+ step_size = config.hr_est_samples # Step size for HR estimation
166
+
167
+ # Estimate the maximum size for preallocation
168
+ valid_segments = (v_start_idx >= edge_add) & (v_end_idx <= len(ppg_preprocessed) - edge_add) # check if the segments are valid, e.g. not too close to the edges (2s)
169
+ valid_start_idx = v_start_idx[valid_segments] # get the valid start indices
170
+ valid_end_idx = v_end_idx[valid_segments] # get the valid end indices
171
+ max_size = np.sum((valid_end_idx - valid_start_idx) // step_size) # maximum size for preallocation
172
+
173
+ # Preallocate arrays
174
+ v_hr_rel = np.empty(max_size, dtype=float)
175
+ t_hr_rel = np.empty(max_size, dtype=float)
176
+
177
+ # Track current position
178
+ hr_pos = 0
179
+
180
+ for start_idx, end_idx in zip(valid_start_idx, valid_end_idx):
181
+ # Extract extended PPG segment
182
+ extended_ppg_segment = ppg_preprocessed[start_idx - edge_add : end_idx + edge_add, ppg_idx]
183
+
184
+ # Estimate heart rate
185
+ hr_est = extract_hr_from_segment(
186
+ extended_ppg_segment,
187
+ config.tfd_length,
188
+ config.sampling_frequency,
189
+ config.kern_type,
190
+ config.kern_params,
191
+ )
192
+ n_hr = len(hr_est) # Number of heart rate estimates
193
+ end_idx_time = n_hr * step_size + start_idx # Calculate end index for time, different from end_idx since it is always a multiple of step_size, while end_idx is not
194
+
195
+ # Extract relative time for HR estimates
196
+ hr_time = ppg_preprocessed[start_idx : end_idx_time : step_size, time_idx]
197
+
198
+ # Insert into preallocated arrays
199
+ v_hr_rel[hr_pos:hr_pos + n_hr] = hr_est
200
+ t_hr_rel[hr_pos:hr_pos + n_hr] = hr_time
201
+ hr_pos += n_hr
202
+
203
+ df_hr = pd.DataFrame({"time": t_hr_rel, "heart_rate": v_hr_rel})
204
+
205
+ return df_hr
206
+
207
+
208
+ def aggregate_heart_rate(hr_values: np.ndarray, aggregates: List[str] = ['mode', '99p']) -> dict:
209
+ """
210
+ Aggregate the heart rate estimates using the specified aggregation methods.
211
+
212
+ Parameters
213
+ ----------
214
+ hr_values : np.ndarray
215
+ The array containing the heart rate estimates
216
+ aggregates : List[str]
217
+ The list of aggregation methods to be used for the heart rate estimates. The default is ['mode', '99p'].
218
+
219
+ Returns
220
+ -------
221
+ aggregated_results : dict
222
+ The dictionary containing the aggregated results of the heart rate estimates.
223
+ """
224
+ # Initialize the dictionary for the aggregated results
225
+ aggregated_results = {}
226
+
227
+ # Initialize the dictionary for the aggregated results with the metadata
228
+ aggregated_results = {
229
+ 'metadata': {
230
+ 'nr_hr_est': len(hr_values)
231
+ },
232
+ 'hr_aggregates': {}
233
+ }
234
+ for aggregate in aggregates:
235
+ aggregated_results['hr_aggregates'][f'{aggregate}_{DataColumns.HEART_RATE}'] = aggregate_parameter(hr_values, aggregate)
236
+
237
+ return aggregated_results
238
+
239
+
240
+ def extract_temporal_domain_features(
241
+ ppg_windowed: np.ndarray,
242
+ config: HeartRateConfig,
243
+ quality_stats: List[str] = ['mean', 'std']
244
+ ) -> pd.DataFrame:
245
+ """
246
+ Compute temporal domain features for the ppg signal. The features are added to the dataframe. Therefore the original dataframe is modified, and the modified dataframe is returned.
247
+
248
+ Parameters
249
+ ----------
250
+ ppg_windowed: np.ndarray
251
+ The dataframe containing the windowed accelerometer signal
252
+
253
+ config: HeartRateConfig
254
+ The configuration object containing the parameters for the feature extraction
255
+
256
+ quality_stats: list, optional
257
+ The statistics to be computed for the gravity component of the accelerometer signal (default: ['mean', 'std'])
258
+
259
+ Returns
260
+ -------
261
+ pd.DataFrame
262
+ The dataframe with the added temporal domain features.
263
+ """
264
+
265
+ feature_dict = {}
266
+ for stat in quality_stats:
267
+ feature_dict[stat] = compute_statistics(ppg_windowed, stat, abs_stats=True)
268
+
269
+ feature_dict['signal_to_noise'] = compute_signal_to_noise_ratio(ppg_windowed)
270
+ feature_dict['auto_corr'] = compute_auto_correlation(ppg_windowed, config.sampling_frequency)
271
+ return pd.DataFrame(feature_dict)
272
+
273
+
274
+ def extract_spectral_domain_features(
275
+ ppg_windowed: np.ndarray,
276
+ config: HeartRateConfig,
277
+ ) -> pd.DataFrame:
278
+ """
279
+ Calculate the spectral features (dominant frequency, relative power, and spectral entropy)
280
+ for each segment of a PPG signal using a single Welch's method computation. The features are added to the dataframe.
281
+ Therefore the original dataframe is modified, and the modified dataframe is returned.
282
+
283
+ Parameters
284
+ ----------
285
+ ppg_windowed: np.ndarray
286
+ The dataframe containing the windowed ppg signal
287
+
288
+ config: HeartRateConfig
289
+ The configuration object containing the parameters for the feature extraction
290
+
291
+ Returns
292
+ -------
293
+ pd.DataFrame
294
+ The dataframe with the added spectral domain features.
295
+ """
296
+ d_features = {}
297
+
298
+ window = hamming(config.window_length_welch, sym = True)
299
+
300
+ n_samples_window = ppg_windowed.shape[1]
301
+
302
+ freqs, psd = welch(
303
+ ppg_windowed,
304
+ fs=config.sampling_frequency,
305
+ window=window,
306
+ noverlap=config.overlap_welch_window,
307
+ nfft=max(256, 2 ** int(np.log2(n_samples_window))),
308
+ detrend=False,
309
+ axis=1
310
+ )
311
+
312
+ # Calculate each feature using the computed PSD and frequency array
313
+ d_features['f_dom'] = compute_dominant_frequency(freqs, psd)
314
+ d_features['rel_power'] = compute_relative_power(freqs, psd, config)
315
+ d_features['spectral_entropy'] = compute_spectral_entropy(psd, n_samples_window)
316
+
317
+ return pd.DataFrame(d_features)
318
+
319
+
320
+ def extract_acc_power_feature(
321
+ f1: np.ndarray,
322
+ PSD_acc: np.ndarray,
323
+ f2: np.ndarray,
324
+ PSD_ppg: np.ndarray
325
+ ) -> np.ndarray:
326
+ """
327
+ Extract the accelerometer power feature in the PPG frequency range.
328
+
329
+ Parameters
330
+ ----------
331
+ f1: np.ndarray
332
+ The frequency bins of the accelerometer signal.
333
+ PSD_acc: np.ndarray
334
+ The power spectral density of the accelerometer signal.
335
+ f2: np.ndarray
336
+ The frequency bins of the PPG signal.
337
+ PSD_ppg: np.ndarray
338
+ The power spectral density of the PPG signal.
339
+
340
+ Returns
341
+ -------
342
+ np.ndarray
343
+ The accelerometer power feature in the PPG frequency range
344
+ """
345
+
346
+ # Find the index of the maximum PSD value in the PPG signal
347
+ max_PPG_psd_idx = np.argmax(PSD_ppg, axis=1)
348
+ max_PPG_freq_psd = f2[max_PPG_psd_idx]
349
+
350
+ # Find the neighboring indices of the maximum PSD value in the PPG signal
351
+ df_idx = np.column_stack((max_PPG_psd_idx - 1, max_PPG_psd_idx, max_PPG_psd_idx + 1))
352
+
353
+ # Find the index of the closest frequency in the accelerometer signal to the first harmonic of the PPG frequency
354
+ corr_acc_psd_fh_idx = np.argmin(np.abs(f1[:, None] - max_PPG_freq_psd*2), axis=0)
355
+ fh_idx = np.column_stack((corr_acc_psd_fh_idx - 1, corr_acc_psd_fh_idx, corr_acc_psd_fh_idx + 1))
356
+
357
+ # Compute the power in the ranges corresponding to the PPG frequency
358
+ acc_power_PPG_range = (
359
+ np.trapz(PSD_acc[np.arange(PSD_acc.shape[0])[:, None], df_idx], f1[df_idx], axis=1) +
360
+ np.trapz(PSD_acc[np.arange(PSD_acc.shape[0])[:, None], fh_idx], f1[fh_idx], axis=1)
361
+ )
362
+
363
+ # Compute the total power across the entire frequency range
364
+ acc_power_total = np.trapz(PSD_acc, f1)
365
+
366
+ # Compute the power ratio of the accelerometer signal in the PPG frequency range
367
+ acc_power_ratio = acc_power_PPG_range / acc_power_total
368
+
369
+ return acc_power_ratio
370
+
371
+ def extract_accelerometer_feature(
372
+ acc_windowed: np.ndarray,
373
+ ppg_windowed: np.ndarray,
374
+ config: HeartRateConfig
375
+ ) -> pd.DataFrame:
376
+ """
377
+ Extract accelerometer features from the accelerometer signal in the PPG frequency range.
378
+
379
+ Parameters
380
+ ----------
381
+ acc_windowed: np.ndarray
382
+ The dataframe containing the windowed accelerometer signal
383
+
384
+ ppg_windowed: np.ndarray
385
+ The dataframe containing the corresponding windowed ppg signal
386
+
387
+ config: HeartRateConfig
388
+ The configuration object containing the parameters for the feature extraction
389
+
390
+ Returns
391
+ -------
392
+ pd.DataFrame
393
+ The dataframe with the relative power accelerometer feature.
394
+ """
395
+
396
+ if config.sensor not in ['imu', 'ppg']:
397
+ raise ValueError("Sensor not recognized.")
398
+
399
+ d_freq = {}
400
+ d_psd = {}
401
+ for sensor in ['imu', 'ppg']:
402
+ config.set_sensor(sensor)
403
+
404
+ if sensor == 'imu':
405
+ windows = acc_windowed
406
+ else:
407
+ windows = ppg_windowed
408
+
409
+ window_type = hann(config.window_length_welch, sym = True)
410
+ d_freq[sensor], d_psd[sensor] = welch(
411
+ windows,
412
+ fs=config.sampling_frequency,
413
+ window=window_type,
414
+ noverlap=config.overlap_welch_window,
415
+ nfft=config.nfft,
416
+ detrend=False,
417
+ axis=1
418
+ )
419
+
420
+ d_psd['imu'] = np.sum(d_psd['imu'], axis=2) # Sum the PSDs of the three axes
421
+
422
+ acc_power_ratio = extract_acc_power_feature(d_freq['imu'], d_psd['imu'], d_freq['ppg'], d_psd['ppg'])
423
+
424
+ return pd.DataFrame(acc_power_ratio, columns=['acc_power_ratio'])
425
+
426
+