paradigma 1.0.2__py3-none-any.whl → 1.0.4__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.
@@ -1,24 +1,37 @@
1
- import json
1
+ from typing import List
2
+
2
3
  import numpy as np
3
- import os
4
4
  import pandas as pd
5
- from pathlib import Path
6
5
  from scipy.signal import welch
7
6
  from scipy.signal.windows import hamming, hann
8
- import tsdf
9
- from typing import List
10
7
 
11
8
  from paradigma.classification import ClassifierPackage
12
- from paradigma.constants import DataColumns
13
9
  from paradigma.config import PulseRateConfig
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.pulse_rate_utils import assign_sqa_label, extract_pr_segments, extract_pr_from_segment
17
- from paradigma.segmenting import tabulate_windows, WindowedDataExtractor
10
+ from paradigma.constants import DataColumns
11
+ from paradigma.feature_extraction import (
12
+ compute_auto_correlation,
13
+ compute_dominant_frequency,
14
+ compute_relative_power,
15
+ compute_signal_to_noise_ratio,
16
+ compute_spectral_entropy,
17
+ compute_statistics,
18
+ )
19
+ from paradigma.pipelines.pulse_rate_utils import (
20
+ assign_sqa_label,
21
+ extract_pr_from_segment,
22
+ extract_pr_segments,
23
+ )
24
+ from paradigma.segmenting import WindowedDataExtractor, tabulate_windows
18
25
  from paradigma.util import aggregate_parameter
19
26
 
20
- def extract_signal_quality_features(df_ppg: pd.DataFrame, df_acc: pd.DataFrame, ppg_config: PulseRateConfig, acc_config: PulseRateConfig) -> pd.DataFrame:
21
- """
27
+
28
+ def extract_signal_quality_features(
29
+ df_ppg: pd.DataFrame,
30
+ ppg_config: PulseRateConfig,
31
+ df_acc: pd.DataFrame | None = None,
32
+ acc_config: PulseRateConfig | None = None,
33
+ ) -> pd.DataFrame:
34
+ """
22
35
  Extract signal quality features from the PPG signal.
23
36
  The features are extracted from the temporal and spectral domain of the PPG signal.
24
37
  The temporal domain features include variance, mean, median, kurtosis, skewness, signal-to-noise ratio, and autocorrelation.
