paradigma 1.0.3__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.
@@ -1,7 +1,5 @@
1
1
  import numpy as np
2
2
  import pandas as pd
3
- from typing import List, Tuple
4
-
5
3
  from scipy.integrate import cumulative_trapezoid
6
4
  from scipy.signal import find_peaks, windows
7
5
  from scipy.stats import kurtosis, skew
@@ -10,7 +8,9 @@ from sklearn.decomposition import PCA
10
8
  from paradigma.config import PulseRateConfig
11
9
 
12
10
 
13
- def compute_statistics(data: np.ndarray, statistic: str, abs_stats: bool=False) -> np.ndarray:
11
+ def compute_statistics(
12
+ data: np.ndarray, statistic: str, abs_stats: bool = False
13
+ ) -> np.ndarray:
14
14
  """
15
15
  Compute a specific statistical measure along the timestamps of a 2D or 3D array.
16
16
 
@@ -29,7 +29,7 @@ def compute_statistics(data: np.ndarray, statistic: str, abs_stats: bool=False)
29
29
  - 'kurtosis': Kurtosis.
30
30
  - 'skewness': Skewness.
31
31
  abs_stats : bool, optional
32
- Whether to compute the statistics on the absolute values of the data for
32
+ Whether to compute the statistics on the absolute values of the data for
33
33
  the mean and median (default: False).
34
34
 
35
35
  Returns
@@ -41,35 +41,45 @@ def compute_statistics(data: np.ndarray, statistic: str, abs_stats: bool=False)
41
41
  Raises
42
42
  ------
43
43
  ValueError
44
- If the specified `statistic` is not supported or if the input data has an invalid shape.
44
+ If the specified `statistic` is not supported or if the input data
45
+ has an invalid shape.
45
46
  """
46
- if statistic not in ['mean', 'median', 'var', 'std', 'max', 'min', 'kurtosis', 'skewness']:
47
+ if statistic not in [
48
+ "mean",
49
+ "median",
50
+ "var",
51
+ "std",
52
+ "max",
53
+ "min",
54
+ "kurtosis",
55
+ "skewness",
56
+ ]:
47
57
  raise ValueError(f"Statistic '{statistic}' is not supported.")
48
-
58
+
49
59
  if data.ndim > 3 or data.ndim < 2:
50
60
  raise ValueError("Input data must be a 1D, 2D or 3D array.")
51
61
 
52
- if statistic == 'mean':
62
+ if statistic == "mean":
53
63
  if abs_stats:
54
64
  return np.mean(np.abs(data), axis=1)
55
65
  else:
56
66
  return np.mean(data, axis=1)
57
- elif statistic == 'median':
67
+ elif statistic == "median":
58
68
  if abs_stats:
59
69
  return np.median(np.abs(data), axis=1)
60
70
  else:
61
71
  return np.median(data, axis=1)
62
- elif statistic == 'var':
72
+ elif statistic == "var":
63
73
  return np.var(data, ddof=1, axis=1)
64
- elif statistic == 'std':
74
+ elif statistic == "std":
65
75
  return np.std(data, axis=1)
66
- elif statistic == 'max':
76
+ elif statistic == "max":
67
77
  return np.max(data, axis=1)
68
- elif statistic == 'min':
78
+ elif statistic == "min":
69
79
  return np.min(data, axis=1)
70
- elif statistic == 'kurtosis':
80
+ elif statistic == "kurtosis":
71
81
  return kurtosis(data, fisher=False, axis=1)
72
- elif statistic == 'skewness':
82
+ elif statistic == "skewness":
73
83
  return skew(data, axis=1)
74
84
  else:
75
85
  raise ValueError(f"Statistic '{statistic}' is not supported.")
@@ -79,8 +89,8 @@ def compute_std_euclidean_norm(data: np.ndarray) -> np.ndarray:
79
89
  """
80
90
  Compute the standard deviation of the Euclidean norm for each window of sensor data.
81
91
 
82
- The function calculates the Euclidean norm (L2 norm) across sensor axes for each
83
- timestamp within a window, and then computes the standard deviation of these norms
92
+ The function calculates the Euclidean norm (L2 norm) across sensor axes for each
93
+ timestamp within a window, and then computes the standard deviation of these norms
84
94
  for each window.
85
95
 
86
96
  Parameters
@@ -94,36 +104,40 @@ def compute_std_euclidean_norm(data: np.ndarray) -> np.ndarray:
94
104
  Returns
95
105
  -------
96
106
  np.ndarray
97
- A 1D array of shape (n_windows,) containing the standard deviation of the
107
+ A 1D array of shape (n_windows,) containing the standard deviation of the
98
108
  Euclidean norm for each window.
99
109
  """
100
- norms = np.linalg.norm(data, axis=2) # Norm along the sensor axes (norm per timestamp, per window)
110
+ norms = np.linalg.norm(
111
+ data, axis=2
112
+ ) # Norm along the sensor axes (norm per timestamp, per window)
101
113
  return np.std(norms, axis=1) # Standard deviation per window
102
114
 
103
115
 
