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.
- paradigma/classification.py +28 -11
- paradigma/config.py +158 -101
- paradigma/constants.py +39 -34
- paradigma/feature_extraction.py +270 -211
- paradigma/pipelines/gait_pipeline.py +286 -190
- paradigma/pipelines/pulse_rate_pipeline.py +202 -133
- paradigma/pipelines/pulse_rate_utils.py +144 -142
- paradigma/pipelines/tremor_pipeline.py +139 -95
- paradigma/preprocessing.py +179 -110
- paradigma/segmenting.py +138 -113
- paradigma/testing.py +359 -172
- paradigma/util.py +171 -80
- {paradigma-1.0.2.dist-info → paradigma-1.0.4.dist-info}/METADATA +39 -36
- paradigma-1.0.4.dist-info/RECORD +23 -0
- {paradigma-1.0.2.dist-info → paradigma-1.0.4.dist-info}/WHEEL +1 -1
- paradigma-1.0.4.dist-info/entry_points.txt +4 -0
- {paradigma-1.0.2.dist-info → paradigma-1.0.4.dist-info/licenses}/LICENSE +0 -1
- paradigma-1.0.2.dist-info/RECORD +0 -22
paradigma/classification.py
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import numpy as np
|
|
2
1
|
import pickle
|
|
3
|
-
|
|
4
2
|
from pathlib import Path
|
|
5
|
-
from sklearn.base import BaseEstimator
|
|
6
3
|
from typing import Any, Optional
|
|
7
4
|
|
|
5
|
+
import numpy as np
|
|
6
|
+
from sklearn.base import BaseEstimator
|
|
7
|
+
from sklearn.preprocessing import StandardScaler
|
|
8
|
+
|
|
9
|
+
|
|
8
10
|
class ClassifierPackage:
|
|
9
|
-
def __init__(
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
classifier: Optional[BaseEstimator] = None,
|
|
14
|
+
threshold: Optional[float] = None,
|
|
15
|
+
scaler: Optional[Any] = None,
|
|
16
|
+
):
|
|
12
17
|
"""
|
|
13
18
|
Initialize the ClassifierPackage with a classifier, threshold, and scaler.
|
|
14
19
|
|
|
@@ -43,6 +48,18 @@ class ClassifierPackage:
|
|
|
43
48
|
return X
|
|
44
49
|
return self.scaler.transform(X)
|
|
45
50
|
|
|
51
|
+
def update_scaler(self, x_train: np.ndarray) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Update the scaler used for feature transformation.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
x_train : np.ndarray
|
|
58
|
+
The training data to fit the scaler.
|
|
59
|
+
"""
|
|
60
|
+
scaler = StandardScaler()
|
|
61
|
+
self.scaler = scaler.fit(x_train)
|
|
62
|
+
|
|
46
63
|
def predict_proba(self, X) -> float:
|
|
47
64
|
"""
|
|
48
65
|
Make predictions using the classifier and apply the threshold.
|
|
@@ -61,7 +78,7 @@ class ClassifierPackage:
|
|
|
61
78
|
if not self.classifier:
|
|
62
79
|
raise ValueError("Classifier is not loaded.")
|
|
63
80
|
return self.classifier.predict_proba(X)[:, 1]
|
|
64
|
-
|
|
81
|
+
|
|
65
82
|
def predict(self, X) -> int:
|
|
66
83
|
"""
|
|
67
84
|
Make predictions using the classifier and apply the threshold.
|
|
@@ -80,7 +97,7 @@ class ClassifierPackage:
|
|
|
80
97
|
if not self.classifier:
|
|
81
98
|
raise ValueError("Classifier is not loaded.")
|
|
82
99
|
return int(self.predict_proba(X) >= self.threshold)
|
|
83
|
-
|
|
100
|
+
|
|
84
101
|
def save(self, filepath: str | Path) -> None:
|
|
85
102
|
"""
|
|
86
103
|
Save the ClassifierPackage to a file.
|
|
@@ -90,7 +107,7 @@ class ClassifierPackage:
|
|
|
90
107
|
filepath : str
|
|
91
108
|
The path to the file.
|
|
92
109
|
"""
|
|
93
|
-
with open(filepath,
|
|
110
|
+
with open(filepath, "wb") as f:
|
|
94
111
|
pickle.dump(self, f)
|
|
95
112
|
|
|
96
113
|
@classmethod
|
|
@@ -109,7 +126,7 @@ class ClassifierPackage:
|
|
|
109
126
|
The loaded classifier package.
|
|
110
127
|
"""
|
|
111
128
|
try:
|
|
112
|
-
with open(filepath,
|
|
129
|
+
with open(filepath, "rb") as f:
|
|
113
130
|
return pickle.load(f)
|
|
114
131
|
except Exception as e:
|
|
115
|
-
raise ValueError(f"Failed to load classifier package: {e}") from e
|
|
132
|
+
raise ValueError(f"Failed to load classifier package: {e}") from e
|
paradigma/config.py
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from dataclasses import asdict
|
|
1
3
|
from typing import Dict, List
|
|
2
|
-
|
|
4
|
+
|
|
3
5
|
import numpy as np
|
|
4
6
|
|
|
7
|
+
from paradigma.constants import DataColumns, DataUnits
|
|
8
|
+
|
|
9
|
+
|
|
5
10
|
class BaseConfig:
|
|
6
11
|
def __init__(self) -> None:
|
|
7
|
-
self.meta_filename =
|
|
8
|
-
self.values_filename =
|
|
9
|
-
self.time_filename =
|
|
12
|
+
self.meta_filename = ""
|
|
13
|
+
self.values_filename = ""
|
|
14
|
+
self.time_filename = ""
|
|
10
15
|
|
|
11
16
|
def set_sensor(self, sensor: str) -> None:
|
|
12
17
|
"""Sets the sensor and derived filenames"""
|
|
@@ -15,7 +20,7 @@ class BaseConfig:
|
|
|
15
20
|
|
|
16
21
|
def set_filenames(self, prefix: str) -> None:
|
|
17
22
|
"""Sets the filenames based on the prefix. This method is duplicated from `gaits_analysis_config.py`.
|
|
18
|
-
|
|
23
|
+
|
|
19
24
|
Parameters
|
|
20
25
|
----------
|
|
21
26
|
prefix : str
|
|
@@ -25,47 +30,67 @@ class BaseConfig:
|
|
|
25
30
|
self.time_filename = f"{prefix}_time.bin"
|
|
26
31
|
self.values_filename = f"{prefix}_values.bin"
|
|
27
32
|
|
|
33
|
+
|
|
28
34
|
class IMUConfig(BaseConfig):
|
|
35
|
+
"""
|
|
36
|
+
IMU configuration that uses DataColumns() to dynamically map available channels.
|
|
37
|
+
Works even if only accelerometer or only gyroscope data is present.
|
|
38
|
+
"""
|
|
29
39
|
|
|
30
|
-
def __init__(self) -> None:
|
|
40
|
+
def __init__(self, column_mapping: dict[str, str] | None = None) -> None:
|
|
31
41
|
super().__init__()
|
|
32
|
-
|
|
33
|
-
self.set_filenames('IMU')
|
|
42
|
+
self.set_filenames("IMU")
|
|
34
43
|
|
|
35
44
|
self.acceleration_units = DataUnits.ACCELERATION
|
|
36
45
|
self.rotation_units = DataUnits.ROTATION
|
|
37
|
-
|
|
38
46
|
self.axes = ["x", "y", "z"]
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
# Generate a default mapping or override with user-provided mapping
|
|
49
|
+
default_mapping = asdict(DataColumns())
|
|
50
|
+
self.column_mapping = {**default_mapping, **(column_mapping or {})}
|
|
51
|
+
|
|
52
|
+
self.time_colname = self.column_mapping["TIME"]
|
|
53
|
+
|
|
54
|
+
self.accelerometer_colnames: List[str] = []
|
|
55
|
+
self.gyroscope_colnames: List[str] = []
|
|
56
|
+
self.gravity_colnames: List[str] = []
|
|
57
|
+
|
|
58
|
+
self.d_channels_accelerometer: Dict[str, str] = {}
|
|
59
|
+
self.d_channels_gyroscope: Dict[str, str] = {}
|
|
60
|
+
|
|
61
|
+
accel_keys = ["ACCELEROMETER_X", "ACCELEROMETER_Y", "ACCELEROMETER_Z"]
|
|
62
|
+
grav_keys = [
|
|
63
|
+
"GRAV_ACCELEROMETER_X",
|
|
64
|
+
"GRAV_ACCELEROMETER_Y",
|
|
65
|
+
"GRAV_ACCELEROMETER_Z",
|
|
54
66
|
]
|
|
67
|
+
gyro_keys = ["GYROSCOPE_X", "GYROSCOPE_Y", "GYROSCOPE_Z"]
|
|
55
68
|
|
|
56
|
-
self.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
if all(k in self.column_mapping for k in accel_keys):
|
|
70
|
+
self.accelerometer_colnames = [self.column_mapping[k] for k in accel_keys]
|
|
71
|
+
|
|
72
|
+
if all(k in self.column_mapping for k in grav_keys):
|
|
73
|
+
self.gravity_colnames = [self.column_mapping[k] for k in grav_keys]
|
|
74
|
+
|
|
75
|
+
self.d_channels_accelerometer = {
|
|
76
|
+
c: self.acceleration_units for c in self.accelerometer_colnames
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if all(k in self.column_mapping for k in gyro_keys):
|
|
80
|
+
self.gyroscope_colnames = [self.column_mapping[k] for k in gyro_keys]
|
|
81
|
+
|
|
82
|
+
self.d_channels_gyroscope = {
|
|
83
|
+
c: self.rotation_units for c in self.gyroscope_colnames
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
self.d_channels_imu: Dict[str, str] = {
|
|
87
|
+
**self.d_channels_accelerometer,
|
|
88
|
+
**self.d_channels_gyroscope,
|
|
65
89
|
}
|
|
66
|
-
self.d_channels_imu = {**self.d_channels_accelerometer, **self.d_channels_gyroscope}
|
|
67
90
|
|
|
68
91
|
self.sampling_frequency = 100
|
|
92
|
+
self.resampling_frequency = 100
|
|
93
|
+
self.tolerance = 3 * 1 / self.sampling_frequency
|
|
69
94
|
self.lower_cutoff_frequency = 0.2
|
|
70
95
|
self.upper_cutoff_frequency = 3.5
|
|
71
96
|
self.filter_order = 4
|
|
@@ -73,30 +98,36 @@ class IMUConfig(BaseConfig):
|
|
|
73
98
|
|
|
74
99
|
class PPGConfig(BaseConfig):
|
|
75
100
|
|
|
76
|
-
def __init__(self) -> None:
|
|
101
|
+
def __init__(self, column_mapping: dict[str, str] | None = None) -> None:
|
|
77
102
|
super().__init__()
|
|
78
103
|
|
|
79
|
-
self.set_filenames(
|
|
104
|
+
self.set_filenames("PPG")
|
|
80
105
|
|
|
81
|
-
|
|
106
|
+
# Generate a default mapping or override with user-provided mapping
|
|
107
|
+
default_mapping = asdict(DataColumns())
|
|
108
|
+
self.column_mapping = {**default_mapping, **(column_mapping or {})}
|
|
109
|
+
|
|
110
|
+
self.time_colname = self.column_mapping["TIME"]
|
|
111
|
+
self.ppg_colname = self.column_mapping["PPG"]
|
|
82
112
|
|
|
83
113
|
self.sampling_frequency = 30
|
|
114
|
+
self.resampling_frequency = 30
|
|
115
|
+
self.tolerance = 3 * 1 / self.sampling_frequency
|
|
84
116
|
self.lower_cutoff_frequency = 0.4
|
|
85
117
|
self.upper_cutoff_frequency = 3.5
|
|
86
118
|
self.filter_order = 4
|
|
87
119
|
|
|
88
|
-
self.d_channels_ppg = {
|
|
89
|
-
DataColumns.PPG: DataUnits.NONE
|
|
90
|
-
}
|
|
120
|
+
self.d_channels_ppg = {self.ppg_colname: DataUnits.NONE}
|
|
91
121
|
|
|
92
122
|
|
|
93
123
|
# Domain base configs
|
|
94
124
|
class GaitConfig(IMUConfig):
|
|
95
125
|
|
|
96
|
-
def __init__(self, step) -> None:
|
|
97
|
-
|
|
126
|
+
def __init__(self, step, column_mapping: dict[str, str] | None = None) -> None:
|
|
127
|
+
# Pass column_mapping through to IMUConfig
|
|
128
|
+
super().__init__(column_mapping=column_mapping)
|
|
98
129
|
|
|
99
|
-
self.set_sensor(
|
|
130
|
+
self.set_sensor("accelerometer")
|
|
100
131
|
|
|
101
132
|
# ----------
|
|
102
133
|
# Segmenting
|
|
@@ -104,7 +135,7 @@ class GaitConfig(IMUConfig):
|
|
|
104
135
|
self.max_segment_gap_s = 1.5
|
|
105
136
|
self.min_segment_length_s = 1.5
|
|
106
137
|
|
|
107
|
-
if step ==
|
|
138
|
+
if step == "gait":
|
|
108
139
|
self.window_length_s: float = 6
|
|
109
140
|
self.window_step_length_s: float = 1
|
|
110
141
|
else:
|
|
@@ -165,11 +196,15 @@ class GaitConfig(IMUConfig):
|
|
|
165
196
|
}
|
|
166
197
|
|
|
167
198
|
for mfcc_coef in range(1, self.mfcc_n_coefficients + 1):
|
|
168
|
-
self.d_channels_values[f"accelerometer_mfcc_{mfcc_coef}"] =
|
|
199
|
+
self.d_channels_values[f"accelerometer_mfcc_{mfcc_coef}"] = (
|
|
200
|
+
DataUnits.GRAVITY
|
|
201
|
+
)
|
|
169
202
|
|
|
170
|
-
if step ==
|
|
203
|
+
if step == "arm_activity":
|
|
171
204
|
for mfcc_coef in range(1, self.mfcc_n_coefficients + 1):
|
|
172
|
-
self.d_channels_values[f"gyroscope_mfcc_{mfcc_coef}"] =
|
|
205
|
+
self.d_channels_values[f"gyroscope_mfcc_{mfcc_coef}"] = (
|
|
206
|
+
DataUnits.GRAVITY
|
|
207
|
+
)
|
|
173
208
|
|
|
174
209
|
|
|
175
210
|
class TremorConfig(IMUConfig):
|
|
@@ -183,7 +218,7 @@ class TremorConfig(IMUConfig):
|
|
|
183
218
|
"""
|
|
184
219
|
super().__init__()
|
|
185
220
|
|
|
186
|
-
self.set_sensor(
|
|
221
|
+
self.set_sensor("gyroscope")
|
|
187
222
|
|
|
188
223
|
# ----------
|
|
189
224
|
# Segmenting
|
|
@@ -194,12 +229,12 @@ class TremorConfig(IMUConfig):
|
|
|
194
229
|
# -----------------
|
|
195
230
|
# Feature extraction
|
|
196
231
|
# -----------------
|
|
197
|
-
self.window_type =
|
|
232
|
+
self.window_type = "hann"
|
|
198
233
|
self.overlap_fraction: float = 0.8
|
|
199
234
|
self.segment_length_psd_s: float = 3
|
|
200
235
|
self.segment_length_spectrogram_s: float = 2
|
|
201
236
|
self.spectral_resolution: float = 0.25
|
|
202
|
-
|
|
237
|
+
|
|
203
238
|
# PSD based features
|
|
204
239
|
self.fmin_peak_search: float = 1
|
|
205
240
|
self.fmax_peak_search: float = 25
|
|
@@ -222,12 +257,13 @@ class TremorConfig(IMUConfig):
|
|
|
222
257
|
# -----------
|
|
223
258
|
# Aggregation
|
|
224
259
|
# -----------
|
|
225
|
-
self.aggregates_tremor_power: List[str] = [
|
|
260
|
+
self.aggregates_tremor_power: List[str] = ["mode_binned", "median", "90p"]
|
|
261
|
+
self.evaluation_points_tremor_power: np.ndarray = np.linspace(0, 6, 301)
|
|
226
262
|
|
|
227
263
|
# -----------------
|
|
228
264
|
# TSDF data storage
|
|
229
265
|
# -----------------
|
|
230
|
-
if step ==
|
|
266
|
+
if step == "features":
|
|
231
267
|
self.d_channels_values: Dict[str, str] = {}
|
|
232
268
|
for mfcc_coef in range(1, self.n_coefficients_mfcc + 1):
|
|
233
269
|
self.d_channels_values[f"mfcc_{mfcc_coef}"] = "unitless"
|
|
@@ -235,81 +271,102 @@ class TremorConfig(IMUConfig):
|
|
|
235
271
|
self.d_channels_values["freq_peak"] = "Hz"
|
|
236
272
|
self.d_channels_values["below_tremor_power"] = "(deg/s)^2"
|
|
237
273
|
self.d_channels_values["tremor_power"] = "(deg/s)^2"
|
|
238
|
-
elif step ==
|
|
274
|
+
elif step == "classification":
|
|
239
275
|
self.d_channels_values = {
|
|
240
276
|
DataColumns.PRED_TREMOR_PROBA: "probability",
|
|
241
277
|
DataColumns.PRED_TREMOR_LOGREG: "boolean",
|
|
242
278
|
DataColumns.PRED_TREMOR_CHECKED: "boolean",
|
|
243
|
-
DataColumns.PRED_ARM_AT_REST: "boolean"
|
|
279
|
+
DataColumns.PRED_ARM_AT_REST: "boolean",
|
|
244
280
|
}
|
|
245
281
|
|
|
246
|
-
|
|
282
|
+
|
|
247
283
|
class PulseRateConfig(PPGConfig):
|
|
248
|
-
def __init__(
|
|
284
|
+
def __init__(
|
|
285
|
+
self,
|
|
286
|
+
sensor: str = "ppg",
|
|
287
|
+
ppg_sampling_frequency: int = 30,
|
|
288
|
+
imu_sampling_frequency: int | None = None,
|
|
289
|
+
min_window_length_s: int = 30,
|
|
290
|
+
accelerometer_colnames: list[str] | None = None,
|
|
291
|
+
) -> None:
|
|
249
292
|
super().__init__()
|
|
250
293
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
294
|
+
self.ppg_sampling_frequency = ppg_sampling_frequency
|
|
295
|
+
|
|
296
|
+
if sensor == "imu":
|
|
297
|
+
if imu_sampling_frequency is not None:
|
|
298
|
+
self.imu_sampling_frequency = imu_sampling_frequency
|
|
299
|
+
else:
|
|
300
|
+
self.imu_sampling_frequency = IMUConfig().sampling_frequency
|
|
301
|
+
warnings.warn(
|
|
302
|
+
f"imu_sampling_frequency not provided, using default of {self.imu_sampling_frequency} Hz"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Windowing parameters
|
|
254
306
|
self.window_length_s: int = 6
|
|
255
307
|
self.window_step_length_s: int = 1
|
|
256
308
|
self.window_overlap_s = self.window_length_s - self.window_step_length_s
|
|
257
309
|
|
|
258
|
-
self.
|
|
310
|
+
self.accelerometer_colnames = accelerometer_colnames
|
|
259
311
|
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
#
|
|
263
|
-
self.
|
|
264
|
-
self.bandwidth = 0.2 # Hz
|
|
265
|
-
self.freq_bin_resolution = 0.05 # Hz
|
|
312
|
+
# Signal quality analysis parameters
|
|
313
|
+
self.freq_band_physio = [0.75, 3] # Hz
|
|
314
|
+
self.bandwidth = 0.2 # Hz
|
|
315
|
+
self.freq_bin_resolution = 0.05 # Hz
|
|
266
316
|
|
|
267
|
-
#
|
|
268
|
-
# Pulse rate estimation
|
|
269
|
-
# ---------------------
|
|
270
|
-
self.set_tfd_length(min_window_length_s) # Set tfd length to default of 30 seconds
|
|
317
|
+
# Pulse rate estimation parameters
|
|
271
318
|
self.threshold_sqa = 0.5
|
|
272
319
|
self.threshold_sqa_accelerometer = 0.10
|
|
273
320
|
|
|
321
|
+
# Set initial sensor and update sampling-dependent params
|
|
322
|
+
self.set_sensor(sensor, min_window_length_s)
|
|
323
|
+
|
|
324
|
+
def set_sensor(self, sensor: str, min_window_length_s: int | None = None) -> None:
|
|
325
|
+
"""Sets the active sensor and recomputes sampling-dependent parameters."""
|
|
326
|
+
if sensor not in ["ppg", "imu"]:
|
|
327
|
+
raise ValueError(f"Invalid sensor type: {sensor}")
|
|
328
|
+
self.sensor = sensor
|
|
329
|
+
|
|
330
|
+
# Decide which frequency to use
|
|
331
|
+
self.sampling_frequency = (
|
|
332
|
+
self.imu_sampling_frequency
|
|
333
|
+
if sensor == "imu"
|
|
334
|
+
else self.ppg_sampling_frequency
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Update all frequency-dependent parameters
|
|
338
|
+
if min_window_length_s is not None:
|
|
339
|
+
self._update_sampling_dependent_params(min_window_length_s)
|
|
340
|
+
else:
|
|
341
|
+
# Reuse previous tfd_length if it exists, else fallback to 30
|
|
342
|
+
self._update_sampling_dependent_params(getattr(self, "tfd_length", 30))
|
|
343
|
+
|
|
344
|
+
def _update_sampling_dependent_params(self, tfd_length: int):
|
|
345
|
+
"""Compute attributes that depend on sampling frequency."""
|
|
346
|
+
|
|
347
|
+
# --- PPG-dependent parameters ---
|
|
348
|
+
self.tfd_length = tfd_length
|
|
349
|
+
self.min_pr_samples = int(round(self.tfd_length * self.ppg_sampling_frequency))
|
|
350
|
+
|
|
274
351
|
pr_est_length = 2 # pulse rate estimation length in seconds
|
|
275
|
-
self.pr_est_samples = pr_est_length * self.
|
|
352
|
+
self.pr_est_samples = pr_est_length * self.ppg_sampling_frequency
|
|
276
353
|
|
|
277
354
|
# Time-frequency distribution parameters
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
win_type_lag = 'hamm'
|
|
355
|
+
win_type_doppler = "hamm"
|
|
356
|
+
win_type_lag = "hamm"
|
|
281
357
|
win_length_doppler = 8
|
|
282
358
|
win_length_lag = 1
|
|
283
|
-
doppler_samples = self.
|
|
284
|
-
lag_samples = win_length_lag * self.
|
|
359
|
+
doppler_samples = self.ppg_sampling_frequency * win_length_doppler
|
|
360
|
+
lag_samples = win_length_lag * self.ppg_sampling_frequency
|
|
361
|
+
self.kern_type = "sep"
|
|
285
362
|
self.kern_params = {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
'win_type': win_type_doppler,
|
|
289
|
-
},
|
|
290
|
-
'lag': {
|
|
291
|
-
'win_length': lag_samples,
|
|
292
|
-
'win_type': win_type_lag,
|
|
293
|
-
}
|
|
363
|
+
"doppler": {"win_length": doppler_samples, "win_type": win_type_doppler},
|
|
364
|
+
"lag": {"win_length": lag_samples, "win_type": win_type_lag},
|
|
294
365
|
}
|
|
295
366
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def set_tfd_length(self, tfd_length: int):
|
|
299
|
-
self.tfd_length = tfd_length
|
|
300
|
-
self.min_pr_samples = int(round(self.tfd_length * self.sampling_frequency))
|
|
301
|
-
|
|
302
|
-
def set_sensor(self, sensor):
|
|
303
|
-
self.sensor = sensor
|
|
304
|
-
|
|
305
|
-
if sensor not in ['ppg', 'imu']:
|
|
306
|
-
raise ValueError(f"Invalid sensor type: {sensor}")
|
|
307
|
-
|
|
308
|
-
if sensor == 'imu':
|
|
309
|
-
self.sampling_frequency = IMUConfig().sampling_frequency
|
|
310
|
-
else:
|
|
311
|
-
self.sampling_frequency = PPGConfig().sampling_frequency
|
|
312
|
-
|
|
367
|
+
# --- Welch / FFT parameters based on current sensor frequency ---
|
|
313
368
|
self.window_length_welch = 3 * self.sampling_frequency
|
|
314
369
|
self.overlap_welch_window = self.window_length_welch // 2
|
|
315
|
-
self.nfft =
|
|
370
|
+
self.nfft = (
|
|
371
|
+
len(np.arange(0, self.sampling_frequency / 2, self.freq_bin_resolution)) * 2
|
|
372
|
+
)
|
paradigma/constants.py
CHANGED
|
@@ -2,39 +2,40 @@ from dataclasses import dataclass
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
@dataclass(frozen=True)
|
|
5
|
-
class DataColumns
|
|
5
|
+
class DataColumns:
|
|
6
6
|
"""
|
|
7
7
|
Class containing the data channels in `tsdf`.
|
|
8
8
|
"""
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
|
|
10
|
+
ACCELEROMETER_X: str = "accelerometer_x"
|
|
11
|
+
ACCELEROMETER_Y: str = "accelerometer_y"
|
|
12
|
+
ACCELEROMETER_Z: str = "accelerometer_z"
|
|
13
|
+
GYROSCOPE_X: str = "gyroscope_x"
|
|
14
|
+
GYROSCOPE_Y: str = "gyroscope_y"
|
|
15
|
+
GYROSCOPE_Z: str = "gyroscope_z"
|
|
16
|
+
PPG: str = "green"
|
|
17
|
+
TIME: str = "time"
|
|
18
|
+
SEGMENT_NR: str = "segment_nr"
|
|
18
19
|
SEGMENT_CAT: str = "segment_category"
|
|
19
20
|
|
|
20
|
-
# Gait
|
|
21
|
-
GRAV_ACCELEROMETER_X
|
|
22
|
-
GRAV_ACCELEROMETER_Y
|
|
23
|
-
GRAV_ACCELEROMETER_Z
|
|
21
|
+
# Gait
|
|
22
|
+
GRAV_ACCELEROMETER_X: str = "accelerometer_x_grav"
|
|
23
|
+
GRAV_ACCELEROMETER_Y: str = "accelerometer_y_grav"
|
|
24
|
+
GRAV_ACCELEROMETER_Z: str = "accelerometer_z_grav"
|
|
24
25
|
PRED_GAIT_PROBA: str = "pred_gait_proba"
|
|
25
|
-
PRED_GAIT
|
|
26
|
+
PRED_GAIT: str = "pred_gait"
|
|
26
27
|
PRED_NO_OTHER_ARM_ACTIVITY_PROBA: str = "pred_no_other_arm_activity_proba"
|
|
27
|
-
PRED_NO_OTHER_ARM_ACTIVITY
|
|
28
|
-
ANGLE
|
|
29
|
-
VELOCITY
|
|
28
|
+
PRED_NO_OTHER_ARM_ACTIVITY: str = "pred_no_other_arm_activity"
|
|
29
|
+
ANGLE: str = "angle"
|
|
30
|
+
VELOCITY: str = "velocity"
|
|
30
31
|
DOMINANT_FREQUENCY: str = "dominant_frequency"
|
|
31
32
|
RANGE_OF_MOTION: str = "range_of_motion"
|
|
32
33
|
PEAK_VELOCITY: str = "peak_velocity"
|
|
33
34
|
|
|
34
35
|
# The following are used in tremor analysis
|
|
35
36
|
PRED_TREMOR_PROBA: str = "pred_tremor_proba"
|
|
36
|
-
PRED_TREMOR_LOGREG
|
|
37
|
-
PRED_TREMOR_CHECKED
|
|
37
|
+
PRED_TREMOR_LOGREG: str = "pred_tremor_logreg"
|
|
38
|
+
PRED_TREMOR_CHECKED: str = "pred_tremor_checked"
|
|
38
39
|
PRED_ARM_AT_REST: str = "pred_arm_at_rest"
|
|
39
40
|
|
|
40
41
|
# Constants for PPG features
|
|
@@ -60,47 +61,51 @@ class DataColumns():
|
|
|
60
61
|
|
|
61
62
|
# Constants for pulse rate
|
|
62
63
|
PULSE_RATE: str = "pulse_rate"
|
|
63
|
-
|
|
64
|
+
|
|
65
|
+
|
|
64
66
|
@dataclass(frozen=True)
|
|
65
|
-
class DataUnits
|
|
67
|
+
class DataUnits:
|
|
66
68
|
"""
|
|
67
69
|
Class containing the data channel unit types in `tsdf`.
|
|
68
70
|
"""
|
|
71
|
+
|
|
69
72
|
ACCELERATION: str = "m/s^2"
|
|
70
73
|
""" The acceleration is in m/s^2. """
|
|
71
|
-
|
|
74
|
+
|
|
72
75
|
ROTATION: str = "deg/s"
|
|
73
76
|
""" The rotation is in degrees per second. """
|
|
74
|
-
|
|
77
|
+
|
|
75
78
|
GRAVITY: str = "g"
|
|
76
79
|
""" The acceleration due to gravity is in g. """
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
POWER_SPECTRAL_DENSITY: str = "g^2/Hz"
|
|
79
82
|
""" The power spectral density is in g^2/Hz. """
|
|
80
|
-
|
|
83
|
+
|
|
81
84
|
FREQUENCY: str = "Hz"
|
|
82
85
|
""" The frequency is in Hz. """
|
|
83
86
|
|
|
84
87
|
NONE: str = "none"
|
|
85
88
|
""" The data channel has no unit. """
|
|
86
|
-
|
|
89
|
+
|
|
87
90
|
|
|
88
91
|
@dataclass(frozen=True)
|
|
89
|
-
class TimeUnit
|
|
92
|
+
class TimeUnit:
|
|
90
93
|
"""
|
|
91
94
|
Class containing the `time` channel unit types in `tsdf`.
|
|
92
95
|
"""
|
|
93
|
-
|
|
96
|
+
|
|
97
|
+
RELATIVE_MS: str = "relative_ms"
|
|
94
98
|
""" The time is relative to the start time in milliseconds. """
|
|
95
|
-
RELATIVE_S
|
|
99
|
+
RELATIVE_S: str = "relative_s"
|
|
96
100
|
""" The time is relative to the start time in seconds. """
|
|
97
|
-
ABSOLUTE_MS
|
|
101
|
+
ABSOLUTE_MS: str = "absolute_ms"
|
|
98
102
|
""" The time is absolute in milliseconds. """
|
|
99
|
-
ABSOLUTE_S
|
|
103
|
+
ABSOLUTE_S: str = "absolute_s"
|
|
100
104
|
""" The time is absolute in seconds. """
|
|
101
|
-
DIFFERENCE_MS
|
|
105
|
+
DIFFERENCE_MS: str = "difference_ms"
|
|
102
106
|
""" The time is the difference between consecutive samples in milliseconds. """
|
|
103
|
-
DIFFERENCE_S
|
|
107
|
+
DIFFERENCE_S: str = "difference_s"
|
|
104
108
|
""" The time is the difference between consecutive samples in seconds. """
|
|
105
109
|
|
|
110
|
+
|
|
106
111
|
UNIX_TICKS_MS: int = 1000
|