paradigma 0.1.2__py3-none-any.whl → 0.2.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.
Files changed (112) hide show
  1. paradigma/__init__.py +1 -3
  2. paradigma/constants.py +35 -0
  3. paradigma/feature_extraction.py +678 -0
  4. paradigma/gait_analysis.py +413 -0
  5. paradigma/gait_analysis_config.py +244 -0
  6. paradigma/heart_rate_analysis.py +127 -0
  7. paradigma/heart_rate_analysis_config.py +9 -0
  8. paradigma/heart_rate_util.py +173 -0
  9. paradigma/imu_preprocessing.py +229 -0
  10. paradigma/ppg/classifier/LR_PPG_quality.pkl +0 -0
  11. paradigma/ppg/classifier/LR_model.mat +0 -0
  12. paradigma/ppg/feat_extraction/acc_feature.m +20 -0
  13. paradigma/ppg/feat_extraction/peakdet.m +64 -0
  14. paradigma/ppg/feat_extraction/ppg_features.m +53 -0
  15. paradigma/ppg/glob_functions/extract_hr_segments.m +37 -0
  16. paradigma/ppg/glob_functions/extract_overlapping_segments.m +23 -0
  17. paradigma/ppg/glob_functions/jsonlab/AUTHORS.txt +41 -0
  18. paradigma/ppg/glob_functions/jsonlab/ChangeLog.txt +74 -0
  19. paradigma/ppg/glob_functions/jsonlab/LICENSE_BSD.txt +25 -0
  20. paradigma/ppg/glob_functions/jsonlab/LICENSE_GPLv3.txt +699 -0
  21. paradigma/ppg/glob_functions/jsonlab/README.txt +394 -0
  22. paradigma/ppg/glob_functions/jsonlab/examples/.svn/entries +368 -0
  23. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_jsonlab_basic.m.svn-base +180 -0
  24. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/demo_ubjson_basic.m.svn-base +180 -0
  25. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example1.json.svn-base +23 -0
  26. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example2.json.svn-base +22 -0
  27. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example3.json.svn-base +11 -0
  28. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/example4.json.svn-base +34 -0
  29. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_basictest.matlab.svn-base +662 -0
  30. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.m.svn-base +27 -0
  31. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_selftest.matlab.svn-base +144 -0
  32. paradigma/ppg/glob_functions/jsonlab/examples/.svn/text-base/jsonlab_speedtest.m.svn-base +21 -0
  33. paradigma/ppg/glob_functions/jsonlab/examples/demo_jsonlab_basic.m +180 -0
  34. paradigma/ppg/glob_functions/jsonlab/examples/demo_ubjson_basic.m +180 -0
  35. paradigma/ppg/glob_functions/jsonlab/examples/example1.json +23 -0
  36. paradigma/ppg/glob_functions/jsonlab/examples/example2.json +22 -0
  37. paradigma/ppg/glob_functions/jsonlab/examples/example3.json +11 -0
  38. paradigma/ppg/glob_functions/jsonlab/examples/example4.json +34 -0
  39. paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_basictest.matlab +662 -0
  40. paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.m +27 -0
  41. paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_selftest.matlab +144 -0
  42. paradigma/ppg/glob_functions/jsonlab/examples/jsonlab_speedtest.m +21 -0
  43. paradigma/ppg/glob_functions/jsonlab/jsonopt.m +32 -0
  44. paradigma/ppg/glob_functions/jsonlab/loadjson.m +566 -0
  45. paradigma/ppg/glob_functions/jsonlab/loadubjson.m +528 -0
  46. paradigma/ppg/glob_functions/jsonlab/mergestruct.m +33 -0
  47. paradigma/ppg/glob_functions/jsonlab/savejson.m +475 -0
  48. paradigma/ppg/glob_functions/jsonlab/saveubjson.m +504 -0
  49. paradigma/ppg/glob_functions/jsonlab/varargin2struct.m +40 -0
  50. paradigma/ppg/glob_functions/sample_prob_final.m +49 -0
  51. paradigma/ppg/glob_functions/synchronization.m +76 -0
  52. paradigma/ppg/glob_functions/tsdf_scan_meta.m +22 -0
  53. paradigma/ppg/hr_functions/Long_TFD_JOT.m +37 -0
  54. paradigma/ppg/hr_functions/PPG_TFD_HR.m +59 -0
  55. paradigma/ppg/hr_functions/TFD toolbox JOT/.gitignore +4 -0
  56. paradigma/ppg/hr_functions/TFD toolbox JOT/CHANGELOG.md +23 -0
  57. paradigma/ppg/hr_functions/TFD toolbox JOT/LICENCE.md +27 -0
  58. paradigma/ppg/hr_functions/TFD toolbox JOT/README.md +251 -0
  59. paradigma/ppg/hr_functions/TFD toolbox JOT/README.pdf +0 -0
  60. paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_kern.m +142 -0
  61. paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_Doppler_lag_kern.m +314 -0
  62. paradigma/ppg/hr_functions/TFD toolbox JOT/common/gen_lag_kern.m +123 -0
  63. paradigma/ppg/hr_functions/TFD toolbox JOT/dec_tfd.m +154 -0
  64. paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_di_gdtfd.m +194 -0
  65. paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_li_gdtfd.m +200 -0
  66. paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_nonsep_gdtfd.m +229 -0
  67. paradigma/ppg/hr_functions/TFD toolbox JOT/decimated_TFDs/dec_sep_gdtfd.m +241 -0
  68. paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/di_gdtfd.m +157 -0
  69. paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/li_gdtfd.m +190 -0
  70. paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/nonsep_gdtfd.m +196 -0
  71. paradigma/ppg/hr_functions/TFD toolbox JOT/full_TFDs/sep_gdtfd.m +199 -0
  72. paradigma/ppg/hr_functions/TFD toolbox JOT/full_tfd.m +144 -0
  73. paradigma/ppg/hr_functions/TFD toolbox JOT/load_curdir.m +13 -0
  74. paradigma/ppg/hr_functions/TFD toolbox JOT/pics/decimated_TFDs_examples.png +0 -0
  75. paradigma/ppg/hr_functions/TFD toolbox JOT/pics/full_TFDs_examples.png +0 -0
  76. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/check_dec_params_seq.m +79 -0
  77. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispEE.m +9 -0
  78. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/dispVars.m +26 -0
  79. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/disp_bytes.m +25 -0
  80. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_full.m +40 -0
  81. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/fold_vector_half.m +34 -0
  82. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/gen_LFM.m +29 -0
  83. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_analytic_signal.m +76 -0
  84. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/get_window.m +176 -0
  85. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/isreal_fn.m +11 -0
  86. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/padWin.m +97 -0
  87. paradigma/ppg/hr_functions/TFD toolbox JOT/utils/vtfd.m +149 -0
  88. paradigma/ppg/preprocessing/preprocessing_imu.m +15 -0
  89. paradigma/ppg/preprocessing/preprocessing_ppg.m +13 -0
  90. paradigma/ppg_preprocessing.py +313 -0
  91. paradigma/preprocessing_config.py +64 -0
  92. paradigma/quantification.py +58 -0
  93. paradigma/tremor/TremorFeaturesAndClassification.m +345 -0
  94. paradigma/tremor/feat_extraction/DerivativesExtract.m +22 -0
  95. paradigma/tremor/feat_extraction/ExtractBandSignalsRMS.m +72 -0
  96. paradigma/tremor/feat_extraction/MFCCExtract.m +100 -0
  97. paradigma/tremor/feat_extraction/PSDBandPower.m +52 -0
  98. paradigma/tremor/feat_extraction/PSDEst.m +63 -0
  99. paradigma/tremor/feat_extraction/PSDExtrAxis.m +88 -0
  100. paradigma/tremor/feat_extraction/PSDExtrOpt.m +95 -0
  101. paradigma/tremor/preprocessing/InterpData.m +32 -0
  102. paradigma/tremor/weekly_aggregates/WeeklyAggregates.m +295 -0
  103. paradigma/util.py +50 -0
  104. paradigma/windowing.py +217 -0
  105. paradigma-0.2.0.dist-info/LICENSE +192 -0
  106. paradigma-0.2.0.dist-info/METADATA +58 -0
  107. paradigma-0.2.0.dist-info/RECORD +108 -0
  108. paradigma/dummy.py +0 -3
  109. paradigma-0.1.2.dist-info/LICENSE +0 -201
  110. paradigma-0.1.2.dist-info/METADATA +0 -18
  111. paradigma-0.1.2.dist-info/RECORD +0 -6
  112. {paradigma-0.1.2.dist-info → paradigma-0.2.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,413 @@
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+
5
+ import tsdf
6
+
7
+ from paradigma.constants import DataColumns
8
+ from paradigma.gait_analysis_config import GaitFeatureExtractionConfig, GaitDetectionConfig, \
9
+ ArmSwingFeatureExtractionConfig, ArmSwingDetectionConfig, ArmSwingQuantificationConfig
10
+ from paradigma.feature_extraction import extract_temporal_domain_features, \
11
+ extract_spectral_domain_features, pca_transform_gyroscope, compute_angle, \
12
+ remove_moving_average_angle, extract_angle_extremes, extract_range_of_motion, \
13
+ extract_peak_angular_velocity, signal_to_ffts, get_dominant_frequency, compute_perc_power
14
+ from paradigma.quantification import aggregate_segments
15
+ from paradigma.windowing import tabulate_windows, create_segments, discard_segments
16
+ from paradigma.util import get_end_iso8601, write_data, read_metadata
17
+
18
+
19
+ def extract_gait_features(input_path: str, output_path: str, config: GaitFeatureExtractionConfig) -> None:
20
+ # load data
21
+ metadata_time, metadata_samples = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename)
22
+ df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns)
23
+
24
+ # group sequences of timestamps into windows
25
+ df_windowed = tabulate_windows(
26
+ df=df,
27
+ time_column_name=config.time_colname,
28
+ data_point_level_cols=config.l_data_point_level_cols,
29
+ window_length_s=config.window_length_s,
30
+ window_step_size_s=config.window_step_size_s,
31
+ sampling_frequency=config.sampling_frequency
32
+ )
33
+
34
+
35
+ # compute statistics of the temporal domain signals
36
+ df_windowed = extract_temporal_domain_features(config, df_windowed, l_gravity_stats=['mean', 'std'])
37
+
38
+ # transform the signals from the temporal domain to the spectral domain using the fast fourier transform
39
+ # and extract spectral features
40
+ df_windowed = extract_spectral_domain_features(config, df_windowed, config.sensor, config.l_accelerometer_cols)
41
+
42
+ end_iso8601 = get_end_iso8601(start_iso8601=metadata_time.start_iso8601,
43
+ window_length_seconds=int(df_windowed[config.time_colname][-1:].values[0] + config.window_length_s))
44
+
45
+ metadata_samples.end_iso8601 = end_iso8601
46
+ metadata_samples.file_name = 'gait_values.bin'
47
+ metadata_time.end_iso8601 = end_iso8601
48
+ metadata_time.file_name = 'gait_time.bin'
49
+
50
+ metadata_samples.channels = list(config.d_channels_values.keys())
51
+ metadata_samples.units = list(config.d_channels_values.values())
52
+
53
+ metadata_time.channels = ['time']
54
+ metadata_time.units = ['relative_time_ms']
55
+ metadata_time.data_type = np.int64
56
+
57
+ write_data(metadata_time, metadata_samples, output_path, 'gait_meta.json', df_windowed)
58
+
59
+
60
+ def detect_gait(input_path: str, output_path: str, path_to_classifier_input: str, config: GaitDetectionConfig) -> None:
61
+
62
+ # Load the data
63
+ metadata_time, metadata_samples = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename)
64
+ df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns)
65
+
66
+ # Initialize the classifier
67
+ clf = pd.read_pickle(os.path.join(path_to_classifier_input, config.classifier_file_name))
68
+ with open(os.path.join(path_to_classifier_input, config.thresholds_file_name), 'r') as f:
69
+ threshold = float(f.read())
70
+
71
+ # Prepare the data
72
+ clf.feature_names_in_ = [f'{x}_power_below_gait' for x in config.l_accel_cols] + \
73
+ [f'{x}_power_gait' for x in config.l_accel_cols] + \
74
+ [f'{x}_power_tremor' for x in config.l_accel_cols] + \
75
+ [f'{x}_power_above_tremor' for x in config.l_accel_cols] + \
76
+ ['std_norm_acc'] + [f'cc_{i}_accelerometer' for i in range(1, 13)] + [f'grav_{x}_{y}' for x in config.l_accel_cols for y in ['mean', 'std']] + \
77
+ [f'{x}_dominant_frequency' for x in config.l_accel_cols]
78
+ X = df.loc[:, clf.feature_names_in_]
79
+
80
+ # Make prediction
81
+ df['pred_gait_proba'] = clf.predict_proba(X)[:, 1]
82
+ df['pred_gait'] = df['pred_gait_proba'] > threshold
83
+
84
+ # Prepare the metadata
85
+ metadata_samples.file_name = 'gait_values.bin'
86
+ metadata_time.file_name = 'gait_time.bin'
87
+
88
+ metadata_samples.channels = ['pred_gait_proba']
89
+ metadata_samples.units = ['probability']
90
+ metadata_samples.data_type = np.float32
91
+ metadata_samples.bits = 32
92
+
93
+ metadata_time.channels = [config.time_colname]
94
+ metadata_time.units = ['relative_time_ms']
95
+ metadata_time.data_type = np.int32
96
+ metadata_time.bits = 32
97
+
98
+ write_data(metadata_time, metadata_samples, output_path, 'gait_meta.json', df)
99
+
100
+
101
+ def extract_arm_swing_features(input_path: str, output_path: str, config: ArmSwingFeatureExtractionConfig) -> None:
102
+ # load accelerometer and gyroscope data
103
+ l_dfs = []
104
+ for sensor in ['accelerometer', 'gyroscope']:
105
+ config.set_sensor(sensor)
106
+ meta_filename = f'{sensor}_meta.json'
107
+ values_filename = f'{sensor}_samples.bin'
108
+ time_filename = f'{sensor}_time.bin'
109
+
110
+ metadata_dict = tsdf.load_metadata_from_path(os.path.join(input_path, meta_filename))
111
+ metadata_time = metadata_dict[time_filename]
112
+ metadata_samples = metadata_dict[values_filename]
113
+ l_dfs.append(tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns))
114
+
115
+ df = pd.merge(l_dfs[0], l_dfs[1], on=config.time_colname)
116
+
117
+ # temporary add "random" predictions
118
+ df[config.pred_gait_colname] = np.concatenate([np.repeat([1], df.shape[0]//3), np.repeat([0], df.shape[0]//3), np.repeat([1], df.shape[0] + 1 - 2*df.shape[0]//3)], axis=0)
119
+
120
+ # perform principal component analysis on the gyroscope signals to obtain the angular velocity in the
121
+ # direction of the swing of the arm
122
+ df[config.velocity_colname] = pca_transform_gyroscope(
123
+ df=df,
124
+ y_gyro_colname=DataColumns.GYROSCOPE_Y,
125
+ z_gyro_colname=DataColumns.GYROSCOPE_Z,
126
+ pred_gait_colname=config.pred_gait_colname
127
+ )
128
+
129
+ # integrate the angular velocity to obtain an estimation of the angle
130
+ df[config.angle_colname] = compute_angle(
131
+ velocity_col=df[config.velocity_colname],
132
+ time_col=df[config.time_colname]
133
+ )
134
+
135
+ # remove the moving average from the angle to account for possible drift caused by the integration
136
+ # of noise in the angular velocity
137
+ df[config.angle_smooth_colname] = remove_moving_average_angle(
138
+ angle_col=df[config.angle_colname],
139
+ sampling_frequency=config.sampling_frequency
140
+ )
141
+
142
+ # use only predicted gait for the subsequent steps
143
+ df = df.loc[df[config.pred_gait_colname]==1].reset_index(drop=True)
144
+
145
+ # group consecutive timestamps into segments with new segments starting after a pre-specified gap
146
+ df_segments = create_segments(
147
+ df=df,
148
+ time_colname=config.time_colname,
149
+ segment_nr_colname='segment_nr',
150
+ minimum_gap_s=3
151
+ )
152
+
153
+ # remove any segments that do not adhere to predetermined criteria
154
+ df_segments = discard_segments(
155
+ df=df_segments,
156
+ time_colname=config.time_colname,
157
+ segment_nr_colname='segment_nr',
158
+ minimum_segment_length_s=3
159
+ )
160
+
161
+ # create windows of a fixed length and step size from the time series per segment
162
+ l_dfs = []
163
+ for segment_nr in df_segments[config.segment_nr_colname].unique():
164
+ df_single_segment = df_segments.loc[df_segments[config.segment_nr_colname]==segment_nr].copy().reset_index(drop=True)
165
+ l_dfs.append(tabulate_windows(
166
+ df=df_single_segment,
167
+ time_column_name=config.time_colname,
168
+ segment_nr_colname=config.segment_nr_colname,
169
+ data_point_level_cols=config.l_data_point_level_cols,
170
+ window_length_s=config.window_length_s,
171
+ window_step_size_s=config.window_step_size_s,
172
+ segment_nr=segment_nr,
173
+ sampling_frequency=config.sampling_frequency,
174
+ )
175
+ )
176
+ df_windowed = pd.concat(l_dfs).reset_index(drop=True)
177
+
178
+ del df, df_segments
179
+
180
+ # transform the angle from the temporal domain to the spectral domain using the fast fourier transform
181
+ df_windowed['angle_freqs'], df_windowed['angle_fft'] = signal_to_ffts(
182
+ sensor_col=df_windowed[config.angle_smooth_colname],
183
+ window_type=config.window_type,
184
+ sampling_frequency=config.sampling_frequency)
185
+
186
+ # obtain the dominant frequency of the angle signal in the frequency band of interest
187
+ # defined by the highest peak in the power spectrum
188
+ df_windowed['angle_dominant_frequency'] = df_windowed.apply(
189
+ lambda x: get_dominant_frequency(signal_ffts=x['angle_fft'],
190
+ signal_freqs=x['angle_freqs'],
191
+ fmin=config.power_band_low_frequency,
192
+ fmax=config.power_band_high_frequency
193
+ ), axis=1
194
+ )
195
+
196
+ df_windowed = df_windowed.drop(columns=['angle_fft', 'angle_freqs'])
197
+
198
+ # compute the percentage of power in the frequency band of interest (i.e., the frequency band of the arm swing)
199
+ df_windowed['angle_perc_power'] = df_windowed[config.angle_smooth_colname].apply(
200
+ lambda x: compute_perc_power(
201
+ sensor_col=x,
202
+ fmin_band=config.power_band_low_frequency,
203
+ fmax_band=config.power_band_high_frequency,
204
+ fmin_total=config.spectrum_low_frequency,
205
+ fmax_total=config.spectrum_high_frequency,
206
+ sampling_frequency=config.sampling_frequency,
207
+ window_type=config.window_type
208
+ )
209
+ )
210
+
211
+ # note to eScience: why are the columns 'angle_new_minima', 'angle_new_maxima',
212
+ # 'angle_minima_deleted' and 'angle_maxima deleted' created here? Should a copy
213
+ # of 'df_windowed' be created inside 'extract_angle_extremes' to prevent this from
214
+ # happening?
215
+ # determine the extrema (minima and maxima) of the angle signal
216
+ extract_angle_extremes(
217
+ df=df_windowed,
218
+ angle_colname=config.angle_smooth_colname,
219
+ dominant_frequency_colname='angle_dominant_frequency',
220
+ sampling_frequency=config.sampling_frequency
221
+ )
222
+
223
+ df_windowed = df_windowed.drop(columns=[config.angle_smooth_colname])
224
+
225
+ # calculate the change in angle between consecutive extrema (minima and maxima) of the angle signal inside the window
226
+ df_windowed['angle_amplitudes'] = extract_range_of_motion(
227
+ angle_extrema_values_col=df_windowed['angle_extrema_values']
228
+ )
229
+
230
+ df_windowed = df_windowed.drop(columns=['angle_extrema_values'])
231
+
232
+ # aggregate the changes in angle between consecutive extrema to obtain the range of motion
233
+ df_windowed['range_of_motion'] = df_windowed['angle_amplitudes'].apply(lambda x: np.mean(x) if len(x) > 0 else 0).replace(np.nan, 0)
234
+
235
+ df_windowed = df_windowed.drop(columns=['angle_amplitudes'])
236
+
237
+ # compute the forward and backward peak angular velocity using the extrema of the angular velocity
238
+ extract_peak_angular_velocity(
239
+ df=df_windowed,
240
+ velocity_colname=config.velocity_colname,
241
+ angle_minima_colname='angle_minima',
242
+ angle_maxima_colname='angle_maxima'
243
+ )
244
+
245
+ df_windowed = df_windowed.drop(columns=['angle_minima','angle_maxima', 'angle_new_minima',
246
+ 'angle_new_maxima', config.velocity_colname])
247
+
248
+ # compute aggregated measures of the peak angular velocity
249
+ for dir in ['forward', 'backward']:
250
+ df_windowed[f'{dir}_peak_ang_vel_mean'] = df_windowed[f'{dir}_peak_ang_vel'].apply(lambda x: np.mean(x) if len(x) > 0 else 0)
251
+ df_windowed[f'{dir}_peak_ang_vel_std'] = df_windowed[f'{dir}_peak_ang_vel'].apply(lambda x: np.std(x) if len(x) > 0 else 0)
252
+
253
+ df_windowed = df_windowed.drop(columns=[f'{dir}_peak_ang_vel'])
254
+
255
+ # compute statistics of the temporal domain accelerometer signals
256
+ df_windowed = extract_temporal_domain_features(config, df_windowed, l_gravity_stats=['mean', 'std'])
257
+
258
+ # transform the accelerometer and gyroscope signals from the temporal domain to the spectral domain
259
+ # using the fast fourier transform and extract spectral features
260
+ for sensor, l_sensor_colnames in zip(['accelerometer', 'gyroscope'], [config.l_accelerometer_cols, config.l_gyroscope_cols]):
261
+ df_windowed = extract_spectral_domain_features(config, df_windowed, sensor, l_sensor_colnames)
262
+
263
+ end_iso8601 = get_end_iso8601(metadata_samples.start_iso8601,
264
+ df_windowed[config.time_colname][-1:].values[0] + config.window_length_s)
265
+
266
+ metadata_samples.end_iso8601 = end_iso8601
267
+ metadata_samples.file_name = 'arm_swing_values.bin'
268
+ metadata_time.end_iso8601 = end_iso8601
269
+ metadata_time.file_name = 'arm_swing_time.bin'
270
+
271
+ metadata_samples.channels = list(config.d_channels_values.keys())
272
+ metadata_samples.units = list(config.d_channels_values.values())
273
+ metadata_samples.data_type = np.float32
274
+ metadata_samples.bits = 32
275
+
276
+ metadata_time.channels = [config.time_colname]
277
+ metadata_time.units = ['relative_time_ms']
278
+ metadata_time.data_type = np.int32
279
+ metadata_time.bits = 32
280
+
281
+ write_data(metadata_time, metadata_samples, output_path, 'arm_swing_meta.json', df_windowed)
282
+
283
+
284
+ def detect_arm_swing(input_path: str, output_path: str, path_to_classifier_input: str, config: ArmSwingDetectionConfig) -> None:
285
+ # Load the data
286
+ metadata_time, metadata_samples = read_metadata(input_path, config.meta_filename, config.time_filename, config.values_filename)
287
+ df = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns)
288
+
289
+ # Initialize the classifier
290
+ clf = pd.read_pickle(os.path.join(path_to_classifier_input, config.classifier_file_name))
291
+
292
+ # Prepare the data
293
+ clf.feature_names_in_ = ['std_norm_acc'] + [f'{x}_power_below_gait' for x in config.l_accel_cols] + \
294
+ [f'{x}_power_gait' for x in config.l_accel_cols] + \
295
+ [f'{x}_power_tremor' for x in config.l_accel_cols] + \
296
+ [f'{x}_power_above_tremor' for x in config.l_accel_cols] + \
297
+ [f'cc_{i}_accelerometer' for i in range(1, 13)] + [f'cc_{i}_gyroscope' for i in range(1, 13)] + \
298
+ [f'grav_{x}_mean' for x in config.l_accel_cols] + [f'grav_{x}_std' for x in config.l_accel_cols] + \
299
+ ['range_of_motion', 'forward_peak_ang_vel_mean', 'backward_peak_ang_vel_mean', 'forward_peak_ang_vel_std',
300
+ 'backward_peak_ang_vel_std', 'angle_perc_power', 'angle_dominant_frequency'] + \
301
+ [f'{x}_dominant_frequency' for x in config.l_accel_cols]
302
+
303
+ X = df.loc[:, clf.feature_names_in_]
304
+
305
+ # Make prediction
306
+ # df['pred_arm_swing_proba'] = clf.predict_proba(X)[:, 1]
307
+ df['pred_arm_swing'] = clf.predict(X)
308
+
309
+ # Prepare the metadata
310
+ metadata_samples.file_name = 'arm_swing_values.bin'
311
+ metadata_time.file_name = 'arm_swing_time.bin'
312
+
313
+ metadata_samples.channels = ['pred_arm_swing']
314
+ metadata_samples.units = ['boolean']
315
+ metadata_samples.data_type = np.int8
316
+ metadata_samples.bits = 8
317
+
318
+ metadata_time.channels = ['time']
319
+ metadata_time.units = ['relative_time_ms']
320
+ metadata_time.data_type = np.int32
321
+ metadata_time.bits = 32
322
+
323
+ write_data(metadata_time, metadata_samples, output_path, 'arm_swing_meta.json', df)
324
+
325
+
326
+ def quantify_arm_swing(path_to_feature_input: str, path_to_prediction_input: str, output_path: str, config: ArmSwingQuantificationConfig) -> None:
327
+ # Load the features & predictions
328
+ metadata_time, metadata_samples = read_metadata(path_to_feature_input, config.meta_filename, config.time_filename, config.values_filename)
329
+ df_features = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns)
330
+
331
+ metadata_dict = tsdf.load_metadata_from_path(os.path.join(path_to_prediction_input, config.meta_filename))
332
+ metadata_time = metadata_dict[config.time_filename]
333
+ metadata_samples = metadata_dict[config.values_filename]
334
+ df_predictions = tsdf.load_dataframe_from_binaries([metadata_time, metadata_samples], tsdf.constants.ConcatenationType.columns)
335
+
336
+ # Validate
337
+ # dataframes have same length
338
+ assert df_features.shape[0] == df_predictions.shape[0]
339
+
340
+ # dataframes have same time column
341
+ assert df_features['time'].equals(df_predictions['time'])
342
+
343
+ # Prepare the data
344
+
345
+ # subset features
346
+ l_feature_cols = ['time', 'range_of_motion', 'forward_peak_ang_vel_mean', 'backward_peak_ang_vel_mean']
347
+ df_features = df_features[l_feature_cols]
348
+
349
+ # concatenate features and predictions
350
+ df = pd.concat([df_features, df_predictions[config.pred_arm_swing_colname]], axis=1)
351
+
352
+ # temporarily for testing: manually determine predictions
353
+ df[config.pred_arm_swing_colname] = np.concatenate([np.repeat([1], df.shape[0]//3), np.repeat([0], df.shape[0]//3), np.repeat([1], df.shape[0] - 2*df.shape[0]//3)], axis=0)
354
+
355
+ # keep only predicted arm swing
356
+ df_arm_swing = df.loc[df[config.pred_arm_swing_colname]==1].copy().reset_index(drop=True)
357
+
358
+ del df
359
+
360
+ # create peak angular velocity
361
+ df_arm_swing.loc[:, 'peak_ang_vel'] = df_arm_swing.loc[:, ['forward_peak_ang_vel_mean', 'backward_peak_ang_vel_mean']].mean(axis=1)
362
+ df_arm_swing = df_arm_swing.drop(columns=['forward_peak_ang_vel_mean', 'backward_peak_ang_vel_mean'])
363
+
364
+ # Segmenting
365
+
366
+ df_arm_swing = create_segments(
367
+ df=df_arm_swing,
368
+ time_colname='time',
369
+ segment_nr_colname='segment_nr',
370
+ minimum_gap_s=config.segment_gap_s
371
+ )
372
+ df_arm_swing = discard_segments(
373
+ df=df_arm_swing,
374
+ time_colname='time',
375
+ segment_nr_colname='segment_nr',
376
+ minimum_segment_length_s=config.min_segment_length_s
377
+ )
378
+
379
+ # Quantify arm swing
380
+ df_aggregates = aggregate_segments(
381
+ df=df_arm_swing,
382
+ time_colname='time',
383
+ segment_nr_colname='segment_nr',
384
+ window_step_size_s=config.window_step_size,
385
+ l_metrics=['range_of_motion', 'peak_ang_vel'],
386
+ l_aggregates=['median'],
387
+ l_quantiles=[0.95]
388
+ )
389
+
390
+ df_aggregates['segment_duration_ms'] = df_aggregates['segment_duration_s'] * 1000
391
+ df_aggregates = df_aggregates.drop(columns=['segment_nr'])
392
+
393
+ # Store data
394
+ metadata_samples.file_name = 'arm_swing_values.bin'
395
+ metadata_time.file_name = 'arm_swing_time.bin'
396
+
397
+ metadata_samples.channels = ['range_of_motion_median', 'range_of_motion_quantile_95',
398
+ 'peak_ang_vel_median', 'peak_ang_vel_quantile_95']
399
+ metadata_samples.units = ['deg', 'deg', 'deg/s', 'deg/s']
400
+ metadata_samples.data_type = np.float32
401
+ metadata_samples.bits = 32
402
+
403
+ metadata_time.channels = ['time', 'segment_duration_ms']
404
+ metadata_time.units = ['relative_time_ms', 'ms']
405
+ metadata_time.data_type = np.int32
406
+ metadata_time.bits = 32
407
+
408
+ write_data(metadata_time, metadata_samples, output_path, 'arm_swing_meta.json', df_aggregates)
409
+
410
+
411
+ def aggregate_weekly_arm_swing():
412
+ pass
413
+
@@ -0,0 +1,244 @@
1
+ from typing import Dict, List
2
+
3
+ from paradigma.constants import DataColumns
4
+
5
+
6
+ class GaitFeatureExtractionConfig:
7
+
8
+ def __init__(self) -> None:
9
+ self.set_sensor('accelerometer')
10
+ self.set_sampling_frequency(100)
11
+
12
+ self.window_type: str = 'hann'
13
+ self.verbose: int = 0
14
+
15
+ self.window_length_s: int = 6
16
+ self.window_step_size_s: int = 1
17
+
18
+ # cepstral coefficients
19
+ self.cc_low_frequency = 0
20
+ self.cc_high_frequency = 25
21
+ self.n_dct_filters_cc: int = 20
22
+ self.n_coefficients_cc: int = 12
23
+
24
+ self.d_frequency_bandwidths: Dict[str, List[float]] = {
25
+ 'power_below_gait': [0.3, 0.7],
26
+ 'power_gait': [0.7, 3.5],
27
+ 'power_tremor': [3.5, 8],
28
+ 'power_above_tremor': [8, self.sampling_frequency]
29
+ }
30
+
31
+ self.time_colname = 'time'
32
+
33
+ self.l_accelerometer_cols: List[str] = [
34
+ DataColumns.ACCELEROMETER_X,
35
+ DataColumns.ACCELEROMETER_Y,
36
+ DataColumns.ACCELEROMETER_Z
37
+ ]
38
+
39
+ self.l_gravity_cols: List[str] = [f'grav_{x}' for x in self.l_accelerometer_cols]
40
+ self.l_window_level_cols: List[str] = ['id', 'window_nr', 'window_start', 'window_end']
41
+ self.l_data_point_level_cols: List[str] = self.l_accelerometer_cols + self.l_gravity_cols
42
+
43
+ # TODO: generate this dictionary using object attributes (self.X) and parameters (e.g., n_dct_filters for cc)
44
+ self.d_channels_values: Dict[str, str] = {
45
+ f'grav_{self.sensor}_x_mean': 'g',
46
+ f'grav_{self.sensor}_y_mean': 'g',
47
+ f'grav_{self.sensor}_z_mean': 'g',
48
+ f'grav_{self.sensor}_x_std': 'g',
49
+ f'grav_{self.sensor}_y_std': 'g',
50
+ f'grav_{self.sensor}_z_std': 'g',
51
+ f'{self.sensor}_x_power_below_gait': 'g^2/Hz',
52
+ f'{self.sensor}_y_power_below_gait': 'g^2/Hz',
53
+ f'{self.sensor}_z_power_below_gait': 'g^2/Hz',
54
+ f'{self.sensor}_x_power_gait': 'g^2/Hz',
55
+ f'{self.sensor}_y_power_gait': 'g^2/Hz',
56
+ f'{self.sensor}_z_power_gait': 'g^2/Hz',
57
+ f'{self.sensor}_x_power_tremor': 'g^2/Hz',
58
+ f'{self.sensor}_y_power_tremor': 'g^2/Hz',
59
+ f'{self.sensor}_z_power_tremor': 'g^2/Hz',
60
+ f'{self.sensor}_x_power_above_tremor': 'g^2/Hz',
61
+ f'{self.sensor}_y_power_above_tremor': 'g^2/Hz',
62
+ f'{self.sensor}_z_power_above_tremor': 'g^2/Hz',
63
+ f'{self.sensor}_x_dominant_frequency': 'Hz',
64
+ f'{self.sensor}_y_dominant_frequency': 'Hz',
65
+ f'{self.sensor}_z_dominant_frequency': 'Hz',
66
+ f'std_norm_acc': 'g',
67
+ }
68
+
69
+ for cc_coef in range(1, self.n_coefficients_cc+1):
70
+ self.d_channels_values[f'cc_{cc_coef}_{self.sensor}'] = 'g'
71
+
72
+ # TODO: move to higher level config class (duplicate in armswing feature extraction)
73
+ def set_sensor(self, sensor: str) -> None:
74
+ """ Sets the sensor and derived filenames """
75
+ self.sensor: str = sensor
76
+ self.meta_filename: str = f'{self.sensor}_meta.json'
77
+ self.values_filename: str = f'{self.sensor}_samples.bin'
78
+ self.time_filename: str = f'{self.sensor}_time.bin'
79
+
80
+ def set_sampling_frequency(self, sampling_frequency: int) -> None:
81
+ """ Sets the sampling frequency and derived variables """
82
+ self.sampling_frequency: int = sampling_frequency
83
+ self.spectrum_low_frequency: int = 0 # Hz
84
+ self.spectrum_high_frequency: int = int(self.sampling_frequency / 2) # Hz
85
+ self.filter_length: int = self.spectrum_high_frequency - 1
86
+
87
+
88
+ class GaitDetectionConfig:
89
+
90
+ def __init__(self) -> None:
91
+ self.classifier_file_name = 'gd_classifier.pkl'
92
+ self.thresholds_file_name = 'gd_threshold.txt'
93
+
94
+ self.meta_filename = 'gait_meta.json'
95
+ self.time_filename = 'gait_time.bin'
96
+ self.values_filename = 'gait_values.bin'
97
+
98
+ self.l_accel_cols = [DataColumns.ACCELEROMETER_X, DataColumns.ACCELEROMETER_Y, DataColumns.ACCELEROMETER_Z]
99
+
100
+ self.time_colname = 'time'
101
+
102
+
103
+ class ArmSwingFeatureExtractionConfig:
104
+
105
+ def initialize_window_length_fields(self, window_length_s: int) -> None:
106
+ self.window_length_s = window_length_s
107
+ self.window_overlap_s = window_length_s * 0.75
108
+ self.window_step_size_s = window_length_s - self.window_overlap_s
109
+
110
+ def initialize_sampling_frequency_fields(self, sampling_frequency: int) -> None:
111
+ self.sampling_frequency = sampling_frequency
112
+
113
+ # computing power
114
+ self.power_band_low_frequency = 0.3
115
+ self.power_band_high_frequency = 3
116
+ self.spectrum_low_frequency = 0
117
+ self.spectrum_high_frequency = int(sampling_frequency / 2)
118
+
119
+ self.d_frequency_bandwidths = {
120
+ 'power_below_gait': [0.3, 0.7],
121
+ 'power_gait': [0.7, 3.5],
122
+ 'power_tremor': [3.5, 8],
123
+ 'power_above_tremor': [8, sampling_frequency]
124
+ }
125
+
126
+ # cepstral coefficients
127
+ self.cc_low_frequency = 0
128
+ self.cc_high_frequency = 25
129
+ self.n_dct_filters_cc: int = 20
130
+ self.n_coefficients_cc: int = 12
131
+
132
+ def initialize_column_names(self, time_colname='time', pred_gait_colname='pred_gait',
133
+ angle_smooth_colname='angle_smooth', angle_colname='angle',
134
+ velocity_colname='velocity', segment_nr_colname='segment_nr') -> None:
135
+ self.time_colname = time_colname
136
+ self.pred_gait_colname = pred_gait_colname
137
+ self.angle_smooth_colname = angle_smooth_colname
138
+ self.angle_colname = angle_colname
139
+ self.velocity_colname = velocity_colname
140
+ self.segment_nr_colname = segment_nr_colname
141
+
142
+ self.l_accelerometer_cols: List[str] = [
143
+ DataColumns.ACCELEROMETER_X,
144
+ DataColumns.ACCELEROMETER_Y,
145
+ DataColumns.ACCELEROMETER_Z
146
+ ]
147
+
148
+ self.l_gyroscope_cols: List[str] = [
149
+ DataColumns.GYROSCOPE_X,
150
+ DataColumns.GYROSCOPE_Y,
151
+ DataColumns.GYROSCOPE_Z
152
+ ]
153
+
154
+ self.l_gravity_cols: List[str] = [f'grav_{x}' for x in self.l_accelerometer_cols]
155
+
156
+ self.l_data_point_level_cols = self.l_accelerometer_cols + self.l_gyroscope_cols + self.l_gravity_cols + [
157
+ angle_smooth_colname, velocity_colname
158
+ ]
159
+
160
+ def __init__(self) -> None:
161
+ # general
162
+ self.sensor = 'IMU'
163
+ self.units = 'degrees'
164
+
165
+ # windowing
166
+ self.window_type = 'hann'
167
+ self.initialize_window_length_fields(3)
168
+
169
+ self.initialize_sampling_frequency_fields(100)
170
+
171
+ self.initialize_column_names()
172
+
173
+ self.d_channels_values = {
174
+ 'angle_perc_power': 'proportion',
175
+ 'range_of_motion': 'deg',
176
+ 'forward_peak_ang_vel_mean': 'deg/s',
177
+ 'forward_peak_ang_vel_std': 'deg/s',
178
+ 'backward_peak_ang_vel_mean': 'deg/s',
179
+ 'backward_peak_ang_vel_std': 'deg/s',
180
+ 'std_norm_acc': 'g',
181
+ 'grav_accelerometer_x_mean': 'g',
182
+ 'grav_accelerometer_x_std': 'g',
183
+ 'grav_accelerometer_y_mean': 'g',
184
+ 'grav_accelerometer_y_std': 'g',
185
+ 'grav_accelerometer_z_mean': 'g',
186
+ 'grav_accelerometer_z_std': 'g',
187
+ 'accelerometer_x_power_below_gait': 'X',
188
+ 'accelerometer_x_power_gait': 'X',
189
+ 'accelerometer_x_power_tremor': 'X',
190
+ 'accelerometer_x_power_above_tremor': 'X',
191
+ 'accelerometer_x_dominant_frequency': 'Hz',
192
+ 'accelerometer_y_power_below_gait': 'X',
193
+ 'accelerometer_y_power_gait': 'X',
194
+ 'accelerometer_y_power_tremor': 'X',
195
+ 'accelerometer_y_power_above_tremor': 'X',
196
+ 'accelerometer_y_dominant_frequency': 'Hz',
197
+ 'accelerometer_z_power_below_gait': 'X',
198
+ 'accelerometer_z_power_gait': 'X',
199
+ 'accelerometer_z_power_tremor': 'X',
200
+ 'accelerometer_z_power_above_tremor': 'X',
201
+ 'accelerometer_z_dominant_frequency': 'Hz',
202
+ 'angle_dominant_frequency': 'Hz',
203
+ }
204
+
205
+ for sensor in ['accelerometer', 'gyroscope']:
206
+ for cc_coef in range(1, self.n_coefficients_cc+1):
207
+ self.d_channels_values[f'cc_{cc_coef}_{sensor}'] = 'g'
208
+
209
+
210
+ # TODO: move to higher level config class (duplicate in gait feature extraction)
211
+ def set_sensor(self, sensor: str) -> None:
212
+ """ Sets the sensor and derived filenames """
213
+ self.sensor: str = sensor
214
+ self.meta_filename: str = f'{self.sensor}_meta.json'
215
+ self.values_filename: str = f'{self.sensor}_samples.bin'
216
+ self.time_filename: str = f'{self.sensor}_time.bin'
217
+
218
+
219
+ class ArmSwingDetectionConfig:
220
+
221
+ def __init__(self) -> None:
222
+ self.classifier_file_name = 'asd_classifier.pkl'
223
+
224
+ self.meta_filename = 'arm_swing_meta.json'
225
+ self.time_filename = 'arm_swing_time.bin'
226
+ self.values_filename = 'arm_swing_values.bin'
227
+
228
+ self.l_accel_cols = [DataColumns.ACCELEROMETER_X, DataColumns.ACCELEROMETER_Y, DataColumns.ACCELEROMETER_Z]
229
+ self.l_gyro_cols = [DataColumns.GYROSCOPE_X, DataColumns.GYROSCOPE_Y, DataColumns.GYROSCOPE_Z]
230
+
231
+
232
+ class ArmSwingQuantificationConfig:
233
+
234
+ def __init__(self) -> None:
235
+ self.meta_filename = 'arm_swing_meta.json'
236
+ self.time_filename = 'arm_swing_time.bin'
237
+ self.values_filename = 'arm_swing_values.bin'
238
+
239
+ self.pred_arm_swing_colname = 'pred_arm_swing'
240
+
241
+ self.window_length_s = 3
242
+ self.window_step_size = 0.75
243
+ self.segment_gap_s = 3
244
+ self.min_segment_length_s = 3