104
116
  def compute_power_in_bandwidth(
105
- freqs: np.ndarray,
106
- psd: np.ndarray,
107
- fmin: float,
108
- fmax: float,
109
- include_max: bool = True,
110
- spectral_resolution: float = 1,
111
- cumulative_sum_method: str = 'trapz'
112
- ) -> np.ndarray:
117
+ freqs: np.ndarray,
118
+ psd: np.ndarray,
119
+ fmin: float,
120
+ fmax: float,
121
+ include_max: bool = True,
122
+ spectral_resolution: float = 1,
123
+ cumulative_sum_method: str = "trapezoid",
124
+ ) -> np.ndarray:
113
125
  """
114
126
  Compute the logarithmic power within specified frequency bands for each sensor axis.
115
127
 
116
- This function integrates the power spectral density (PSD) over user-defined frequency
117
- bands and computes the logarithm of the resulting power for each axis of the sensor.
128
+ This function integrates the power spectral density (PSD) over
129
+ user-defined frequency bands and computes the logarithm of the
130
+ resulting power for each axis of the sensor.
118
131
 
119
132
  Parameters
120
133
  ----------
121
134
  freqs : np.ndarray
122
- A 1D array of shape (n_frequencies,) containing the frequencies corresponding
135
+ A 1D array of shape (n_frequencies,) containing the frequencies corresponding
123
136
  to the PSD values.
124
137
  psd : np.ndarray
125
- A 2D array of shape (n_windows, n_frequencies) or 3D array of shape (n_windows, n_frequencies, n_axes)
126
- representing the power spectral density (PSD) of the sensor data.
138
+ A 2D array of shape (n_windows, n_frequencies) or 3D array of shape
139
+ (n_windows, n_frequencies, n_axes) representing the power spectral
140
+ density (PSD) of the sensor data.
127
141
  fmin : float
128
142
  The lower bound of the frequency band in Hz.
129
143
  fmax : float
@@ -133,9 +147,10 @@ def compute_power_in_bandwidth(
133
147
  spectral_resolution : float, optional
134
148
  The spectral resolution of the PSD in Hz (default: 1).
135
149
  cumulative_sum_method : str, optional
136
- The method used to integrate the PSD over the frequency band. Supported values are:
137
- - 'trapz': Trapezoidal rule.
138
- - 'sum': Simple summation (default: 'trapz').
150
+ The method used to integrate the PSD over the frequency band
151
+ (default: 'trapezoid'). Supported values are:
152
+ - 'trapezoid': Trapezoidal rule.
153
+ - 'sum': Simple summation.
139
154
 
140
155
  Returns
141
156
  -------
@@ -148,55 +163,60 @@ def compute_power_in_bandwidth(
148
163
  band_mask = (freqs >= fmin) & (freqs <= fmax)
149
164
  else:
150
165
  band_mask = (freqs >= fmin) & (freqs < fmax)
151
-
166
+
152
167
  # Integrate PSD over the selected frequency band using the band mask
153
168
  if psd.ndim == 2:
154
169
  masked_psd = psd[:, band_mask]
155
170
  elif psd.ndim == 3:
156
171
  masked_psd = psd[:, band_mask, :]
157
172
 
158
- if cumulative_sum_method == 'trapz':
159
- band_power = spectral_resolution * np.trapz(masked_psd, freqs[band_mask], axis=1)
160
- elif cumulative_sum_method == 'sum':
173
+ if cumulative_sum_method == "trapezoid":
174
+ band_power = spectral_resolution * np.trapezoid(
175
+ masked_psd, freqs[band_mask], axis=1
176
+ )
177
+ elif cumulative_sum_method == "sum":
161
178
  band_power = spectral_resolution * np.sum(masked_psd, axis=1)
162
179
  else:
163
- raise ValueError("cumulative_sum_method must be 'trapz' or 'sum'.")
180
+ raise ValueError("cumulative_sum_method must be 'trapezoid' or 'sum'.")
164
181
 
165
182
  return band_power
166
183
 
167
184
 
168
185
  def compute_total_power(psd: np.ndarray) -> np.ndarray:
169
186
  """
170
- Compute the total power by summing the power spectral density (PSD) across frequency bins.
187
+ Compute the total power by summing the power spectral density (PSD)
188
+ across frequency bins.
171
189
 
172
- This function calculates the total power for each window and each sensor axis by
190
+ This function calculates the total power for each window and each sensor axis by
173
191
  summing the PSD values across all frequency bins.
174
192
 
175
193
  Parameters
176
194
  ----------
177
195
  psd : np.ndarray
178
- A 3D array of shape (n_windows, n_frequencies, n_axes) representing the
196
+ A 3D array of shape (n_windows, n_frequencies, n_axes) representing the
179
197
  power spectral density (PSD) of the sensor data.
180
198
 
181
199
  Returns
182
200
  -------
183
201
  np.ndarray
184
- A 2D array of shape (n_windows, n_axes) containing the total power for each
202
+ A 2D array of shape (n_windows, n_axes) containing the total power for each
185
203
  window and each sensor axis.
186
204
  """
187
205
  return np.sum(psd, axis=-1) # Sum across frequency bins
188
206
 
189
207
 
190
208
  def extract_tremor_power(
191
- freqs: np.ndarray,
209
+ freqs: np.ndarray,
192
210
  total_psd: np.ndarray,
193
211
  fmin: float = 3,
194
212
  fmax: float = 7,
195
- spectral_resolution: float = 0.25
196
- ) -> np.ndarray:
213
+ spectral_resolution: float = 0.25,
214
+ ) -> np.ndarray:
215
+ """Computes the tremor power.
216
+
217
+ Tremor power is 1.25 Hz around the peak within the tremor
218
+ frequency band.
197
219
 
198
- """Computes the tremor power (1.25 Hz around the peak within the tremor frequency band)
199
-
200
220
  Parameters
201
221
  ----------
202
222
  total_psd: np.ndarray
@@ -209,16 +229,16 @@ def extract_tremor_power(
209
229
  The upper bound of the tremor frequency band in Hz (default: 7)
210
230
  spectral_resolution: float
211
231
  The spectral resolution of the PSD in Hz (default: 0.25)
212
-
232
+
213
233
  Returns
214
234
  -------
215
235
  pd.Series
216
236
  The tremor power across windows
217
237
  """
218
-
238
+
219
239
  freq_idx = (freqs >= fmin) & (freqs <= fmax)
220
240
  peak_idx = np.argmax(total_psd[:, freq_idx], axis=1) + np.min(np.where(freq_idx)[0])
221
- left_idx = np.maximum((peak_idx - 0.5 / spectral_resolution).astype(int), 0)
241
+ left_idx = np.maximum((peak_idx - 0.5 / spectral_resolution).astype(int), 0)
222
242
  right_idx = (peak_idx + 0.5 / spectral_resolution).astype(int)
223
243
 
224
244
  row_indices = np.arange(total_psd.shape[1])
@@ -234,24 +254,26 @@ def extract_tremor_power(
234
254
 
235
255
 
236
256
  def compute_dominant_frequency(
237
- freqs: np.ndarray,
238
- psd: np.ndarray,
239
- fmin: float | None = None,
240
- fmax: float | None = None
241
- ) -> np.ndarray:
257
+ freqs: np.ndarray,
258
+ psd: np.ndarray,
259
+ fmin: float | None = None,
260
+ fmax: float | None = None,
261
+ ) -> np.ndarray:
242
262
  """
243
- Compute the dominant frequency within a specified frequency range for each window and sensor axis.
263
+ Compute the dominant frequency within a specified frequency range for
264
+ each window and sensor axis.
244
265
 
245
- The dominant frequency is defined as the frequency corresponding to the maximum power in the
246
- power spectral density (PSD) within the specified range.
266
+ The dominant frequency is defined as the frequency corresponding to the
267
+ maximum power in the power spectral density (PSD) within the specified
268
+ range.
247
269
 
248
270
  Parameters
249
271
  ----------
250
272
  freqs : np.ndarray
251
- A 1D array of shape (n_frequencies,) containing the frequencies corresponding
273
+ A 1D array of shape (n_frequencies,) containing the frequencies corresponding
252
274
  to the PSD values.
253
275
  psd : np.ndarray
254
- A 2D array of shape (n_windows, n_frequencies) or a 3D array of shape
276
+ A 2D array of shape (n_windows, n_frequencies) or a 3D array of shape
255
277
  (n_windows, n_frequencies, n_axes) representing the power spectral density.
256
278
  fmin : float
257
279
  The lower bound of the frequency range (inclusive).
@@ -261,10 +283,10 @@ def compute_dominant_frequency(
261
283
  Returns
262
284
  -------
263
285
  np.ndarray
264
- - If `psd` is 2D: A 1D array of shape (n_windows,) containing the dominant frequency
265
- for each window.
266
- - If `psd` is 3D: A 2D array of shape (n_windows, n_axes) containing the dominant
267
- frequency for each window and each axis.
286
+ - If `psd` is 2D: A 1D array of shape (n_windows,) containing the
287
+ dominant frequency for each window.
288
+ - If `psd` is 3D: A 2D array of shape (n_windows, n_axes) containing
289
+ the dominant frequency for each window and each axis.
268
290
 
269
291
  Raises
270
292
  ------
@@ -272,7 +294,8 @@ def compute_dominant_frequency(
272
294
  If `fmin` or `fmax` is outside the bounds of the `freqs` array.
273
295
  If `psd` is not a 2D or 3D array.
274
296
  """
