paradigma 1.0.3__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 +157 -102
- paradigma/constants.py +39 -34
- paradigma/feature_extraction.py +270 -211
- paradigma/pipelines/gait_pipeline.py +232 -184
- paradigma/pipelines/pulse_rate_pipeline.py +202 -133
- paradigma/pipelines/pulse_rate_utils.py +144 -142
- paradigma/pipelines/tremor_pipeline.py +138 -85
- paradigma/preprocessing.py +179 -110
- paradigma/segmenting.py +138 -113
- paradigma/testing.py +359 -172
- paradigma/util.py +158 -83
- {paradigma-1.0.3.dist-info → paradigma-1.0.4.dist-info}/METADATA +31 -29
- paradigma-1.0.4.dist-info/RECORD +23 -0
- {paradigma-1.0.3.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.3.dist-info → paradigma-1.0.4.dist-info/licenses}/LICENSE +0 -1
- paradigma-1.0.3.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,48 +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
|
|
69
92
|
self.resampling_frequency = 100
|
|
93
|
+
self.tolerance = 3 * 1 / self.sampling_frequency
|
|
70
94
|
self.lower_cutoff_frequency = 0.2
|
|
71
95
|
self.upper_cutoff_frequency = 3.5
|
|
72
96
|
self.filter_order = 4
|
|
@@ -74,30 +98,36 @@ class IMUConfig(BaseConfig):
|
|
|
74
98
|
|
|
75
99
|
class PPGConfig(BaseConfig):
|
|
76
100
|
|
|
77
|
-
def __init__(self) -> None:
|
|
101
|
+
def __init__(self, column_mapping: dict[str, str] | None = None) -> None:
|
|
78
102
|
super().__init__()
|
|
79
103
|
|
|
80
|
-
self.set_filenames(
|
|
104
|
+
self.set_filenames("PPG")
|
|
81
105
|
|
|
82
|
-
|
|
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"]
|
|
83
112
|
|
|
84
113
|
self.sampling_frequency = 30
|
|
114
|
+
self.resampling_frequency = 30
|
|
115
|
+
self.tolerance = 3 * 1 / self.sampling_frequency
|
|
85
116
|
self.lower_cutoff_frequency = 0.4
|
|
86
117
|
self.upper_cutoff_frequency = 3.5
|
|
87
118
|
self.filter_order = 4
|
|
88
119
|
|
|
89
|
-
self.d_channels_ppg = {
|
|
90
|
-
DataColumns.PPG: DataUnits.NONE
|
|
91
|
-
}
|
|
120
|
+
self.d_channels_ppg = {self.ppg_colname: DataUnits.NONE}
|
|
92
121
|
|
|
93
122
|
|
|
94
123
|
# Domain base configs
|
|
95
124
|
class GaitConfig(IMUConfig):
|
|
96
125
|
|
|
97
|
-
def __init__(self, step) -> None:
|
|
98
|
-
|
|
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)
|
|
99
129
|
|
|
100
|
-
self.set_sensor(
|
|
130
|
+
self.set_sensor("accelerometer")
|
|
101
131
|
|
|
102
132
|
# ----------
|
|
103
133
|
# Segmenting
|
|
@@ -105,7 +135,7 @@ class GaitConfig(IMUConfig):
|
|
|
105
135
|
self.max_segment_gap_s = 1.5
|
|
106
136
|
self.min_segment_length_s = 1.5
|
|
107
137
|
|
|
108
|
-
if step ==
|
|
138
|
+
if step == "gait":
|
|
109
139
|
self.window_length_s: float = 6
|
|
110
140
|
self.window_step_length_s: float = 1
|
|
111
141
|
else:
|
|
@@ -166,11 +196,15 @@ class GaitConfig(IMUConfig):
|
|
|
166
196
|
}
|
|
167
197
|
|
|
168
198
|
for mfcc_coef in range(1, self.mfcc_n_coefficients + 1):
|
|
169
|
-
self.d_channels_values[f"accelerometer_mfcc_{mfcc_coef}"] =
|
|
199
|
+
self.d_channels_values[f"accelerometer_mfcc_{mfcc_coef}"] = (
|
|
200
|
+
DataUnits.GRAVITY
|
|
201
|
+
)
|
|
170
202
|
|
|
171
|
-
if step ==
|
|
203
|
+
if step == "arm_activity":
|
|
172
204
|
for mfcc_coef in range(1, self.mfcc_n_coefficients + 1):
|
|
173
|
-
self.d_channels_values[f"gyroscope_mfcc_{mfcc_coef}"] =
|
|
205
|
+
self.d_channels_values[f"gyroscope_mfcc_{mfcc_coef}"] = (
|
|
206
|
+
DataUnits.GRAVITY
|
|
207
|
+
)
|
|
174
208
|
|
|
175
209
|
|
|
176
210
|
class TremorConfig(IMUConfig):
|
|
@@ -184,7 +218,7 @@ class TremorConfig(IMUConfig):
|
|
|
184
218
|
"""
|
|
185
219
|
super().__init__()
|
|
186
220
|
|
|
187
|
-
self.set_sensor(
|
|
221
|
+
self.set_sensor("gyroscope")
|
|
188
222
|
|
|
189
223
|
# ----------
|
|
190
224
|
# Segmenting
|
|
@@ -195,12 +229,12 @@ class TremorConfig(IMUConfig):
|
|
|
195
229
|
# -----------------
|
|
196
230
|
# Feature extraction
|
|
197
231
|
# -----------------
|
|
198
|
-
self.window_type =
|
|
232
|
+
self.window_type = "hann"
|
|
199
233
|
self.overlap_fraction: float = 0.8
|
|
200
234
|
self.segment_length_psd_s: float = 3
|
|
201
235
|
self.segment_length_spectrogram_s: float = 2
|
|
202
236
|
self.spectral_resolution: float = 0.25
|
|
203
|
-
|
|
237
|
+
|
|
204
238
|
# PSD based features
|
|
205
239
|
self.fmin_peak_search: float = 1
|
|
206
240
|
self.fmax_peak_search: float = 25
|
|
@@ -223,13 +257,13 @@ class TremorConfig(IMUConfig):
|
|
|
223
257
|
# -----------
|
|
224
258
|
# Aggregation
|
|
225
259
|
# -----------
|
|
226
|
-
self.aggregates_tremor_power: List[str] = [
|
|
227
|
-
self.evaluation_points_tremor_power: np.ndarray = np.linspace(0, 6, 301)
|
|
260
|
+
self.aggregates_tremor_power: List[str] = ["mode_binned", "median", "90p"]
|
|
261
|
+
self.evaluation_points_tremor_power: np.ndarray = np.linspace(0, 6, 301)
|
|
228
262
|
|
|
229
263
|
# -----------------
|
|
230
264
|
# TSDF data storage
|
|
231
265
|
# -----------------
|
|
232
|
-
if step ==
|
|
266
|
+
if step == "features":
|
|
233
267
|
self.d_channels_values: Dict[str, str] = {}
|
|
234
268
|
for mfcc_coef in range(1, self.n_coefficients_mfcc + 1):
|
|
235
269
|
self.d_channels_values[f"mfcc_{mfcc_coef}"] = "unitless"
|
|
@@ -237,81 +271,102 @@ class TremorConfig(IMUConfig):
|
|
|
237
271
|
self.d_channels_values["freq_peak"] = "Hz"
|
|
238
272
|
self.d_channels_values["below_tremor_power"] = "(deg/s)^2"
|
|
239
273
|
self.d_channels_values["tremor_power"] = "(deg/s)^2"
|
|
240
|
-
elif step ==
|
|
274
|
+
elif step == "classification":
|
|
241
275
|
self.d_channels_values = {
|
|
242
276
|
DataColumns.PRED_TREMOR_PROBA: "probability",
|
|
243
277
|
DataColumns.PRED_TREMOR_LOGREG: "boolean",
|
|
244
278
|
DataColumns.PRED_TREMOR_CHECKED: "boolean",
|
|
245
|
-
DataColumns.PRED_ARM_AT_REST: "boolean"
|
|
279
|
+
DataColumns.PRED_ARM_AT_REST: "boolean",
|
|
246
280
|
}
|
|
247
281
|
|
|
248
|
-
|
|
282
|
+
|
|
249
283
|
class PulseRateConfig(PPGConfig):
|
|
250
|
-
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:
|
|
251
292
|
super().__init__()
|
|
252
293
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
256
306
|
self.window_length_s: int = 6
|
|
257
307
|
self.window_step_length_s: int = 1
|
|
258
308
|
self.window_overlap_s = self.window_length_s - self.window_step_length_s
|
|
259
309
|
|
|
260
|
-
self.
|
|
310
|
+
self.accelerometer_colnames = accelerometer_colnames
|
|
261
311
|
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
#
|
|
265
|
-
self.
|
|
266
|
-
self.bandwidth = 0.2 # Hz
|
|
267
|
-
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
|
|
268
316
|
|
|
269
|
-
#
|
|
270
|
-
# Pulse rate estimation
|
|
271
|
-
# ---------------------
|
|
272
|
-
self.set_tfd_length(min_window_length_s) # Set tfd length to default of 30 seconds
|
|
317
|
+
# Pulse rate estimation parameters
|
|
273
318
|
self.threshold_sqa = 0.5
|
|
274
319
|
self.threshold_sqa_accelerometer = 0.10
|
|
275
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
|
+
|
|
276
351
|
pr_est_length = 2 # pulse rate estimation length in seconds
|
|
277
|
-
self.pr_est_samples = pr_est_length * self.
|
|
352
|
+
self.pr_est_samples = pr_est_length * self.ppg_sampling_frequency
|
|
278
353
|
|
|
279
354
|
# Time-frequency distribution parameters
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
win_type_lag = 'hamm'
|
|
355
|
+
win_type_doppler = "hamm"
|
|
356
|
+
win_type_lag = "hamm"
|
|
283
357
|
win_length_doppler = 8
|
|
284
358
|
win_length_lag = 1
|
|
285
|
-
doppler_samples = self.
|
|
286
|
-
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"
|
|
287
362
|
self.kern_params = {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
'win_type': win_type_doppler,
|
|
291
|
-
},
|
|
292
|
-
'lag': {
|
|
293
|
-
'win_length': lag_samples,
|
|
294
|
-
'win_type': win_type_lag,
|
|
295
|
-
}
|
|
363
|
+
"doppler": {"win_length": doppler_samples, "win_type": win_type_doppler},
|
|
364
|
+
"lag": {"win_length": lag_samples, "win_type": win_type_lag},
|
|
296
365
|
}
|
|
297
366
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def set_tfd_length(self, tfd_length: int):
|
|
301
|
-
self.tfd_length = tfd_length
|
|
302
|
-
self.min_pr_samples = int(round(self.tfd_length * self.sampling_frequency))
|
|
303
|
-
|
|
304
|
-
def set_sensor(self, sensor):
|
|
305
|
-
self.sensor = sensor
|
|
306
|
-
|
|
307
|
-
if sensor not in ['ppg', 'imu']:
|
|
308
|
-
raise ValueError(f"Invalid sensor type: {sensor}")
|
|
309
|
-
|
|
310
|
-
if sensor == 'imu':
|
|
311
|
-
self.sampling_frequency = IMUConfig().sampling_frequency
|
|
312
|
-
else:
|
|
313
|
-
self.sampling_frequency = PPGConfig().sampling_frequency
|
|
314
|
-
|
|
367
|
+
# --- Welch / FFT parameters based on current sensor frequency ---
|
|
315
368
|
self.window_length_welch = 3 * self.sampling_frequency
|
|
316
369
|
self.overlap_welch_window = self.window_length_welch // 2
|
|
317
|
-
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
|