pyForceDAQ 2.0.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.
- pyforcedaq/__init__.py +43 -0
- pyforcedaq/__main__.py +42 -0
- pyforcedaq/_lib/__init__.py +1 -0
- pyforcedaq/_lib/lsl.py +56 -0
- pyforcedaq/_lib/misc.py +126 -0
- pyforcedaq/_lib/polling_time_profile.py +52 -0
- pyforcedaq/_lib/process_priority_manager.py +148 -0
- pyforcedaq/_lib/timer.py +45 -0
- pyforcedaq/_lib/types.py +400 -0
- pyforcedaq/_lib/udp_connection.py +326 -0
- pyforcedaq/daq/__init__.py +19 -0
- pyforcedaq/daq/_daq_read_Analog_pydaqmx.py +114 -0
- pyforcedaq/daq/_daq_read_analog_nidaqmx.py +84 -0
- pyforcedaq/daq/_mock_sensor.py +80 -0
- pyforcedaq/daq/_pyATIDAQ.py +306 -0
- pyforcedaq/daq/config.py +13 -0
- pyforcedaq/extras/__init__.py +0 -0
- pyforcedaq/extras/convert.py +275 -0
- pyforcedaq/extras/expyriment_daq_control.py +246 -0
- pyforcedaq/extras/opensesame_daq_control.py +280 -0
- pyforcedaq/extras/read_force_data.py +89 -0
- pyforcedaq/extras/remote_control.py +93 -0
- pyforcedaq/force/__init__.py +13 -0
- pyforcedaq/force/_log.py +18 -0
- pyforcedaq/force/data_recorder.py +400 -0
- pyforcedaq/force/sensor.py +200 -0
- pyforcedaq/force/sensor_process.py +251 -0
- pyforcedaq/gui/__init__.py +6 -0
- pyforcedaq/gui/_gui_status.py +306 -0
- pyforcedaq/gui/_layout.py +104 -0
- pyforcedaq/gui/_level_indicator.py +59 -0
- pyforcedaq/gui/_pg_surface.py +100 -0
- pyforcedaq/gui/_plotter.py +234 -0
- pyforcedaq/gui/_run.py +522 -0
- pyforcedaq/gui/_scaling.py +71 -0
- pyforcedaq/gui/_settings.py +98 -0
- pyforcedaq/gui/forceDAQ_logo.png +0 -0
- pyforcedaq/gui/launcher.py +249 -0
- pyforcedaq-2.0.0.dist-info/METADATA +15 -0
- pyforcedaq-2.0.0.dist-info/RECORD +42 -0
- pyforcedaq-2.0.0.dist-info/WHEEL +4 -0
- pyforcedaq-2.0.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""pyATIDAQ: Python wrapper for atidaq c library
|
|
2
|
+
|
|
3
|
+
Notes
|
|
4
|
+
-----
|
|
5
|
+
see ftconfig.h & ftsharedrt.h for details
|
|
6
|
+
|
|
7
|
+
See COPYING file distributed along with the pyForceDAQ copyright and license terms.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__author__ = "Oliver Lindemann"
|
|
11
|
+
|
|
12
|
+
from ctypes import *
|
|
13
|
+
from sys import platform
|
|
14
|
+
|
|
15
|
+
from .._lib.misc import find_calibration_file
|
|
16
|
+
|
|
17
|
+
# ### DATA TYPES ####
|
|
18
|
+
VOLTAGE_SAMPLE_TYPE = c_float * 7
|
|
19
|
+
FT_SAMPLE_TYPE = c_float * 6
|
|
20
|
+
DISPLACEMENT_VECTOR = c_float * 8
|
|
21
|
+
UNITS = c_char_p
|
|
22
|
+
MAX_AXES = 6
|
|
23
|
+
MAX_GAUGES = 8
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# calibration information required for F/T conversions
|
|
27
|
+
class RTCoefs(Structure):
|
|
28
|
+
_fields_ = [
|
|
29
|
+
('NumChannels', c_ushort),
|
|
30
|
+
('NumAxes', c_ushort),
|
|
31
|
+
('working_matrix', (c_float * MAX_AXES) * MAX_GAUGES),
|
|
32
|
+
('bias_slopes', c_float * MAX_GAUGES),
|
|
33
|
+
('gain_slopes', c_float * MAX_GAUGES),
|
|
34
|
+
('thermistor', c_float),
|
|
35
|
+
('bias_vector', c_float * (MAX_GAUGES + 1)),
|
|
36
|
+
('TCbias_vector', c_float * MAX_GAUGES)]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Transform(Structure):
|
|
40
|
+
_fields_ = [
|
|
41
|
+
('TT', c_float * 6), # displacement/rotation vector dx, dy, dz, rx, ry, r
|
|
42
|
+
('DistUnits', UNITS), # units of dx, dy, dz
|
|
43
|
+
('AngleUnits', UNITS)] # units of rx, ry, rz
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Configuration(Structure):
|
|
47
|
+
_fields_ = [
|
|
48
|
+
('ForceUnits', UNITS), # force units of output
|
|
49
|
+
('TorqueUnits', UNITS), # torque units of output
|
|
50
|
+
('UserTransform', Transform), # coordinate system transform set by user
|
|
51
|
+
('TempCompEnabled', c_bool)] # is temperature compensation enabled?
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Calibration(Structure):
|
|
55
|
+
_fields_ = [
|
|
56
|
+
('BasicMatrix', (c_float * MAX_AXES) * MAX_GAUGES), # non-usable matrix; use rt.working_matrix for calculations
|
|
57
|
+
('ForceUnits', UNITS), # force units of basic matrix, as read from file; constant
|
|
58
|
+
('TorqueUnits', UNITS), # torque units of basic matrix, as read from file; constant
|
|
59
|
+
('TempCompAvailable', c_bool), # does this calibration have optional temperature compensation?
|
|
60
|
+
('BasicTransform', Transform), # built-in coordinate transform; for internal use
|
|
61
|
+
('MaxLoads', c_float * MAX_AXES), # maximum loads of each axis, in units above
|
|
62
|
+
('AxisNames', c_char_p * MAX_AXES), # names of each axis
|
|
63
|
+
('Serial', c_char_p), # serial number of transducer (such as "FT4566")
|
|
64
|
+
('BodyStyle', c_char_p), # transducer's body style (such as "Delta")
|
|
65
|
+
('PartNumber', c_char_p), # calibration part number (such as "US-600-3600")
|
|
66
|
+
('Family', c_char_p), # family of transducer (typ. "DAQ")
|
|
67
|
+
('CalDate', c_char_p), # date of calibration
|
|
68
|
+
('cfg', Configuration), # struct containing configurable parameters
|
|
69
|
+
('rt', RTCoefs)] # struct containing coefficients used in realtime calculations
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
calibration_p = POINTER(Calibration)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ATI_CDLL(object):
|
|
76
|
+
|
|
77
|
+
def __init__(self):
|
|
78
|
+
|
|
79
|
+
if platform.startswith('linux'):
|
|
80
|
+
lib_path = "/usr/lib/atidaq.so"
|
|
81
|
+
elif platform == 'win32':
|
|
82
|
+
lib_path = "C:\\Windows\\System\\atidaq.dll"
|
|
83
|
+
else:
|
|
84
|
+
raise RuntimeError("Your plattform is not supported")
|
|
85
|
+
|
|
86
|
+
self.cdll = CDLL(lib_path)
|
|
87
|
+
|
|
88
|
+
self.cdll.createCalibration.argtype = [c_char_p, c_ushort]
|
|
89
|
+
self.cdll.createCalibration.restype = calibration_p
|
|
90
|
+
|
|
91
|
+
self.cdll.destroyCalibration.argtype = [calibration_p]
|
|
92
|
+
self.cdll.destroyCalibration.restype = None
|
|
93
|
+
|
|
94
|
+
self.cdll.SetToolTransform.argtype = [calibration_p, DISPLACEMENT_VECTOR,
|
|
95
|
+
c_char_p, c_char_p]
|
|
96
|
+
self.cdll.SetToolTransform.restype = c_short
|
|
97
|
+
|
|
98
|
+
self.cdll.SetForceUnits.argtype = [calibration_p, c_char_p]
|
|
99
|
+
self.cdll.SetForceUnits.restype = c_short
|
|
100
|
+
|
|
101
|
+
self.cdll.SetTorqueUnits.argtype = [calibration_p, c_char_p]
|
|
102
|
+
self.cdll.SetTorqueUnits.restype = c_short
|
|
103
|
+
|
|
104
|
+
self.cdll.SetTempComp.argtype = [calibration_p, c_char_p]
|
|
105
|
+
self.cdll.SetTempComp.restype = c_short
|
|
106
|
+
|
|
107
|
+
self.cdll.Bias.argtype = [calibration_p, POINTER(c_float)]
|
|
108
|
+
self.cdll.Bias.restype = None
|
|
109
|
+
|
|
110
|
+
self.cdll.ConvertToFT.argtype = [calibration_p, POINTER(c_float),
|
|
111
|
+
POINTER(c_float)]
|
|
112
|
+
self.cdll.ConvertToFT.restype = None
|
|
113
|
+
|
|
114
|
+
self.cdll.printCalInfo.argtype = [calibration_p]
|
|
115
|
+
self.cdll.printCalInfo.restype = None
|
|
116
|
+
|
|
117
|
+
self._calibration = None
|
|
118
|
+
|
|
119
|
+
def __del__(self):
|
|
120
|
+
self.destroyCalibration()
|
|
121
|
+
|
|
122
|
+
def calibration(self):
|
|
123
|
+
"""The current calibration
|
|
124
|
+
Returns
|
|
125
|
+
cal: POINTER(Calibration), calibration pointer
|
|
126
|
+
initialized Calibration struct
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
return self._calibration
|
|
130
|
+
|
|
131
|
+
def createCalibration(self, CalFilePath, index):
|
|
132
|
+
""" Loads calibration info for a transducer into a new Calibration struct
|
|
133
|
+
Parameters:
|
|
134
|
+
CalFilePath: c_char_p
|
|
135
|
+
the name and path of the calibration file
|
|
136
|
+
index: c_ushort
|
|
137
|
+
the number of the calibration within the file (usually 1)
|
|
138
|
+
NULL: Could not load the desired calibration.
|
|
139
|
+
Notes: For each Calibration object initialized by this function,
|
|
140
|
+
destroyCalibration must be called for cleanup.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
self._calibration = self.cdll.createCalibration(CalFilePath.encode(), index)
|
|
144
|
+
if self._calibration == 0:
|
|
145
|
+
raise RuntimeError("Specified calibration could not be loaded.")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def destroyCalibration(self):
|
|
149
|
+
"""Frees memory allocated for Calibration struct by a successful
|
|
150
|
+
call to createCalibration. Must be called when Calibration
|
|
151
|
+
struct is no longer needed.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
return self.cdll.destroyCalibration(self._calibration)
|
|
155
|
+
|
|
156
|
+
def setToolTransform(self, Vector, DistUnits, AngleUnits):
|
|
157
|
+
"""Performs a 6-axis translation/rotation on the transducer's coordinate system.
|
|
158
|
+
Parameters:
|
|
159
|
+
Vector: array of float
|
|
160
|
+
displacements and rotations in the order Dx, Dy, Dz, Rx, Ry, Rz
|
|
161
|
+
DistUnits: c_char_p
|
|
162
|
+
units of Dx, Dy, Dz
|
|
163
|
+
AngleUnits: c_char_p
|
|
164
|
+
units of Rx, Ry, Rz
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
error = self.cdll.SetToolTransform(self._calibration, DISPLACEMENT_VECTOR(*Vector),
|
|
168
|
+
DistUnits.encode(),
|
|
169
|
+
AngleUnits.encode())
|
|
170
|
+
if error:
|
|
171
|
+
if error == 1:
|
|
172
|
+
raise RuntimeError("Invalid Calibration struct.")
|
|
173
|
+
elif error == 2:
|
|
174
|
+
raise RuntimeError("Invalid distance units.")
|
|
175
|
+
elif error == 3:
|
|
176
|
+
raise RuntimeError("Invalid angle units.")
|
|
177
|
+
else:
|
|
178
|
+
raise RuntimeError("Unknown error.")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def setForceUnits(self, NewUnits):
|
|
183
|
+
"""Sets the units of force output
|
|
184
|
+
Parameters:
|
|
185
|
+
NewUnits: c_char_p
|
|
186
|
+
units for force output
|
|
187
|
+
("lb","klb","N","kN","g","kg")
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
error = self.cdll.SetForceUnits(self._calibration,
|
|
191
|
+
NewUnits.encode())
|
|
192
|
+
if error:
|
|
193
|
+
if error == 1:
|
|
194
|
+
raise RuntimeError("Invalid Calibration struct.")
|
|
195
|
+
elif error == 2:
|
|
196
|
+
raise RuntimeError("Invalid force units.")
|
|
197
|
+
else:
|
|
198
|
+
raise RuntimeError("Unknown error.")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def setTorqueUnits(self, NewUnits):
|
|
202
|
+
"""Sets the units of torque output
|
|
203
|
+
Parameters:
|
|
204
|
+
NewUnits: c_char_p
|
|
205
|
+
units for torque output
|
|
206
|
+
("in-lb","ft-lb","N-m","N-mm","kg-cm")
|
|
207
|
+
"""
|
|
208
|
+
error = self.cdll.SetTorqueUnits(self._calibration,
|
|
209
|
+
NewUnits.encode())
|
|
210
|
+
if error:
|
|
211
|
+
if error == 1:
|
|
212
|
+
raise RuntimeError("Invalid Calibration struct.")
|
|
213
|
+
elif error == 2:
|
|
214
|
+
raise RuntimeError("Invalid torque units.")
|
|
215
|
+
else:
|
|
216
|
+
raise RuntimeError("Unknown error.")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def setTempComp(self, TCEnabled):
|
|
220
|
+
"""Enables or disables temperature compensation, if available
|
|
221
|
+
Parameters:
|
|
222
|
+
TCEnabled: c_char_p
|
|
223
|
+
0 = temperature compensation off
|
|
224
|
+
1 = temperature compensation on
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
error = self.cdll.SetTempComp(self._calibration,
|
|
228
|
+
TCEnabled.encode())
|
|
229
|
+
if error:
|
|
230
|
+
if error == 1:
|
|
231
|
+
raise RuntimeError("Invalid Calibration struct.")
|
|
232
|
+
elif error == 2:
|
|
233
|
+
raise RuntimeError("Not available on this transducer system")
|
|
234
|
+
else:
|
|
235
|
+
raise RuntimeError("Unknown error.")
|
|
236
|
+
|
|
237
|
+
def bias(self, voltages):
|
|
238
|
+
"""Stores a voltage reading to be subtracted from subsequent readings,
|
|
239
|
+
effectively "zeroing" the transducer output to remove tooling weight, etc.
|
|
240
|
+
Parameters:
|
|
241
|
+
voltages: array of float
|
|
242
|
+
array of voltages acquired by DAQ system
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
return self.cdll.Bias(self._calibration, VOLTAGE_SAMPLE_TYPE(*voltages))
|
|
246
|
+
|
|
247
|
+
def convertToFT(self, voltages, reverse_parameters=[]):
|
|
248
|
+
"""Converts an array of voltages into forces and torques and
|
|
249
|
+
returns them in result
|
|
250
|
+
Parameters:
|
|
251
|
+
voltages: array of float
|
|
252
|
+
array of voltages acquired by DAQ system
|
|
253
|
+
reverse_parameters: array of integer
|
|
254
|
+
list of ids of parameter that should be reversed due to problems calibration with
|
|
255
|
+
the calibration
|
|
256
|
+
Returns:
|
|
257
|
+
array of force-torque values (typ. 6 elements)
|
|
258
|
+
"""
|
|
259
|
+
ft_array = FT_SAMPLE_TYPE()
|
|
260
|
+
self.cdll.ConvertToFT(self._calibration, VOLTAGE_SAMPLE_TYPE(*voltages),
|
|
261
|
+
byref(ft_array))
|
|
262
|
+
rtn = list(map(lambda x: x, ft_array)) # convert ctype array to python
|
|
263
|
+
# array
|
|
264
|
+
for x in reverse_parameters:
|
|
265
|
+
rtn[x] = -1*rtn[x]
|
|
266
|
+
return rtn
|
|
267
|
+
|
|
268
|
+
def printCalInfo(self):
|
|
269
|
+
"""print Calibration info on the console
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
return self.cdll.printCalInfo(self._calibration)
|
|
273
|
+
|
|
274
|
+
def print_calibration_info(calibration_file):
|
|
275
|
+
"""convenient function to print calibration file infos"""
|
|
276
|
+
atidaq = ATI_CDLL()
|
|
277
|
+
index = c_short(1)
|
|
278
|
+
calibration = atidaq.createCalibration(calibration_file, index)
|
|
279
|
+
atidaq.printCalInfo(calibration)
|
|
280
|
+
atidaq.destroyCalibration(calibration)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
# test module
|
|
285
|
+
|
|
286
|
+
# FT_sensor1.cal
|
|
287
|
+
# Bias reading:
|
|
288
|
+
# 0.265100 -0.017700 -0.038400 -0.042700 -0.189100 0.137300 -3.242300
|
|
289
|
+
# Measurement:
|
|
290
|
+
# -3.286300 0.387500 -3.487700 0.404300 -3.934100 0.547400 -3.210600
|
|
291
|
+
# Result:
|
|
292
|
+
# -0.065867 0.123803 111.156731 0.039974 0.040417 0.079049
|
|
293
|
+
|
|
294
|
+
#filename = raw_input("Calibration file: ")
|
|
295
|
+
filename = find_calibration_file("calibration", "FT30436")
|
|
296
|
+
atidaq = ATI_CDLL()
|
|
297
|
+
# get calibration
|
|
298
|
+
index = c_short(1)
|
|
299
|
+
atidaq.createCalibration(filename, index)
|
|
300
|
+
atidaq.setForceUnits("N")
|
|
301
|
+
atidaq.setTorqueUnits("N-m")
|
|
302
|
+
|
|
303
|
+
atidaq.bias([0.2651, -0.0177, -0.0384, -0.0427, -0.1891, 0.1373, -3.2423])
|
|
304
|
+
atidaq.printCalInfo()
|
|
305
|
+
print("\nConverting")
|
|
306
|
+
print(atidaq.convertToFT([-3.2863, 0.3875, -3.4877, 0.4043, -3.9341, 0.5474, -3.2106]))
|
pyforcedaq/daq/config.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class DAQConfiguration(object):
|
|
2
|
+
"""Settings required for NI-DAQ"""
|
|
3
|
+
def __init__(self, device_name: str, channels: str = "ai0:7",
|
|
4
|
+
rate: int = 1000, minVal: float = -10, maxVal: float = 10):
|
|
5
|
+
self.device_name = device_name
|
|
6
|
+
self.channels = channels
|
|
7
|
+
self.rate = rate
|
|
8
|
+
self.minVal = minVal
|
|
9
|
+
self.maxVal = maxVal
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def physicalChannel(self):
|
|
13
|
+
return "{0}/{1}".format(self.device_name, self.channels)
|
|
File without changes
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Functions to convert force data
|
|
5
|
+
|
|
6
|
+
This module can be also executed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__author__ = 'Oliver Lindemann'
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import gzip
|
|
14
|
+
import numpy as np
|
|
15
|
+
from .read_force_data import read_raw_data, data_frame_to_text
|
|
16
|
+
|
|
17
|
+
PAUSE_CRITERION = 500
|
|
18
|
+
MSEC_PER_SAMPLES = 1
|
|
19
|
+
REF_SAMPLE_PROBE = 1000
|
|
20
|
+
MIN_DELAY_ENDSTREAM = 2
|
|
21
|
+
CONVERTED_SUFFIX = ".conv.csv.gz"
|
|
22
|
+
CONVERTED_SUBFOLDER = "converted"
|
|
23
|
+
|
|
24
|
+
def _periods_from_daq_events(daq_events):
|
|
25
|
+
|
|
26
|
+
periods = {}
|
|
27
|
+
started = None
|
|
28
|
+
sensor_id = None
|
|
29
|
+
evt = np.array(daq_events["value"])
|
|
30
|
+
times = np.array(daq_events["time"]).astype(int)
|
|
31
|
+
idx = np.argsort(times)
|
|
32
|
+
|
|
33
|
+
for t, v in zip(times[idx], evt[idx]):
|
|
34
|
+
try:
|
|
35
|
+
sensor_id = int(v.split(":")[1])
|
|
36
|
+
except:
|
|
37
|
+
sensor_id = None
|
|
38
|
+
|
|
39
|
+
if sensor_id not in periods:
|
|
40
|
+
periods[sensor_id] = []
|
|
41
|
+
|
|
42
|
+
if v.startswith("started"):
|
|
43
|
+
if started is None:
|
|
44
|
+
started = t
|
|
45
|
+
else:
|
|
46
|
+
periods[sensor_id].append((started, None))
|
|
47
|
+
started = None
|
|
48
|
+
elif v.startswith("pause"):
|
|
49
|
+
periods[sensor_id].append((started, t))
|
|
50
|
+
started = None
|
|
51
|
+
|
|
52
|
+
# sort remaining
|
|
53
|
+
if started is not None:
|
|
54
|
+
periods[sensor_id].append((started, None))
|
|
55
|
+
|
|
56
|
+
return periods
|
|
57
|
+
|
|
58
|
+
def _pauses_idx_from_timeline(time, pause_criterion):
|
|
59
|
+
pauses_idx = np.where(np.diff(time) > pause_criterion)[0]
|
|
60
|
+
last_pause = -1
|
|
61
|
+
rtn = []
|
|
62
|
+
for idx in np.append(pauses_idx, len(time)-1):
|
|
63
|
+
rtn.append((last_pause+1, idx))
|
|
64
|
+
last_pause = idx
|
|
65
|
+
return rtn
|
|
66
|
+
|
|
67
|
+
def _most_frequent_value(values):
|
|
68
|
+
(v, cnt) = np.unique(values, return_counts=True)
|
|
69
|
+
idx = np.argmax(cnt)
|
|
70
|
+
return v[idx]
|
|
71
|
+
|
|
72
|
+
def print_histogram(values):
|
|
73
|
+
(v, cnt) = np.unique(values, return_counts=True)
|
|
74
|
+
for a,b in zip(v,cnt):
|
|
75
|
+
print("{} -- {}".format(a,b))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _end_stream_sample(timestamps, min_delay=MIN_DELAY_ENDSTREAM):
|
|
79
|
+
"""finds end of the data stream, that is, sample before next long waiting
|
|
80
|
+
sample or returns None if no end can be detected"""
|
|
81
|
+
|
|
82
|
+
next_t_diffs = np.diff(timestamps)
|
|
83
|
+
try:
|
|
84
|
+
return np.where(next_t_diffs >= min_delay)[0][0] #+1-1
|
|
85
|
+
except:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _linear_timeline_matched_by_single_reference_sample(irregular_timeline,
|
|
90
|
+
id_ref_sample, msec_per_sample):
|
|
91
|
+
"""match timeline that differences between the two is minimal
|
|
92
|
+
new times can not be after irregular times
|
|
93
|
+
"""
|
|
94
|
+
t_ref = irregular_timeline[id_ref_sample]
|
|
95
|
+
t_first = t_ref - (id_ref_sample*msec_per_sample)
|
|
96
|
+
t_last = t_first + ((len(irregular_timeline) - 1) * msec_per_sample)
|
|
97
|
+
return np.arange(t_first, t_last + msec_per_sample, step=msec_per_sample)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _timeline_matched_by_delay_chunked_samples(times, msec_per_sample):
|
|
101
|
+
|
|
102
|
+
rtn = np.empty(len(times))*np.NaN
|
|
103
|
+
p = 0
|
|
104
|
+
while p<len(times):
|
|
105
|
+
next_ref_sample = _end_stream_sample(times[p:])
|
|
106
|
+
if next_ref_sample is not None:
|
|
107
|
+
ref_time = times[p+next_ref_sample]
|
|
108
|
+
rtn[p:(p+next_ref_sample+1)] = np.arange(
|
|
109
|
+
start = ref_time - (next_ref_sample*msec_per_sample),
|
|
110
|
+
stop = ref_time + msec_per_sample,
|
|
111
|
+
step = msec_per_sample)
|
|
112
|
+
p = p + next_ref_sample + 1
|
|
113
|
+
|
|
114
|
+
else:
|
|
115
|
+
# no further refence samples
|
|
116
|
+
rtn[p:] = times[p:]
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
return rtn
|
|
120
|
+
|
|
121
|
+
class Method(object):
|
|
122
|
+
|
|
123
|
+
types = {1: "single reference sample (forced linearity)",
|
|
124
|
+
2: "multiple delayed chunked samples (no linearity assumed)"}
|
|
125
|
+
|
|
126
|
+
def __init__(self, id):
|
|
127
|
+
if id not in Method.types:
|
|
128
|
+
raise RuntimeError("Unkown resampling method")
|
|
129
|
+
self.id = id
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def description(self):
|
|
133
|
+
return Method.types[self.id]
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def get_method_from_description(description):
|
|
137
|
+
for id, desc in Method.types.items():
|
|
138
|
+
if desc == description:
|
|
139
|
+
return Method(id)
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _adjusted_timestamps(timestamps, pauses_idx, evt_periods, method):
|
|
144
|
+
"""
|
|
145
|
+
method=Method(1): _linear_timeline_matched_by_single_reference_sample
|
|
146
|
+
method=Method(2): _timeline_matched_by_delay_chunked_samples
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
# adapting timestamps
|
|
150
|
+
rtn = np.empty(len(timestamps))*np.NaN
|
|
151
|
+
period_counter = 0
|
|
152
|
+
for idx, evt_per in zip(pauses_idx, evt_periods):
|
|
153
|
+
# loop over periods
|
|
154
|
+
|
|
155
|
+
# logging
|
|
156
|
+
period_counter += 1
|
|
157
|
+
n_samples = idx[1] - idx[0] + 1
|
|
158
|
+
if evt_per[1]: # end time
|
|
159
|
+
sample_diff = n_samples - (1+(evt_per[1]-evt_per[0])//MSEC_PER_SAMPLES)
|
|
160
|
+
if sample_diff!=0:
|
|
161
|
+
print("Period {}: Sample difference of {}".format(
|
|
162
|
+
period_counter, sample_diff))
|
|
163
|
+
else:
|
|
164
|
+
print("Period {}: No pause sampling time.".format(period_counter))
|
|
165
|
+
|
|
166
|
+
#convert times
|
|
167
|
+
times = timestamps[idx[0]:idx[1] + 1]
|
|
168
|
+
if method.id==1:
|
|
169
|
+
# match refe samples
|
|
170
|
+
next_ref = _end_stream_sample(times[REF_SAMPLE_PROBE:(REF_SAMPLE_PROBE + 1000)])
|
|
171
|
+
if next_ref is None:
|
|
172
|
+
next_ref = 0
|
|
173
|
+
newtimes = _linear_timeline_matched_by_single_reference_sample(
|
|
174
|
+
times, id_ref_sample=REF_SAMPLE_PROBE + next_ref,
|
|
175
|
+
msec_per_sample=MSEC_PER_SAMPLES)
|
|
176
|
+
elif method.id==2:
|
|
177
|
+
# using delays
|
|
178
|
+
newtimes = _timeline_matched_by_delay_chunked_samples(times,
|
|
179
|
+
msec_per_sample=MSEC_PER_SAMPLES)
|
|
180
|
+
else:
|
|
181
|
+
newtimes = times
|
|
182
|
+
|
|
183
|
+
rtn[idx[0]:idx[1] + 1] = newtimes
|
|
184
|
+
|
|
185
|
+
return rtn.astype(int)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def converted_filename(flname):
|
|
189
|
+
"""returns path and filename of the converted data file"""
|
|
190
|
+
if flname.endswith(".gz"):
|
|
191
|
+
tmp = flname[:-7]
|
|
192
|
+
else:
|
|
193
|
+
tmp = flname[:-4]
|
|
194
|
+
|
|
195
|
+
path, new_filename = os.path.split(tmp)
|
|
196
|
+
converted_path = os.path.join(path, CONVERTED_SUBFOLDER)
|
|
197
|
+
return converted_path, new_filename + CONVERTED_SUFFIX
|
|
198
|
+
|
|
199
|
+
def convert_raw_data(filepath, method, save_time_adjustments=False,
|
|
200
|
+
keep_delay_variable=False):
|
|
201
|
+
"""preprocessing raw pyForceData:
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
# todo only one sensor
|
|
205
|
+
assert(isinstance(method, Method))
|
|
206
|
+
|
|
207
|
+
filepath = os.path.join(os.path.split(sys.argv[0])[0], filepath)
|
|
208
|
+
print("Converting {}".format(filepath))
|
|
209
|
+
print("Method: {}".format(method.description))
|
|
210
|
+
|
|
211
|
+
data, udp_event, daq_events, comments = read_raw_data(filepath)
|
|
212
|
+
print("{} samples".format(len(data["time"])))
|
|
213
|
+
|
|
214
|
+
sensor_id = 1
|
|
215
|
+
if not keep_delay_variable:
|
|
216
|
+
data.pop("delay", None)
|
|
217
|
+
|
|
218
|
+
timestamps = np.array(data["time"]).astype(int)
|
|
219
|
+
|
|
220
|
+
#pauses
|
|
221
|
+
pauses_idx = _pauses_idx_from_timeline(timestamps, pause_criterion=PAUSE_CRITERION)
|
|
222
|
+
evt_periods = _periods_from_daq_events(daq_events)
|
|
223
|
+
|
|
224
|
+
if len(pauses_idx) != len(evt_periods[sensor_id]):
|
|
225
|
+
raise RuntimeError("Pauses in DAQ events do not match recording pauses")
|
|
226
|
+
else:
|
|
227
|
+
data["time"] = _adjusted_timestamps(timestamps=timestamps,
|
|
228
|
+
pauses_idx=pauses_idx,
|
|
229
|
+
evt_periods=evt_periods[
|
|
230
|
+
sensor_id],
|
|
231
|
+
method=method)
|
|
232
|
+
|
|
233
|
+
if save_time_adjustments:
|
|
234
|
+
data["time_adjustment"] = timestamps-data["time"]
|
|
235
|
+
#print("Time difference historgram")
|
|
236
|
+
#print_histogram(data["time_adjustment"])
|
|
237
|
+
|
|
238
|
+
#save
|
|
239
|
+
folder, new_filename = converted_filename(filepath)
|
|
240
|
+
try:
|
|
241
|
+
os.makedirs(folder)
|
|
242
|
+
except:
|
|
243
|
+
pass
|
|
244
|
+
new_filename = os.path.join(folder, new_filename)
|
|
245
|
+
|
|
246
|
+
with gzip.open(new_filename, "wt") as fl:
|
|
247
|
+
fl.write(comments.strip() + "\n")
|
|
248
|
+
fl.write(data_frame_to_text(data))
|
|
249
|
+
|
|
250
|
+
def get_all_data_files(folder):
|
|
251
|
+
rtn = []
|
|
252
|
+
for flname in os.listdir(folder):
|
|
253
|
+
if (flname.endswith(".csv") or flname.endswith(".csv.gz")) and not \
|
|
254
|
+
flname.endswith(CONVERTED_SUFFIX):
|
|
255
|
+
flname = os.path.join(folder, flname)
|
|
256
|
+
rtn.append(flname)
|
|
257
|
+
return rtn
|
|
258
|
+
|
|
259
|
+
def get_all_unconverted_data_files(folder):
|
|
260
|
+
rtn = []
|
|
261
|
+
files = get_all_data_files(folder)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
try: # make subfolder
|
|
265
|
+
c_path, _ = converted_filename(files[0])
|
|
266
|
+
converted_files = os.listdir(c_path)
|
|
267
|
+
except:
|
|
268
|
+
converted_files = []
|
|
269
|
+
|
|
270
|
+
for flname in files:
|
|
271
|
+
_, c_flname = converted_filename(flname)
|
|
272
|
+
if c_flname not in converted_files:
|
|
273
|
+
rtn.append(flname)
|
|
274
|
+
return rtn
|
|
275
|
+
|