275
- # Set default values for fmin and fmax to the minimum and maximum frequencies if not provided
297
+ # Set default values for fmin and fmax to the minimum and maximum
298
+ # frequencies if not provided
276
299
  if fmin is None:
277
300
  fmin = freqs[0]
278
301
  if fmax is None:
@@ -280,40 +303,46 @@ def compute_dominant_frequency(
280
303
 
281
304
  # Validate the frequency range
282
305
  if fmin < freqs[0] or fmax > freqs[-1]:
283
- raise ValueError(f"fmin {fmin} or fmax {fmax} are out of bounds of the frequency array.")
284
-
306
+ raise ValueError(
307
+ f"fmin {fmin} or fmax {fmax} are out of bounds of the frequency array."
308
+ )
309
+
285
310
  # Find the indices corresponding to fmin and fmax
286
311
  min_index = np.searchsorted(freqs, fmin)
287
312
  max_index = np.searchsorted(freqs, fmax)
288
313
 
289
314
  # Slice the PSD and frequency array to the desired range
290
- psd_filtered = psd[:, min_index:max_index] if psd.ndim == 2 else psd[:, min_index:max_index, :]
315
+ psd_filtered = (
316
+ psd[:, min_index:max_index] if psd.ndim == 2 else psd[:, min_index:max_index, :]
317
+ )
291
318
  freqs_filtered = freqs[min_index:max_index]
292
319
 
293
320
  # Compute dominant frequency
294
321
  if psd.ndim == 3:
295
322
  # 3D: Compute for each axis
296
- return np.array([
297
- freqs_filtered[np.argmax(psd_filtered[:, :, i], axis=1)]
298
- for i in range(psd.shape[-1])
299
- ]).T
323
+ return np.array(
324
+ [
325
+ freqs_filtered[np.argmax(psd_filtered[:, :, i], axis=1)]
326
+ for i in range(psd.shape[-1])
327
+ ]
328
+ ).T
300
329
  elif psd.ndim == 2:
301
330
  # 2D: Compute for each window
302
331
  return freqs_filtered[np.argmax(psd_filtered, axis=1)]
303
332
  else:
304
333
  raise ValueError("PSD array must be 2D or 3D.")
305
-
334
+
306
335
 
307
336
  def extract_frequency_peak(
308
337
  freqs: np.ndarray,
309
338
  psd: np.ndarray,
310
339
  fmin: float | None = None,
311
340
  fmax: float | None = None,
312
- include_max: bool = True
313
- ) -> pd.Series:
341
+ include_max: bool = True,
342
+ ) -> pd.Series:
343
+ """Extract the frequency of the peak in the power spectral density
344
+ within the specified frequency band.
314
345
 
315
- """Extract the frequency of the peak in the power spectral density within the specified frequency band.
316
-
317
346
  Parameters
318
347
  ----------
319
348
  freqs: pd.Series