@@ -39,62 +52,78 @@ def extract_signal_quality_features(df_ppg: pd.DataFrame, df_acc: pd.DataFrame,
39
52
  -------
40
53
  df_features : pd.DataFrame
41
54
  The DataFrame containing the extracted signal quality features.
42
-
55
+
43
56
  """
44
57
  # Group sequences of timestamps into windows
45
- ppg_windowed_cols = [DataColumns.TIME, ppg_config.ppg_colname]
58
+ ppg_windowed_colnames = [ppg_config.time_colname, ppg_config.ppg_colname]
46
59
  ppg_windowed = tabulate_windows(
47
- df=df_ppg,
48
- columns=ppg_windowed_cols,
60
+ df=df_ppg,
61
+ columns=ppg_windowed_colnames,
49
62
  window_length_s=ppg_config.window_length_s,
50
63
  window_step_length_s=ppg_config.window_step_length_s,
51
- fs=ppg_config.sampling_frequency
64
+ fs=ppg_config.sampling_frequency,
52
65
  )
53
66
 
54
67
  # Extract data from the windowed PPG signal
55
- extractor = WindowedDataExtractor(ppg_windowed_cols)
56
- idx_time = extractor.get_index(DataColumns.TIME)
68
+ extractor = WindowedDataExtractor(ppg_windowed_colnames)
69
+ idx_time = extractor.get_index(ppg_config.time_colname)
57
70
  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
71
+ start_time_ppg = np.min(
72
+ ppg_windowed[:, :, idx_time], axis=1
73
+ ) # Start time of the window is relative to the first datapoint in the PPG data
59
74
  ppg_values_windowed = ppg_windowed[:, :, idx_ppg]
60
75
 
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
- )
76
+ df_features = pd.DataFrame(start_time_ppg, columns=[ppg_config.time_colname])
77
+
78
+ if df_acc is not None and acc_config is not None:
79
+
80
+ acc_windowed_colnames = [
81
+ acc_config.time_colname
82
+ ] + acc_config.accelerometer_colnames
83
+ acc_windowed = tabulate_windows(
84
+ df=df_acc,
85
+ columns=acc_windowed_colnames,
86
+ window_length_s=acc_config.window_length_s,
87
+ window_step_length_s=acc_config.window_step_length_s,
88
+ fs=acc_config.sampling_frequency,
89
+ )
69
90
 
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]
91
+ # Extract data from the windowed accelerometer signal
92
+ extractor = WindowedDataExtractor(acc_windowed_colnames)
93
+ idx_acc = extractor.get_slice(acc_config.accelerometer_colnames)
94
+ acc_values_windowed = acc_windowed[:, :, idx_acc]
95
+
96
+ # Compute periodicity feature of the accelerometer signal
97
+ df_accelerometer_feature = extract_accelerometer_feature(
98
+ acc_values_windowed, ppg_values_windowed, acc_config
99
+ )
100
+ # Combine the accelerometer feature with the previously computed features
101
+ df_features = pd.concat([df_features, df_accelerometer_feature], axis=1)
74
102
 
75
- df_features = pd.DataFrame(start_time_ppg, columns=[DataColumns.TIME])
76
103
  # 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
-
104
+ df_temporal_features = extract_temporal_domain_features(
105
+ ppg_values_windowed,
106
+ ppg_config,
107
+ quality_stats=["var", "mean", "median", "kurtosis", "skewness"],
108
+ )
109
+
79
110
  # Combine temporal features with the start time
80
111
  df_features = pd.concat([df_features, df_temporal_features], axis=1)
81
112
 
82
113
  # Compute features of the spectral domain of the PPG signal
83
- df_spectral_features = extract_spectral_domain_features(ppg_values_windowed, ppg_config)
114
+ df_spectral_features = extract_spectral_domain_features(
115
+ ppg_values_windowed, ppg_config
116
+ )
84
117
 
85
118
  # Combine the spectral features with the previously computed temporal features
86
119
  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
120
 
94
121
  return df_features
95
122
 
96
123
 
97
- def signal_quality_classification(df: pd.DataFrame, config: PulseRateConfig, full_path_to_classifier_package: str | Path) -> pd.DataFrame:
124
+ def signal_quality_classification(
125
+ df: pd.DataFrame, config: PulseRateConfig, clf_package: ClassifierPackage
126
+ ) -> pd.DataFrame:
98
127
  """
99
128
  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
129
  The classifier is trained on features extracted from the PPG signal. The features are extracted using the extract_signal_quality_features function.
@@ -107,28 +136,40 @@ def signal_quality_classification(df: pd.DataFrame, config: PulseRateConfig, ful
107
136
  The DataFrame containing the PPG features and the accelerometer feature for signal quality classification.
108
137
  config : PulseRateConfig
109
138
  The configuration for the signal quality classification.
110
- full_path_to_classifier_package : str | Path
111
- The path to the directory containing the classifier.
139
+ clf_package : ClassifierPackage
140
+ The classifier package containing the classifier and scaler.
112
141
 
113
142
  Returns
114
143
  -------
115
144
  df_sqa pd.DataFrame
116
145
  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
146
  """
118
- clf_package = ClassifierPackage.load(full_path_to_classifier_package) # Load the classifier package
147
+ # Set classifier
119
148
  clf = clf_package.classifier # Load the logistic regression classifier
120
149
 
121
150
  # Apply scaling to relevant columns
122
- scaled_features = clf_package.transform_features(df.loc[:, clf.feature_names_in]) # Apply scaling to the features
151
+ scaled_features = clf_package.transform_features(
152
+ df.loc[:, clf.feature_names_in]
153
+ ) # Apply scaling to the features
123
154
 
124
155
  # Make predictions for PPG signal quality assessment, and assign the probabilities to the DataFrame and drop the features
125
156
  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
157
+ keep_cols = [config.time_colname, DataColumns.PRED_SQA_PROBA]
129
158
 
159
+ if DataColumns.ACC_POWER_RATIO in df.columns:
160
+ df[DataColumns.PRED_SQA_ACC_LABEL] = (
161
+ df[DataColumns.ACC_POWER_RATIO] < config.threshold_sqa_accelerometer
162
+ ).astype(
163
+ int
164
+ ) # Assign accelerometer label to the DataFrame based on the threshold
165
+ keep_cols += [DataColumns.PRED_SQA_ACC_LABEL]
130
166
 
131
- def estimate_pulse_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame, config: PulseRateConfig) -> pd.DataFrame:
167
+ return df[keep_cols]
168
+
169
+
170
+ def estimate_pulse_rate(
171
+ df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame, config: PulseRateConfig
172
+ ) -> pd.DataFrame:
132
173
  """
133
174
  Estimate the pulse rate from the PPG signal using the time-frequency domain method.
134
175
 
@@ -149,37 +190,58 @@ def estimate_pulse_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame,
149
190
 
150
191
  # Extract NumPy arrays for faster operations
151
192
  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
-
193
+
194
+ if DataColumns.PRED_SQA_ACC_LABEL in df_sqa.columns:
195
+ acc_label = df_sqa[DataColumns.PRED_SQA_ACC_LABEL].to_numpy()
196
+ else:
197
+ acc_label = None
198
+
199
+ ppg_preprocessed = df_ppg_preprocessed.values
200
+ time_idx = df_ppg_preprocessed.columns.get_loc(
201
+ config.time_colname
202
+ ) # Get the index of the time column
203
+ ppg_idx = df_ppg_preprocessed.columns.get_loc(
204
+ config.ppg_colname
205
+ ) # Get the index of the PPG column
206
+
157
207
  # 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_pr_segments(sqa_label, config.min_pr_samples) # extracts pulse rate segments based on the SQA label
160
-
208
+ sqa_label = assign_sqa_label(
209
+ ppg_post_prob, config, acc_label
210
+ ) # assigns a signal quality label to every individual data point
211
+ v_start_idx, v_end_idx = extract_pr_segments(
212
+ sqa_label, config.min_pr_samples
213
+ ) # extracts pulse rate segments based on the SQA label
214
+
161
215
  v_pr_rel = np.array([])
162
216
  t_pr_rel = np.array([])
163
217
 
164
- edge_add = 2 * config.sampling_frequency # Add 2s on both sides of the segment for PR estimation
218
+ edge_add = (
219
+ 2 * config.sampling_frequency
220
+ ) # Add 2s on both sides of the segment for PR estimation
165
221
  step_size = config.pr_est_samples # Step size for PR estimation
166
222
 
167
223
  # 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
-
224
+ valid_segments = (v_start_idx >= edge_add) & (
225
+ v_end_idx <= len(ppg_preprocessed) - edge_add
226
+ ) # check if the segments are valid, e.g. not too close to the edges (2s)
227
+ valid_start_idx = v_start_idx[valid_segments] # get the valid start indices
228
+ valid_end_idx = v_end_idx[valid_segments] # get the valid end indices
229
+ max_size = np.sum(
230
+ (valid_end_idx - valid_start_idx) // step_size
231
+ ) # maximum size for preallocation
232
+
173
233
  # Preallocate arrays
174
- v_pr_rel = np.empty(max_size, dtype=float)
175
- t_pr_rel = np.empty(max_size, dtype=float)
234
+ v_pr_rel = np.empty(max_size, dtype=float)
235
+ t_pr_rel = np.empty(max_size, dtype=float)
176
236
 
177
237
  # Track current position
178
238
  pr_pos = 0
179
239
 
180
240
  for start_idx, end_idx in zip(valid_start_idx, valid_end_idx):
181
241
  # Extract extended PPG segment
182
- extended_ppg_segment = ppg_preprocessed[start_idx - edge_add : end_idx + edge_add, ppg_idx]
242
+ extended_ppg_segment = ppg_preprocessed[
243
+ start_idx - edge_add : end_idx + edge_add, ppg_idx
244
+ ]
183
245
 
184
246
  # Estimate pulse rate
185
247
  pr_est = extract_pr_from_segment(
@@ -190,14 +252,16 @@ def estimate_pulse_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame,
190
252
  config.kern_params,
191
253
  )
192
254
  n_pr = len(pr_est) # Number of pulse rate estimates
193
- end_idx_time = n_pr * 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
255
+ end_idx_time = (
256
+ n_pr * step_size + start_idx
257
+ ) # Calculate end index for time, different from end_idx since it is always a multiple of step_size, while end_idx is not
194
258
 
195
259
  # Extract relative time for PR estimates
196
- pr_time = ppg_preprocessed[start_idx : end_idx_time : step_size, time_idx]
260
+ pr_time = ppg_preprocessed[start_idx:end_idx_time:step_size, time_idx]
197
261
 
198
262
  # Insert into preallocated arrays
199
- v_pr_rel[pr_pos:pr_pos + n_pr] = pr_est
200
- t_pr_rel[pr_pos:pr_pos + n_pr] = pr_time
263
+ v_pr_rel[pr_pos : pr_pos + n_pr] = pr_est
264
+ t_pr_rel[pr_pos : pr_pos + n_pr] = pr_time
201
265
  pr_pos += n_pr
202
266
 
203
267
  df_pr = pd.DataFrame({"time": t_pr_rel, "pulse_rate": v_pr_rel})
@@ -205,7 +269,9 @@ def estimate_pulse_rate(df_sqa: pd.DataFrame, df_ppg_preprocessed: pd.DataFrame,
205
269
  return df_pr
206
270
 
207
271
 
208
- def aggregate_pulse_rate(pr_values: np.ndarray, aggregates: List[str] = ['mode', '99p']) -> dict:
272
+ def aggregate_pulse_rate(
273
+ pr_values: np.ndarray, aggregates: List[str] = ["mode", "99p"]
274
+ ) -> dict:
209
275
  """
210
276
  Aggregate the pulse rate estimates using the specified aggregation methods.
211
277
 
@@ -226,22 +292,22 @@ def aggregate_pulse_rate(pr_values: np.ndarray, aggregates: List[str] = ['mode',
226
292
 
227
293
  # Initialize the dictionary for the aggregated results with the metadata
228
294
  aggregated_results = {
229
- 'metadata': {
230
- 'nr_pr_est': len(pr_values)
231
- },
232
- 'pr_aggregates': {}
233
- }
295
+ "metadata": {"nr_pr_est": len(pr_values)},
296
+ "pr_aggregates": {},
297
+ }
234
298
  for aggregate in aggregates:
235
- aggregated_results['pr_aggregates'][f'{aggregate}_{DataColumns.PULSE_RATE}'] = aggregate_parameter(pr_values, aggregate)
299
+ aggregated_results["pr_aggregates"][f"{aggregate}_{DataColumns.PULSE_RATE}"] = (
300
+ aggregate_parameter(pr_values, aggregate)
301
+ )
236
302
 
237
303
  return aggregated_results
238
304
 
239
305
 
240
306
  def extract_temporal_domain_features(
241
- ppg_windowed: np.ndarray,
242
- config: PulseRateConfig,
243
- quality_stats: List[str] = ['mean', 'std']
244
- ) -> pd.DataFrame:
307
+ ppg_windowed: np.ndarray,
308
+ config: PulseRateConfig,
309
+ quality_stats: List[str] = ["mean", "std"],
310
+ ) -> pd.DataFrame:
245
311
  """
246
312
  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
313
 
@@ -255,29 +321,31 @@ def extract_temporal_domain_features(
255
321
 
256
322
  quality_stats: list, optional
257
323
  The statistics to be computed for the gravity component of the accelerometer signal (default: ['mean', 'std'])
258
-
324
+
259
325
  Returns
260
326
  -------
261
327
  pd.DataFrame
262
328
  The dataframe with the added temporal domain features.
263
329
  """
264
-
330
+
265
331
  feature_dict = {}
266
332
  for stat in quality_stats:
267
333
  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)
334
+
335
+ feature_dict["signal_to_noise"] = compute_signal_to_noise_ratio(ppg_windowed)
336
+ feature_dict["auto_corr"] = compute_auto_correlation(
337
+ ppg_windowed, config.sampling_frequency
338
+ )
271
339
  return pd.DataFrame(feature_dict)
272
340
 
273
341
 
274
342
  def extract_spectral_domain_features(
275
- ppg_windowed: np.ndarray,
276
- config: PulseRateConfig,
277
- ) -> pd.DataFrame:
343
+ ppg_windowed: np.ndarray,
344
+ config: PulseRateConfig,
345
+ ) -> pd.DataFrame:
278
346
  """
279
347
  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.
348
+ for each segment of a PPG signal using a single Welch's method computation. The features are added to the dataframe.
281
349
  Therefore the original dataframe is modified, and the modified dataframe is returned.
282
350
 
283
351
  Parameters
@@ -295,7 +363,7 @@ def extract_spectral_domain_features(
295
363
  """
296
364
  d_features = {}
297
365
 
298
- window = hamming(config.window_length_welch, sym = True)
366
+ window = hamming(config.window_length_welch, sym=True)
299
367
 
300
368
  n_samples_window = ppg_windowed.shape[1]
301
369
 
@@ -306,23 +374,20 @@ def extract_spectral_domain_features(
306
374
  noverlap=config.overlap_welch_window,
307
375
  nfft=max(256, 2 ** int(np.log2(n_samples_window))),
308
376
  detrend=False,
309
- axis=1
377
+ axis=1,
310
378
  )
311
379
 
312
380
  # 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)
381
+ d_features["f_dom"] = compute_dominant_frequency(freqs, psd)
382
+ d_features["rel_power"] = compute_relative_power(freqs, psd, config)
383
+ d_features["spectral_entropy"] = compute_spectral_entropy(psd, n_samples_window)
316
384
 
317
385
  return pd.DataFrame(d_features)
318
386
 
319
387
 
320
388
  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:
389
+ f1: np.ndarray, PSD_acc: np.ndarray, f2: np.ndarray, PSD_ppg: np.ndarray
390
+ ) -> np.ndarray:
326
391
  """
327
392
  Extract the accelerometer power feature in the PPG frequency range.
328
393
 
@@ -342,48 +407,52 @@ def extract_acc_power_feature(
342
407
  np.ndarray
343
408
  The accelerometer power feature in the PPG frequency range
344
409
  """
345
-
410
+
346
411
  # Find the index of the maximum PSD value in the PPG signal
347
412
  max_PPG_psd_idx = np.argmax(PSD_ppg, axis=1)
348
413
  max_PPG_freq_psd = f2[max_PPG_psd_idx]
349
-
414
+
350
415
  # 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
-
416
+ df_idx = np.column_stack(
417
+ (max_PPG_psd_idx - 1, max_PPG_psd_idx, max_PPG_psd_idx + 1)
418
+ )
419
+
353
420
  # 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
-
421
+ corr_acc_psd_fh_idx = np.argmin(np.abs(f1[:, None] - max_PPG_freq_psd * 2), axis=0)
422
+ fh_idx = np.column_stack(
423
+ (corr_acc_psd_fh_idx - 1, corr_acc_psd_fh_idx, corr_acc_psd_fh_idx + 1)
424
+ )
425
+
357
426
  # 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)
427
+ acc_power_PPG_range = np.trapz(
428
+ PSD_acc[np.arange(PSD_acc.shape[0])[:, None], df_idx], f1[df_idx], axis=1
429
+ ) + np.trapz(
430
+ PSD_acc[np.arange(PSD_acc.shape[0])[:, None], fh_idx], f1[fh_idx], axis=1
361
431
  )
362
432
 
363
433
  # Compute the total power across the entire frequency range
364
434
  acc_power_total = np.trapz(PSD_acc, f1)
365
-
435
+
366
436
  # Compute the power ratio of the accelerometer signal in the PPG frequency range
367
437
  acc_power_ratio = acc_power_PPG_range / acc_power_total
368
-
438
+
369
439
  return acc_power_ratio
370
440
 
441
+
371
442
  def extract_accelerometer_feature(
372
- acc_windowed: np.ndarray,
373
- ppg_windowed: np.ndarray,
374
- config: PulseRateConfig
375
- ) -> pd.DataFrame:
443
+ acc_windowed: np.ndarray, ppg_windowed: np.ndarray, config: PulseRateConfig
444
+ ) -> pd.DataFrame:
376
445
  """
377
446
  Extract accelerometer features from the accelerometer signal in the PPG frequency range.
378
-
447
+
379
448
  Parameters
380
- ----------
449
+ ----------
381
450
  acc_windowed: np.ndarray
382
451
  The dataframe containing the windowed accelerometer signal
383
452
 
384
453
  ppg_windowed: np.ndarray
385
454
  The dataframe containing the corresponding windowed ppg signal
386
-
455
+
387
456
  config: PulseRateConfig
388
457
  The configuration object containing the parameters for the feature extraction
389
458
 
@@ -392,21 +461,21 @@ def extract_accelerometer_feature(
392
461
  pd.DataFrame
393
462
  The dataframe with the relative power accelerometer feature.
394
463
  """
395
-
396
- if config.sensor not in ['imu', 'ppg']:
464
+
465
+ if config.sensor not in ["imu", "ppg"]:
397
466
  raise ValueError("Sensor not recognized.")
398
-
467
+
399
468
  d_freq = {}
400
469
  d_psd = {}
401
- for sensor in ['imu', 'ppg']:
470
+ for sensor in ["imu", "ppg"]:
402
471
  config.set_sensor(sensor)
403
472
 
404
- if sensor == 'imu':
473
+ if sensor == "imu":
405
474
  windows = acc_windowed
406
475
  else:
407
476
  windows = ppg_windowed
408
477
 
409
- window_type = hann(config.window_length_welch, sym = True)
478
+ window_type = hann(config.window_length_welch, sym=True)
410
479
  d_freq[sensor], d_psd[sensor] = welch(
411
480
  windows,
412
481
  fs=config.sampling_frequency,
@@ -414,13 +483,13 @@ def extract_accelerometer_feature(
414
483
  noverlap=config.overlap_welch_window,
415
484
  nfft=config.nfft,
416
485
  detrend=False,
417
- axis=1
486
+ axis=1,
418
487
  )
419
488
 
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'])
489
+ d_psd["imu"] = np.sum(d_psd["imu"], axis=2) # Sum the PSDs of the three axes
425
490
 
491
+ acc_power_ratio = extract_acc_power_feature(
492
+ d_freq["imu"], d_psd["imu"], d_freq["ppg"], d_psd["ppg"]
493
+ )
426
494
 
495
+ return pd.DataFrame(acc_power_ratio, columns=["acc_power_ratio"])