@@ -321,17 +350,19 @@ def extract_frequency_peak(
321
350
  psd: pd.Series
322
351
  The total power spectral density of the gyroscope signal
323
352
  fmin: float
324
- The lower bound of the frequency band in Hz (default: None). If not provided, the minimum frequency is used.
353
+ The lower bound of the frequency band in Hz (default: None).
354
+ If not provided, the minimum frequency is used.
325
355
  fmax: float
326
- The upper bound of the frequency band in Hz (default: None). If not provided, the maximum frequency is used.
356
+ The upper bound of the frequency band in Hz (default: None).
357
+ If not provided, the maximum frequency is used.
327
358
  include_max: bool
328
359
  Whether to include the maximum frequency in the search range (default: True)
329
-
360
+
330
361
  Returns
331
362
  -------
332
363
  pd.Series
333
364
  The frequency of the peak across windows
334
- """
365
+ """
335
366
  # Set fmin and fmax to maximum range if not provided
336
367
  if fmin is None:
337
368
  fmin = freqs[0]
@@ -340,9 +371,9 @@ def extract_frequency_peak(
340
371
 
341
372
  # Find the indices corresponding to fmin and fmax
342
373
  if include_max:
343
- freq_idx = np.where((freqs>=fmin) & (freqs<=fmax))[0]
374
+ freq_idx = np.where((freqs >= fmin) & (freqs <= fmax))[0]
344
375
  else:
345
- freq_idx = np.where((freqs>=fmin) & (freqs<fmax))[0]
376
+ freq_idx = np.where((freqs >= fmin) & (freqs < fmax))[0]
346
377
 
347
378
  peak_idx = np.argmax(psd[:, freq_idx], axis=1)
348
379
  frequency_peak = freqs[freq_idx][peak_idx]
@@ -351,12 +382,11 @@ def extract_frequency_peak(
351
382
 
352
383
 
353
384
  def compute_relative_power(
354
- freqs: np.ndarray,
355
- psd: np.ndarray,
356
- config: PulseRateConfig
357
- ) -> list:
385
+ freqs: np.ndarray, psd: np.ndarray, config: PulseRateConfig
386
+ ) -> list:
358
387
  """
359
- Calculate relative power within the dominant frequency band in the physiological range (0.75 - 3 Hz).
388
+ Calculate relative power within the dominant frequency band in the
389
+ physiological range (0.75 - 3 Hz).
360
390
 
361
391
  Parameters
362
392
  ----------
@@ -365,40 +395,50 @@ def compute_relative_power(
365
395
  psd: np.ndarray
366
396
  The power spectral density of the signal.
367
397
  config: PulseRateConfig
368
- The configuration object containing the parameters for the feature extraction. The following
369
- attributes are used:
398
+ The configuration object containing the parameters for the feature
399
+ extraction. The following attributes are used:
370
400
  - freq_band_physio: tuple
371
401
  The frequency band for physiological pulse rate (default: (0.75, 3)).
372
402
  - bandwidth: float
373
- The bandwidth around the peak frequency to consider for relative power calculation (default: 0.5).
403
+ The bandwidth around the peak frequency to consider for
404
+ relative power calculation (default: 0.5).
374
405
 
375
406
  Returns
376
407
  -------
377
408
  list
378
- The relative power within the dominant frequency band in the physiological range (0.75 - 3 Hz).
379
-
409
+ The relative power within the dominant frequency band in the
410
+ physiological range (0.75 - 3 Hz).
411
+
380
412
  """
381
- hr_range_mask = (freqs >= config.freq_band_physio[0]) & (freqs <= config.freq_band_physio[1])
413
+ hr_range_mask = (freqs >= config.freq_band_physio[0]) & (
414
+ freqs <= config.freq_band_physio[1]
415
+ )
382
416
  hr_range_idx = np.where(hr_range_mask)[0]
383
417
  peak_idx = np.argmax(psd[:, hr_range_idx], axis=1)
384
418
  peak_freqs = freqs[hr_range_idx[peak_idx]]
385
419
 
386
- dom_band_idx = [np.where((freqs >= peak_freq - config.bandwidth) & (freqs <= peak_freq + config.bandwidth))[0] for peak_freq in peak_freqs]
387
- rel_power = [np.trapz(psd[j, idx], freqs[idx]) / np.trapz(psd[j, :], freqs) for j, idx in enumerate(dom_band_idx)]
420
+ dom_band_idx = [
421
+ np.where(
422
+ (freqs >= peak_freq - config.bandwidth)
423
+ & (freqs <= peak_freq + config.bandwidth)
424
+ )[0]
425
+ for peak_freq in peak_freqs
426
+ ]
427
+ rel_power = [
428
+ np.trapezoid(psd[j, idx], freqs[idx]) / np.trapezoid(psd[j, :], freqs)
429
+ for j, idx in enumerate(dom_band_idx)
430
+ ]
388
431
  return rel_power
389
432
 
390
433
 
391
- def compute_spectral_entropy(
392
- psd: np.ndarray,
393
- n_samples: int
394
- ) -> np.ndarray:
434
+ def compute_spectral_entropy(psd: np.ndarray, n_samples: int) -> np.ndarray:
395
435
  """
396
436
  Calculate the spectral entropy from the normalized power spectral density.
397
437
 
398
438
  Parameters
399
439
  ----------
400
440
  psd: np.ndarray
401
- The power spectral density of the signal.
441
+ The power spectral density of the signal.
402
442
  n_samples: int
403
443
  The number of samples in the window.
404
444
 
@@ -408,37 +448,41 @@ def compute_spectral_entropy(
408
448
  The spectral entropy of the power spectral density.
409
449
  """
410
450
  psd_norm = psd / np.sum(psd, axis=1, keepdims=True)
411
- spectral_entropy = -np.sum(psd_norm * np.log2(psd_norm), axis=1) / np.log2(n_samples)
412
-
451
+ spectral_entropy = -np.sum(psd_norm * np.log2(psd_norm), axis=1) / np.log2(
452
+ n_samples
453
+ )
454
+
413
455
  return spectral_entropy
414
456
 
415
457
 
416
458
  def compute_mfccs(
417
- total_power_array: np.ndarray,
418
- config,
419
- total_power_type: str = 'psd',
420
- mel_scale: bool = True,
421
- multiplication_factor: float = 1,
422
- rounding_method: str = 'floor'
423
- ) -> np.ndarray:
459
+ total_power_array: np.ndarray,
460
+ config,
461
+ total_power_type: str = "psd",
462
+ mel_scale: bool = True,
463
+ multiplication_factor: float = 1,
464
+ rounding_method: str = "floor",
465
+ ) -> np.ndarray:
424
466
  """
425
- Generate Mel Frequency Cepstral Coefficients (MFCCs) from the total power spectral density or spectrogram of the signal.
467
+ Generate Mel Frequency Cepstral Coefficients (MFCCs) from the total
468
+ power spectral density or spectrogram of the signal.
426
469
 
427
- MFCCs are commonly used features in signal processing for tasks like audio and
470
+ MFCCs are commonly used features in signal processing for tasks like audio and
428
471
  vibration analysis. In this version, we adjusted the MFFCs to the human activity
429
- range according to: https://www.sciencedirect.com/science/article/abs/pii/S016516841500331X#f0050.
430
- This function calculates MFCCs by applying a filterbank
431
- (in either the mel scale or linear scale) to the total power of the signal,
472
+ range according to:
473
+ https://www.sciencedirect.com/science/article/abs/pii/S016516841500331X#f0050
474
+ This function calculates MFCCs by applying a filterbank
475
+ (in either the mel scale or linear scale) to the total power of the signal,
432
476
  followed by a Discrete Cosine Transform (DCT) to obtain coefficients.
433
477
 
434
478
  Parameters
435
479
  ----------
436
480
  total_power_array : np.ndarray
437
- 2D array of shape (n_windows, n_frequencies) containing the total power
438
- of the signal for each window.
481
+ 2D array of shape (n_windows, n_frequencies) containing the total
482
+ power of the signal for each window.
439
483
  OR
440
- 3D array of shape (n_windows, n_frequencies, n_segments) containing the total spectrogram
441
- of the signal for each window.
484
+ 3D array of shape (n_windows, n_frequencies, n_segments) containing
485
+ the total spectrogram of the signal for each window.
442
486
  config : object
443
487
  Configuration object containing the following attributes:
444
488
  - window_length_s : int
@@ -454,14 +498,16 @@ def compute_mfccs(
454
498
  - mfcc_n_coefficients : int
455
499
  Number of coefficients to extract (default: 12).
456
500
  total_power_type : str, optional
457
- The type of the total power array. Supported values are 'psd' and 'spectrogram' (default: 'psd').
501
+ The type of the total power array. Supported values are 'psd' and
502
+ 'spectrogram' (default: 'psd').
458
503
  mel_scale : bool, optional
459
504
  Whether to use the mel scale for the filterbank (default: True).
460
505
  multiplication_factor : float, optional
461
- Multiplication factor for the Mel scale conversion (default: 1). For tremor, the recommended
462
- value is 1. For gait, this is 4.
506
+ Multiplication factor for the Mel scale conversion (default: 1).
507
+ For tremor, the recommended value is 1. For gait, this is 4.
463
508
  rounding_method : str, optional
464
- The method used to round the filter points. Supported values are 'round' and 'floor' (default: 'floor').
509
+ The method used to round the filter points. Supported values are
510
+ 'round' and 'floor' (default: 'floor').
465
511
 
466
512
  Returns
467
513
  -------
@@ -475,61 +521,66 @@ def compute_mfccs(
475
521
  - The function includes filterbank normalization to ensure proper scaling.
476
522
  - DCT filters are constructed to minimize spectral leakage.
477
523
  """
478
-
524
+
479
525
  # Check if total_power_type is either 'psd' or 'spectrogram'
480
- if total_power_type not in ['psd', 'spectrogram']:
481
- raise ValueError("total_power_type should be set to either 'psd' or 'spectrogram'")
526
+ if total_power_type not in ["psd", "spectrogram"]:
527
+ raise ValueError(
528
+ "total_power_type should be set to either 'psd' or 'spectrogram'"
529
+ )
482
530
 
483
531
  # Compute window length in samples
484
532
  window_length = config.window_length_s * config.sampling_frequency
485
-
533
+
486
534
  # Determine the length of subwindows used in the spectrogram computation
487
- if total_power_type == 'spectrogram':
535
+ if total_power_type == "spectrogram":
488
536
  nr_subwindows = total_power_array.shape[2]
489
- window_length = int(window_length/(nr_subwindows - (nr_subwindows - 1) * config.overlap_fraction))
537
+ window_length = int(
538
+ window_length
539
+ / (nr_subwindows - (nr_subwindows - 1) * config.overlap_fraction)
540
+ )
490
541
 
491
542
  # Generate filter points
492
543
  if mel_scale:
493
544
  freqs = np.linspace(
494
- melscale(config.mfcc_low_frequency, multiplication_factor),
495
- melscale(config.mfcc_high_frequency, multiplication_factor),
496
- num=config.mfcc_n_dct_filters + 2
545
+ melscale(config.mfcc_low_frequency, multiplication_factor),
546
+ melscale(config.mfcc_high_frequency, multiplication_factor),
547
+ num=config.mfcc_n_dct_filters + 2,
497
548
  )
498
549
  freqs = inverse_melscale(freqs, multiplication_factor)
499
550
  else:
500
551
  freqs = np.linspace(
501
- config.mfcc_low_frequency,
502
- config.mfcc_high_frequency,
503
- num=config.mfcc_n_dct_filters + 2
552
+ config.mfcc_low_frequency,
553
+ config.mfcc_high_frequency,
554
+ num=config.mfcc_n_dct_filters + 2,
555
+ )
556
+
557
+ if rounding_method == "round":
558
+ filter_points = (
559
+ np.round(window_length / config.sampling_frequency * freqs).astype(int) + 1
504
560
  )
505
-
506
- if rounding_method == 'round':
507
- filter_points = np.round(
508
- window_length / config.sampling_frequency * freqs
509
- ).astype(int) + 1
510
561
 
511
- elif rounding_method == 'floor':
512
- filter_points = np.floor(
513
- window_length / config.sampling_frequency * freqs
514
- ).astype(int) + 1
562
+ elif rounding_method == "floor":
563
+ filter_points = (
564
+ np.floor(window_length / config.sampling_frequency * freqs).astype(int) + 1
565
+ )
515
566
 
516
567
  # Construct triangular filterbank
517
568
  filters = np.zeros((len(filter_points) - 2, int(window_length / 2 + 1)))
518
569
  for j in range(len(filter_points) - 2):
519
570
  filters[j, filter_points[j] : filter_points[j + 2]] = windows.triang(
520
571
  filter_points[j + 2] - filter_points[j]
521
- )
572
+ )
522
573
  # Normalize filter coefficients
523
574
  filters[j, :] /= (
524
- config.sampling_frequency/window_length * np.sum(filters[j,:])
525
- )
575
+ config.sampling_frequency / window_length * np.sum(filters[j, :])
576
+ )
526
577
 
527
578
  # Apply filterbank to total power
528
- if total_power_type == 'spectrogram':
529
- power_filtered = np.tensordot(total_power_array, filters.T, axes=(1,0))
530
- elif total_power_type == 'psd':
579
+ if total_power_type == "spectrogram":
580
+ power_filtered = np.tensordot(total_power_array, filters.T, axes=(1, 0))
581
+ elif total_power_type == "psd":
531
582
  power_filtered = np.dot(total_power_array, filters.T)
532
-
583
+
533
584
  # Convert power to logarithmic scale
534
585
  log_power_filtered = np.log10(power_filtered + 1e-10)
535
586
 
@@ -538,16 +589,20 @@ def compute_mfccs(
538
589
  dct_filters[0, :] = 1.0 / np.sqrt(config.mfcc_n_dct_filters)
539
590
 
540
591
  samples = (
541
- np.arange(1, 2 * config.mfcc_n_dct_filters, 2) * np.pi / (2.0 * config.mfcc_n_dct_filters)
592
+ np.arange(1, 2 * config.mfcc_n_dct_filters, 2)
593
+ * np.pi
594
+ / (2.0 * config.mfcc_n_dct_filters)
542
595
  )
543
596
 
544
597
  for i in range(1, config.mfcc_n_coefficients):
545
- dct_filters[i, :] = np.cos(i * samples) * np.sqrt(2.0 / config.mfcc_n_dct_filters)
598
+ dct_filters[i, :] = np.cos(i * samples) * np.sqrt(
599
+ 2.0 / config.mfcc_n_dct_filters
600
+ )
546
601
 
547
602
  # Compute MFCCs
548
- mfccs = np.dot(log_power_filtered, dct_filters.T)
603
+ mfccs = np.dot(log_power_filtered, dct_filters.T)
549
604
 
550
- if total_power_type == 'spectrogram':
605
+ if total_power_type == "spectrogram":
551
606
  mfccs = np.mean(mfccs, axis=1)
552
607
 
553
608
  return mfccs
@@ -562,15 +617,17 @@ def melscale(x: np.ndarray, multiplication_factor: float = 1) -> np.ndarray:
562
617
  x : np.ndarray
563
618
  Linear frequency values to be converted to the Mel scale.
564
619
  multiplication_factor : float, optional
565
- Multiplication factor for the Mel scale conversion (default: 1). For tremor, the recommended
566
- value is 1. For gait, this is 4.
620
+ Multiplication factor for the Mel scale conversion (default: 1).
621
+ For tremor, the recommended value is 1. For gait, this is 4.
567
622
 
568
623
  Returns
569
624
  -------
570
625
  np.ndarray
571
626
  Frequency values mapped to the Mel scale.
572
627
  """
573
- return (64.875 / multiplication_factor) * np.log10(1 + x / (17.5 / multiplication_factor))
628
+ return (64.875 / multiplication_factor) * np.log10(
629
+ 1 + x / (17.5 / multiplication_factor)
630
+ )
574
631
 
575
632
 
576
633
  def inverse_melscale(x: np.ndarray, multiplication_factor: float = 1) -> np.ndarray:
@@ -578,7 +635,8 @@ def inverse_melscale(x: np.ndarray, multiplication_factor: float = 1) -> np.ndar
578
635
  Maps values from the Mel scale back to linear frequencies.
579
636
 
580
637
  This function performs the inverse transformation of the Mel scale,
581
- converting perceptual frequency values to their corresponding linear frequency values.
638
+ converting perceptual frequency values to their corresponding linear
639
+ frequency values.
582
640
 
583
641
  Parameters
584
642
  ----------
@@ -590,17 +648,19 @@ def inverse_melscale(x: np.ndarray, multiplication_factor: float = 1) -> np.ndar
590
648
  np.ndarray
591
649
  Linear frequency values corresponding to the given Mel scale values.
592
650
  """
593
- return (17.5 / multiplication_factor) * (10 ** (x / (64.875 / multiplication_factor)) - 1)
651
+ return (17.5 / multiplication_factor) * (
652
+ 10 ** (x / (64.875 / multiplication_factor)) - 1
653
+ )
594
654
 
595
655
 
596
656
  def pca_transform_gyroscope(
597
- df: pd.DataFrame,
598
- y_gyro_colname: str,
599
- z_gyro_colname: str,
657
+ df: pd.DataFrame,
658
+ y_gyro_colname: str,
659
+ z_gyro_colname: str,
600
660
  ) -> np.ndarray:
601
661
  """
602
662
  Perform principal component analysis (PCA) on gyroscope data to estimate velocity.
603
-
663
+
604
664
  Parameters
605
665
  ----------
606
666
  df : pd.DataFrame
@@ -609,7 +669,7 @@ def pca_transform_gyroscope(
609
669
  The column name for the y-axis gyroscope data.
610
670
  z_gyro_colname : str
611
671
  The column name for the z-axis gyroscope data.
612
-
672
+
613
673
  Returns
614
674
  -------
615
675
  np.ndarray
@@ -623,7 +683,7 @@ def pca_transform_gyroscope(
623
683
  fit_data = np.column_stack((y_gyro_array, z_gyro_array))
624
684
  full_data = fit_data
625
685
 
626
- pca = PCA(n_components=2, svd_solver='auto', random_state=22)
686
+ pca = PCA(n_components=2, svd_solver="auto", random_state=22)
627
687
  pca.fit(fit_data)
628
688
  velocity = pca.transform(full_data)[:, 0] # First principal component
629
689
 
@@ -632,26 +692,24 @@ def pca_transform_gyroscope(
632
692
 
633
693
  def compute_angle(time_array: np.ndarray, velocity_array: np.ndarray) -> np.ndarray:
634
694
  """
635
- Compute the angle from the angular velocity using cumulative trapezoidal integration.
636
-
695
+ Compute the angle from the angular velocity using cumulative
696
+ trapezoidal integration.
697
+
637
698
  Parameters
638
699
  ----------
639
700
  time_array : np.ndarray
640
701
  The time array corresponding to the angular velocity data.
641
702
  velocity_array : np.ndarray
642
703
  The angular velocity data to integrate.
643
-
704
+
644
705
  Returns
645
706
  -------
646
707
  np.ndarray
647
- The estimated angle based on the cumulative trapezoidal integration of the angular velocity.
708
+ The estimated angle based on the cumulative trapezoidal integration
709
+ of the angular velocity.
648
710
  """
649
711
  # Perform integration and apply absolute value
650
- angle_array = cumulative_trapezoid(
651
- y=velocity_array,
652
- x=time_array,
653
- initial=0
654
- )
712
+ angle_array = cumulative_trapezoid(y=velocity_array, x=time_array, initial=0)
655
713
  return np.abs(angle_array)
656
714
 
657
715
 
@@ -665,31 +723,30 @@ def remove_moving_average_angle(angle_array: np.ndarray, fs: float) -> pd.Series
665
723
  The angle array to remove the moving average from.
666
724
  fs : float
667
725
  The sampling frequency of the data.
668
-
726
+
669
727
  Returns
670
728
  -------
671
729
  pd.Series
672
730
  The angle array with the moving average removed.
673
731
  """
674
732
  window_size = int(2 * (fs * 0.5) + 1)
675
- angle_ma = np.array(pd.Series(angle_array).rolling(
676
- window=window_size,
677
- min_periods=1,
678
- center=True,
679
- closed='both'
680
- ).mean())
681
-
733
+ angle_ma = np.array(
734
+ pd.Series(angle_array)
735
+ .rolling(window=window_size, min_periods=1, center=True, closed="both")
736
+ .mean()
737
+ )
738
+
682
739
  return angle_array - angle_ma
683
740
 
684
741
 
685
742
  def extract_angle_extremes(
686
- angle_array: np.ndarray,
687
- sampling_frequency: float,
688
- max_frequency_activity: float = 1.75,
689
- ) -> tuple[List[int], List[int], List[int]]:
743
+ angle_array: np.ndarray,
744
+ sampling_frequency: float,
745
+ max_frequency_activity: float = 1.75,
746
+ ) -> tuple[list[int], list[int], list[int]]:
690
747
  """
691
748
  Extract extrema (minima and maxima) indices from the angle array.
692
-
749
+
693
750
  Parameters
694
751
  ----------
695
752
  angle_array : np.ndarray
@@ -698,25 +755,21 @@ def extract_angle_extremes(
698
755
  The sampling frequency of the data.
699
756
  max_frequency_activity : float, optional
700
757
  The maximum frequency of human activity in Hz (default: 1.75).
701
-
758
+
702
759
  Returns
703
760
  -------
704
761
  tuple
705
762
  A tuple containing the indices of the angle extrema, minima, and maxima.
706
763
  """
707
764
  distance = sampling_frequency / max_frequency_activity
708
- prominence = 2
765
+ prominence = 2
709
766
 
710
767
  # Find minima and maxima indices for each window
711
768
  minima_indices = find_peaks(
712
- x=-angle_array,
713
- distance=distance,
714
- prominence=prominence
769
+ x=-angle_array, distance=distance, prominence=prominence
715
770
  )[0]
716
771
  maxima_indices = find_peaks(
717
- x=angle_array,
718
- distance=distance,
719
- prominence=prominence
772
+ x=angle_array, distance=distance, prominence=prominence
720
773
  )[0]
721
774
 
722
775
  minima_indices = np.array(minima_indices, dtype=object)
@@ -728,14 +781,20 @@ def extract_angle_extremes(
728
781
  # Start with a minimum
729
782
  while i_pks < minima_indices.size - 1 and i_pks < maxima_indices.size:
730
783
  if minima_indices[i_pks + 1] < maxima_indices[i_pks]:
731
- if angle_array[minima_indices[i_pks + 1]] < angle_array[minima_indices[i_pks]]:
784
+ if (
785
+ angle_array[minima_indices[i_pks + 1]]
786
+ < angle_array[minima_indices[i_pks]]
787
+ ):
732
788
  minima_indices = np.delete(minima_indices, i_pks)
733
789
  else:
734
790
  minima_indices = np.delete(minima_indices, i_pks + 1)
735
791
  i_pks -= 1
736
792
 
737
793
  if i_pks >= 0 and minima_indices[i_pks] > maxima_indices[i_pks]:
738
- if angle_array[maxima_indices[i_pks]] < angle_array[maxima_indices[i_pks - 1]]:
794
+ if (
795
+ angle_array[maxima_indices[i_pks]]
796
+ < angle_array[maxima_indices[i_pks - 1]]
797
+ ):
739
798
  maxima_indices = np.delete(maxima_indices, i_pks)
740
799
  else:
741
800
  maxima_indices = np.delete(maxima_indices, i_pks - 1)
@@ -746,14 +805,20 @@ def extract_angle_extremes(
746
805
  # Start with a maximum
747
806
  while i_pks < maxima_indices.size - 1 and i_pks < minima_indices.size:
748
807
  if maxima_indices[i_pks + 1] < minima_indices[i_pks]:
749
- if angle_array[maxima_indices[i_pks + 1]] < angle_array[maxima_indices[i_pks]]:
808
+ if (
809
+ angle_array[maxima_indices[i_pks + 1]]
810
+ < angle_array[maxima_indices[i_pks]]
811
+ ):
750
812
  maxima_indices = np.delete(maxima_indices, i_pks + 1)
751
813
  else:
752
814
  maxima_indices = np.delete(maxima_indices, i_pks)
753
815
  i_pks -= 1
754
816
 
755
817
  if i_pks >= 0 and maxima_indices[i_pks] > minima_indices[i_pks]:
756
- if angle_array[minima_indices[i_pks]] < angle_array[minima_indices[i_pks - 1]]:
818
+ if (
819
+ angle_array[minima_indices[i_pks]]
820
+ < angle_array[minima_indices[i_pks - 1]]
821
+ ):
757
822
  minima_indices = np.delete(minima_indices, i_pks - 1)
758
823
  else:
759
824
  minima_indices = np.delete(minima_indices, i_pks)
@@ -766,17 +831,19 @@ def extract_angle_extremes(
766
831
  return list(angle_extrema_indices), list(minima_indices), list(maxima_indices)
767
832
 
768
833
 
769
- def compute_range_of_motion(angle_array: np.ndarray, extrema_indices: List[int]) -> np.ndarray:
834
+ def compute_range_of_motion(
835
+ angle_array: np.ndarray, extrema_indices: list[int]
836
+ ) -> np.ndarray:
770
837
  """
771
838
  Compute the range of motion of a time series based on the angle extrema.
772
-
839
+
773
840
  Parameters
774
841
  ----------
775
842
  angle_array : np.ndarray
776
843
  The angle array to compute the range of motion from.
777
844
  extrema_indices : List[int]
778
845
  The indices of the angle extrema.
779
-
846
+
780
847
  Returns
781
848
  -------
782
849
  np.ndarray
@@ -787,9 +854,11 @@ def compute_range_of_motion(angle_array: np.ndarray, extrema_indices: List[int])
787
854
  raise TypeError("extrema_indices must be a list of integers.")
788
855
 
789
856
  # Check bounds
790
- if np.any(np.array(extrema_indices) < 0) or np.any(np.array(extrema_indices) >= len(angle_array)):
857
+ if np.any(np.array(extrema_indices) < 0) or np.any(
858
+ np.array(extrema_indices) >= len(angle_array)
859
+ ):
791
860
  raise ValueError("extrema_indices contains out-of-bounds indices.")
792
-
861
+
793
862
  # Extract angle amplitudes (minima and maxima values)
794
863
  angle_extremas = angle_array[extrema_indices]
795
864
 
@@ -801,7 +870,7 @@ def compute_range_of_motion(angle_array: np.ndarray, extrema_indices: List[int])
801
870
 
802
871
  def compute_peak_angular_velocity(
803
872
  velocity_array: np.ndarray,
804
- angle_extrema_indices: List[int],
873
+ angle_extrema_indices: list[int],
805
874
  ) -> np.ndarray:
806
875
  """
807
876
  Compute the peak angular velocity of a time series based on the angle extrema.
@@ -812,19 +881,21 @@ def compute_peak_angular_velocity(
812
881
  The angular velocity array to compute the peak angular velocity from.
813
882
  angle_extrema_indices : List[int]
814
883
  The indices of the angle extrema.
815
-
884
+
816
885
  Returns
817
886
  -------
818
887
  np.ndarray
819
888
  The peak angular velocities of the time series.
820
889
  """
821
- if np.any(np.array(angle_extrema_indices) < 0) or np.any(np.array(angle_extrema_indices) >= len(velocity_array)):
890
+ if np.any(np.array(angle_extrema_indices) < 0) or np.any(
891
+ np.array(angle_extrema_indices) >= len(velocity_array)
892
+ ):
822
893
  raise ValueError("angle_extrema_indices contains out-of-bounds indices.")
823
-
894
+
824
895
  if len(angle_extrema_indices) < 2:
825
896
  raise ValueError("angle_extrema_indices must contain at least two indices.")
826
-
827
- # Initialize a list to store the peak velocities
897
+
898
+ # Initialize a list to store the peak velocities
828
899
  pav = []
829
900
 
830
901
  # Compute peak angular velocities
@@ -841,10 +912,10 @@ def compute_peak_angular_velocity(
841
912
 
842
913
  def compute_forward_backward_peak_angular_velocity(
843
914
  velocity_array: np.ndarray,
844
- angle_extrema_indices: List[int],
845
- minima_indices: List[int],
846
- maxima_indices: List[int],
847
- ) -> Tuple[np.ndarray, np.ndarray]:
915
+ angle_extrema_indices: list[int],
916
+ minima_indices: list[int],
917
+ maxima_indices: list[int],
918
+ ) -> tuple[np.ndarray, np.ndarray]:
848
919
  """
849
920
  Compute the peak angular velocity of a time series based on the angle extrema.
850
921
 
@@ -858,21 +929,24 @@ def compute_forward_backward_peak_angular_velocity(
858
929
  The indices of the minima.
859
930
  maxima_indices : List[int]
860
931
  The indices of the maxima.
861
-
932
+
862
933
  Returns
863
934
  -------
864
935
  Tuple[np.ndarray, np.ndarray]
865
- A tuple containing the forward and backward peak angular velocities for minima and maxima.
936
+ A tuple containing the forward and backward peak angular velocities
937
+ for minima and maxima.
866
938
  """
867
- if np.any(np.array(angle_extrema_indices) < 0) or np.any(np.array(angle_extrema_indices) >= len(velocity_array)):
939
+ if np.any(np.array(angle_extrema_indices) < 0) or np.any(
940
+ np.array(angle_extrema_indices) >= len(velocity_array)
941
+ ):
868
942
  raise ValueError("angle_extrema_indices contains out-of-bounds indices.")
869
-
943
+
870
944
  if len(angle_extrema_indices) < 2:
871
945
  raise ValueError("angle_extrema_indices must contain at least two indices.")
872
-
946
+
873
947
  if len(minima_indices) == 0:
874
948
  raise ValueError("No minima indices found.")
875
-
949
+
876
950
  if len(maxima_indices) == 0:
877
951
  raise ValueError("No maxima indices found.")
878
952
 
@@ -887,7 +961,8 @@ def compute_forward_backward_peak_angular_velocity(
887
961
  next_peak_idx = angle_extrema_indices[i + 1]
888
962
  segment = velocity_array[current_peak_idx:next_peak_idx]
889
963
 
890
- # Check if the current peak is a minimum or maximum and calculate peak velocity accordingly
964
+ # Check if the current peak is a minimum or maximum and
965
+ # calculate peak velocity accordingly
891
966
  if current_peak_idx in minima_indices:
892
967
  forward_pav.append(np.max(np.abs(segment)))
893
968
  elif current_peak_idx in maxima_indices:
@@ -900,12 +975,10 @@ def compute_forward_backward_peak_angular_velocity(
900
975
  return forward_pav, backward_pav
901
976
 
902
977
 
903
- def compute_signal_to_noise_ratio(
904
- ppg_windowed: np.ndarray
905
- ) -> np.ndarray:
978
+ def compute_signal_to_noise_ratio(ppg_windowed: np.ndarray) -> np.ndarray:
906
979
  """
907
980
  Compute the signal to noise ratio of the PPG signal.
908
-
981
+
909
982
  Parameters
910
983
  ----------
911
984
  ppg_windowed: np.ndarray
@@ -916,21 +989,25 @@ def compute_signal_to_noise_ratio(
916
989
  np.ndarray
917
990
  The signal to noise ratio of the PPG signal.
918
991
  """
919
-
992
+
920
993
  arr_signal = np.var(ppg_windowed, axis=1)
921
994
  arr_noise = np.var(np.abs(ppg_windowed), axis=1)
922
995
  signal_to_noise_ratio = arr_signal / arr_noise
923
-
996
+
924
997
  return signal_to_noise_ratio
925
998
 
926
- def compute_auto_correlation(
927
- ppg_windowed: np.ndarray,
928
- fs: int
929
- ) -> np.ndarray:
999
+
1000
+ def compute_auto_correlation(ppg_windowed: np.ndarray, fs: int) -> np.ndarray:
930
1001
  """
931
- Compute the biased autocorrelation of the PPG signal. The autocorrelation is computed up to 3 seconds. The highest peak value is selected as the autocorrelation value. If no peaks are found, the value is set to 0.
932
- The biased autocorrelation is computed using the biased_autocorrelation function. It differs from the unbiased autocorrelation in that the normalization factor is the length of the original signal, and boundary effects are considered. This results in a smoother autocorrelation function.
933
-
1002
+ Compute the biased autocorrelation of the PPG signal. The autocorrelation
1003
+ is computed up to 3 seconds. The highest peak value is selected as the
1004
+ autocorrelation value. If no peaks are found, the value is set to 0.
1005
+ The biased autocorrelation is computed using the biased_autocorrelation
1006
+ function. It differs from the unbiased autocorrelation in that the
1007
+ normalization factor is the length of the original signal, and boundary
1008
+ effects are considered. This results in a smoother autocorrelation
1009
+ function.
1010
+
934
1011
  Parameters
935
1012
  ----------
936
1013
  ppg_windowed: np.ndarray
@@ -944,21 +1021,26 @@ def compute_auto_correlation(
944
1021
  The autocorrelation of the PPG signal.
945
1022
  """
946
1023
 
947
- auto_correlations = biased_autocorrelation(ppg_windowed, fs*3) # compute the biased autocorrelation of the PPG signal up to 3 seconds
948
- peaks = [find_peaks(x, height=0.01)[0] for x in auto_correlations] # find the peaks of the autocorrelation
949
- sorted_peak_values = [np.sort(auto_correlations[i, indices])[::-1] for i, indices in enumerate(peaks)] # sort the peak values in descending order
950
- auto_correlations = [x[0] if len(x) > 0 else 0 for x in sorted_peak_values] # get the highest peak value if there are any peaks, otherwise set to 0
1024
+ # Compute the biased autocorrelation of the PPG signal up to 3 seconds
1025
+ auto_correlations = biased_autocorrelation(ppg_windowed, fs * 3)
1026
+ # Find the peaks of the autocorrelation
1027
+ peaks = [find_peaks(x, height=0.01)[0] for x in auto_correlations]
1028
+ # Sort the peak values in descending order
1029
+ sorted_peak_values = [
1030
+ np.sort(auto_correlations[i, indices])[::-1] for i, indices in enumerate(peaks)
1031
+ ]
1032
+ # Get the highest peak value if there are any peaks, otherwise set to 0
1033
+ auto_correlations = [x[0] if len(x) > 0 else 0 for x in sorted_peak_values]
951
1034
 
952
1035
  return np.asarray(auto_correlations)
953
1036
 
954
- def biased_autocorrelation(
955
- ppg_windowed: np.ndarray,
956
- max_lag: int
957
- ) -> np.ndarray:
1037
+
1038
+ def biased_autocorrelation(ppg_windowed: np.ndarray, max_lag: int) -> np.ndarray:
958
1039
  """
959
- Compute the biased autocorrelation of a signal (similar to matlabs autocorr function), where the normalization factor
960
- is the length of the original signal, and boundary effects are considered.
961
-
1040
+ Compute the biased autocorrelation of a signal (similar to matlab's
1041
+ autocorr function), where the normalization factor is the length of the
1042
+ original signal, and boundary effects are considered.
1043
+
962
1044
  Parameters
963
1045
  ----------
964
1046
  ppg_windowed: np.ndarray
@@ -972,13 +1054,21 @@ def biased_autocorrelation(
972
1054
  The biased autocorrelation of the PPG signal.
973
1055
 
974
1056
  """
975
- zero_mean_ppg = ppg_windowed - np.mean(ppg_windowed, axis=1, keepdims=True) # Remove the mean of the signal to make it zero-mean
976
- N = zero_mean_ppg.shape[1]
1057
+ zero_mean_ppg = ppg_windowed - np.mean(
1058
+ ppg_windowed, axis=1, keepdims=True
1059
+ ) # Remove the mean of the signal to make it zero-mean
1060
+ n_samples = zero_mean_ppg.shape[1]
977
1061
  autocorr_values = np.zeros((zero_mean_ppg.shape[0], max_lag + 1))
978
-
1062
+
979
1063
  for lag in range(max_lag + 1):
980
1064
  # Compute autocorrelation for current lag
981
- overlapping_points = zero_mean_ppg[:, :N-lag] * zero_mean_ppg[:, lag:]
982
- autocorr_values[:, lag] = np.sum(overlapping_points, axis=1) / N # Divide by N (biased normalization)
983
-
984
- return autocorr_values/autocorr_values[:, 0, np.newaxis] # Normalize the autocorrelation values
1065
+ overlapping_points = (
1066
+ zero_mean_ppg[:, : n_samples - lag] * zero_mean_ppg[:, lag:]
1067
+ )
1068
+ autocorr_values[:, lag] = (
1069
+ np.sum(overlapping_points, axis=1) / n_samples
1070
+ ) # Divide by n_samples (biased normalization)
1071
+
1072
+ return (
1073
+ autocorr_values / autocorr_values[:, 0, np.newaxis]
1074
+ ) # Normalize the autocorrelation values