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.
Files changed (42) hide show
  1. pyforcedaq/__init__.py +43 -0
  2. pyforcedaq/__main__.py +42 -0
  3. pyforcedaq/_lib/__init__.py +1 -0
  4. pyforcedaq/_lib/lsl.py +56 -0
  5. pyforcedaq/_lib/misc.py +126 -0
  6. pyforcedaq/_lib/polling_time_profile.py +52 -0
  7. pyforcedaq/_lib/process_priority_manager.py +148 -0
  8. pyforcedaq/_lib/timer.py +45 -0
  9. pyforcedaq/_lib/types.py +400 -0
  10. pyforcedaq/_lib/udp_connection.py +326 -0
  11. pyforcedaq/daq/__init__.py +19 -0
  12. pyforcedaq/daq/_daq_read_Analog_pydaqmx.py +114 -0
  13. pyforcedaq/daq/_daq_read_analog_nidaqmx.py +84 -0
  14. pyforcedaq/daq/_mock_sensor.py +80 -0
  15. pyforcedaq/daq/_pyATIDAQ.py +306 -0
  16. pyforcedaq/daq/config.py +13 -0
  17. pyforcedaq/extras/__init__.py +0 -0
  18. pyforcedaq/extras/convert.py +275 -0
  19. pyforcedaq/extras/expyriment_daq_control.py +246 -0
  20. pyforcedaq/extras/opensesame_daq_control.py +280 -0
  21. pyforcedaq/extras/read_force_data.py +89 -0
  22. pyforcedaq/extras/remote_control.py +93 -0
  23. pyforcedaq/force/__init__.py +13 -0
  24. pyforcedaq/force/_log.py +18 -0
  25. pyforcedaq/force/data_recorder.py +400 -0
  26. pyforcedaq/force/sensor.py +200 -0
  27. pyforcedaq/force/sensor_process.py +251 -0
  28. pyforcedaq/gui/__init__.py +6 -0
  29. pyforcedaq/gui/_gui_status.py +306 -0
  30. pyforcedaq/gui/_layout.py +104 -0
  31. pyforcedaq/gui/_level_indicator.py +59 -0
  32. pyforcedaq/gui/_pg_surface.py +100 -0
  33. pyforcedaq/gui/_plotter.py +234 -0
  34. pyforcedaq/gui/_run.py +522 -0
  35. pyforcedaq/gui/_scaling.py +71 -0
  36. pyforcedaq/gui/_settings.py +98 -0
  37. pyforcedaq/gui/forceDAQ_logo.png +0 -0
  38. pyforcedaq/gui/launcher.py +249 -0
  39. pyforcedaq-2.0.0.dist-info/METADATA +15 -0
  40. pyforcedaq-2.0.0.dist-info/RECORD +42 -0
  41. pyforcedaq-2.0.0.dist-info/WHEEL +4 -0
  42. pyforcedaq-2.0.0.dist-info/entry_points.txt +3 -0
pyforcedaq/__init__.py ADDED
@@ -0,0 +1,43 @@
1
+ """DAQ tool to record response force data
2
+
3
+ Oliver Lindemann, 2026
4
+
5
+
6
+ launch the GUI force from your Python program:
7
+ ``
8
+ from pyforcedaq import gui
9
+
10
+ gui.run_with_options(remote_control=False,
11
+ ask_filename=True,
12
+ calibration_file="FT_sensor1.cal")
13
+ ``
14
+
15
+
16
+ import relevant stuff to program your own force:
17
+ ``
18
+ from pyforcedaq import force
19
+ ``
20
+
21
+ import relevant stuff for remote control of the GUI force:
22
+ ``
23
+ from pyforcedaq import remote_control
24
+ ``
25
+
26
+ For function to support data handling see the folder pyForceDAQ/analysis
27
+
28
+ Oliver Lindemann
29
+ """
30
+
31
+ __version__ = "2.0.beta"
32
+ __author__ = "Oliver Lindemann"
33
+
34
+ import sys as _sys
35
+
36
+ USE_MOCK_SENSOR = False # <-- change for usage in lab to False
37
+
38
+ if _sys.version_info[0] != 3 or _sys.version_info[1]<12:
39
+ raise RuntimeError("pyForceDAQ {0} ".format(__version__) +
40
+ "is not compatible with Python {0}.{1}. ".format(
41
+ _sys.version_info[0],
42
+ _sys.version_info[1]) +
43
+ "Please use Python 3.12+.")
pyforcedaq/__main__.py ADDED
@@ -0,0 +1,42 @@
1
+
2
+ import argparse
3
+
4
+ from . import __author__, __version__, gui
5
+
6
+
7
+ def cli():
8
+
9
+ parser = argparse.ArgumentParser(
10
+ prog="forcedaq",
11
+ description="Command-line interface for pyforceDAQ",
12
+ epilog=f"Author: {__author__}",
13
+ )
14
+ parser.add_argument(
15
+ "--version", action="version", version=f"%(prog)s {__version__}"
16
+ )
17
+
18
+ parser.add_argument("SETTINGS_FILE", nargs="?", default="", help="settings file")
19
+
20
+ parser.add_argument(
21
+ "-l", "--launcher",
22
+ action="store_true",
23
+ default=False,
24
+ help="Run with launcher GUI to edit settings and start recording",
25
+ )
26
+
27
+ args = parser.parse_args()
28
+
29
+ if args.launcher:
30
+ if len(args.SETTINGS_FILE) > 0:
31
+ print("Can't use launcher and settings file")
32
+ exit()
33
+
34
+ from .gui import launcher
35
+ return launcher.run()
36
+ else:
37
+ gui.run(args.SETTINGS_FILE)
38
+
39
+
40
+
41
+ if __name__ == "__main__": # required because of threading
42
+ cli() # gui.run(), gui.run_with_options()
@@ -0,0 +1 @@
1
+ __author__ = "Oliver Lindemann"
pyforcedaq/_lib/lsl.py ADDED
@@ -0,0 +1,56 @@
1
+ from pylsl import (
2
+ StreamInfo,
3
+ StreamOutlet,
4
+ cf_double64,
5
+ cf_float32,
6
+ cf_int8,
7
+ cf_int16,
8
+ cf_int32,
9
+ cf_int64,
10
+ cf_string,
11
+ cf_undefined,
12
+ )
13
+
14
+
15
+ def init(
16
+ name: str,
17
+ n_channels: int,
18
+ stream_id: str,
19
+ freq: int,
20
+ channel_format: int,
21
+ metadata: dict | None = None,
22
+ ) -> StreamOutlet:
23
+ """
24
+ Initialise a LSL stream
25
+
26
+ Args:
27
+ name: name of the stream
28
+ n_channels: number of channels per sample
29
+ channel_format: format/type of each channel (ex: string, int, ...)
30
+ same format for each channel
31
+ stream_id: unique identifier of the stream
32
+ content_type: content type of stream. By convention LSL uses the content
33
+ types defined in the XDF file format specification where
34
+ applicable
35
+ freq: sampling rate in Hz
36
+
37
+ Return:
38
+ outlet: StreamOulet to push samples with LSL
39
+ """
40
+ # LSL
41
+ # StreamInfo takes name t
42
+ info = StreamInfo(name, "force",
43
+ channel_count=n_channels,
44
+ nominal_srate=freq,
45
+ channel_format=channel_format,
46
+ source_id=stream_id)
47
+
48
+ # Check if there is metadata to add to the lsl stream
49
+ if metadata:
50
+ # Get xml object of the stream created earlier
51
+ xml_info = info.desc()
52
+ # Add meta data to xml object
53
+ for key, data in metadata.items():
54
+ xml_info.append_child_value(key, str(data))
55
+
56
+ return StreamOutlet(info)
@@ -0,0 +1,126 @@
1
+ from os import listdir, path
2
+
3
+ from .timer import get_time_ms
4
+
5
+
6
+ def N2g(N):
7
+ kg = N/9.81
8
+ return kg*1000
9
+
10
+ class MinMaxDetector(object):
11
+
12
+ def __init__(self, start_value, duration):
13
+ self._minmax = [start_value, start_value]
14
+ self._duration = duration
15
+ self._level_change_time = None
16
+
17
+ def process(self, value):
18
+ """Returns minmax (tuple) for a number of samples after the first
19
+ level change has occurred, otherwise None"""
20
+
21
+ if self._level_change_time is not None:
22
+ if (get_time_ms() - self._level_change_time) >= self._duration:
23
+ return tuple(self._minmax)
24
+
25
+ if value > self._minmax[1]:
26
+ self._minmax[1] = value
27
+ elif value < self._minmax[0]:
28
+ self._minmax[0] = value
29
+
30
+ elif self._minmax[0] != value: # level change just occurred
31
+ self._level_change_time = get_time_ms()
32
+ return self.process(value)
33
+
34
+ return None
35
+
36
+ @property
37
+ def is_sampling_for_minmax(self):
38
+ """true true if currently sampling for minmax"""
39
+ return (self._level_change_time is not None) and \
40
+ (get_time_ms() - self._level_change_time) < self._duration
41
+
42
+ def find_calibration_file(calibration_folder, sensor_name,
43
+ calibration_suffix=".cal"):
44
+
45
+ needle = 'Serial="{0}"'.format(sensor_name)
46
+ for x in listdir(path.abspath(calibration_folder)):
47
+ filename = path.join(calibration_folder, x)
48
+ if path.isfile(filename) and filename.endswith(calibration_suffix):
49
+ with open(filename, "r") as fl:
50
+ for l in fl:
51
+ if l.find(needle)>0:
52
+ return filename
53
+ raise RuntimeError("Can't find calibration file for sensor '{0}' : {1}.".format(
54
+ sensor_name, filename))
55
+
56
+
57
+ #Sensor History with moving average filtering and distance, velocity
58
+ class SensorHistory(object):
59
+ """The Sensory History keeps track of the last n recorded sample and
60
+ calculates online the moving average (running mean).
61
+
62
+ SensorHistory.moving_average
63
+
64
+ """
65
+
66
+ def __init__(self, history_size, number_of_parameter):
67
+ self.history = [[0] * number_of_parameter] * history_size
68
+ self.moving_average = [0] * number_of_parameter
69
+ self._correction_cnt = 0
70
+ self._previous_moving_average = self.moving_average
71
+
72
+ def __str__(self):
73
+ return str(self.history)
74
+
75
+ def update(self, values):
76
+ """Update history and calculate moving average
77
+
78
+ (correct for accumulated rounding errors ever 10000 samples)
79
+
80
+ Parameter
81
+ ---------
82
+ values : list of values for all sensor parameters
83
+
84
+ """
85
+
86
+ self._previous_moving_average = self.moving_average
87
+ pop = self.history.pop(0)
88
+ self.history.append(values)
89
+ # pop first element and calc moving average
90
+ if self._correction_cnt > 10000:
91
+ self._correction_cnt = 0
92
+ self.moving_average = self.calc_history_average()
93
+ else:
94
+ self._correction_cnt += 1
95
+ self.moving_average = list(map(
96
+ lambda x: x[0] + (float(x[1] - x[2]) / len(self.history)),
97
+ zip(self.moving_average, values, pop)))
98
+
99
+
100
+ def calc_history_average(self):
101
+ """Calculate history averages for all sensor parameter.
102
+
103
+ The method is more time consuming than calling the property
104
+ `moving_average`. It is does however not suffer from accumulated
105
+ rounding-errors such as moving average.
106
+
107
+ """
108
+
109
+ s = [float(0)] * self.number_of_parameter
110
+ for t in self.history:
111
+ s = list(map(lambda x: x[0] + x[1], zip(s, t)))
112
+ return list(map(lambda x: x / len(self.history), s))
113
+
114
+
115
+ @property
116
+ def history_size(self):
117
+ return len(self.history)
118
+
119
+ @property
120
+ def number_of_parameter(self):
121
+ return len(self.history[0])
122
+
123
+ @property
124
+ def previous_moving_average(self):
125
+ return self._previous_moving_average
126
+
@@ -0,0 +1,52 @@
1
+ import numpy as np
2
+
3
+ from .timer import get_time_ms
4
+
5
+
6
+ class PollingTimeProfile(object):
7
+
8
+ def __init__(self, timing_range=10):
9
+ self._last = None
10
+ self._timing_range = 10
11
+ self._zero_cnt = 0
12
+
13
+ #self._zero_time_polling_frequency = {}
14
+ self.profile_frequency = np.array([0] * (timing_range + 1))
15
+
16
+ def stop(self):
17
+ self._last = None
18
+
19
+ def update(self, time_ms):
20
+ if self._last is not None:
21
+ d = time_ms - self._last
22
+ if d > self._timing_range:
23
+ d = self._timing_range
24
+ self.profile_frequency[d] += 1
25
+
26
+ #if d == 0:
27
+ # self._zero_cnt += 1
28
+ #elif self._zero_cnt > 0:
29
+ # try:
30
+ # self._zero_time_polling_frequency[self._zero_cnt] += 1
31
+ # except:
32
+ # self._zero_time_polling_frequency[self._zero_cnt] = 1
33
+ # self._zero_cnt = 0
34
+
35
+ self._last = time_ms
36
+
37
+ def tick(self):
38
+ self.update(get_time_ms())
39
+
40
+ @property
41
+ def profile_percent(self):
42
+ n = np.sum(self.profile_frequency)
43
+ return self.profile_frequency / n
44
+
45
+ def get_profile_str(self):
46
+ rtn = str(list(self.profile_frequency)
47
+ ).replace("[", "").replace("]", "").replace(" ", "")
48
+ return "polling profile [{}]".format(rtn)
49
+
50
+ #@property
51
+ #def zero_time_polling_frequency(self):
52
+ # return np.array(list(self._zero_time_polling_frequency.items()))
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import gc
4
+ import sys
5
+ import psutil
6
+ import logging
7
+
8
+ from .types import PollingPriority
9
+
10
+ _REALTIME_PRIORITY_CLASS = -18
11
+ _HIGH_PRIORITY_CLASS = -10
12
+
13
+ class ProcessPriorityManager(object):
14
+
15
+ platform = sys.platform
16
+ pybits = 32 + int(sys.maxsize > 2 ** 32) * 32
17
+ main_process_id = psutil.Process().pid
18
+ _normal_nice_value = psutil.Process().nice() # usied on Linux
19
+
20
+ def __init__(self):
21
+ self._subprocs = []
22
+
23
+ def add_subprocess(self, process):
24
+ """add process or list of processes"""
25
+
26
+ if isinstance(process, (list, tuple)):
27
+ for x in process:
28
+ self.add_subprocess(x)
29
+ return
30
+
31
+ try:
32
+ pid = process.pid
33
+ self._subprocs.append(process)
34
+ except:
35
+ logging.warning("Can't add process: {}".format(process))
36
+
37
+ def get_subprocess_priorities(self):
38
+ """returns list with priorities"""
39
+
40
+ rtn = []
41
+ for p in self._subprocs:
42
+ if p.pid is not None:
43
+ rtn.append(get_priority(p.pid))
44
+ else:
45
+ rtn.append(None)
46
+
47
+ return rtn
48
+
49
+ def set_subprocess_priorities(self, level, disable_gc=False):
50
+
51
+ rtn = []
52
+ for p in self._subprocs:
53
+ if p.pid is not None:
54
+ rtn.append(set_priority(level=level, process_id=p.pid,
55
+ disable_gc=disable_gc))
56
+ else:
57
+ rtn.append(False)
58
+
59
+ return rtn
60
+
61
+
62
+ @staticmethod
63
+ def get_main_priority():
64
+ """
65
+ returning main process priority, if subprocess_id=None
66
+ Priority: 'normal', 'high', or 'realtime'
67
+ """
68
+
69
+ return get_priority(ProcessPriorityManager.main_process_id)
70
+
71
+ @staticmethod
72
+ def set_main_priority(level, disable_gc=False):
73
+ """
74
+ changing main process, if subprocess_id=None
75
+ """
76
+
77
+ return set_priority(level=level,
78
+ process_id=ProcessPriorityManager.main_process_id,
79
+ disable_gc=disable_gc)
80
+
81
+ def get_priority(process_id):
82
+ """
83
+ returning main process priority, if subprocess_id=None
84
+ Priority: 'normal', 'high', or 'realtime'
85
+ """
86
+
87
+ try:
88
+ process = psutil.Process(process_id)
89
+ except:
90
+ return None
91
+
92
+ proc_priority = process.nice()
93
+ if ProcessPriorityManager.platform == 'win32':
94
+ if proc_priority == psutil.HIGH_PRIORITY_CLASS:
95
+ return PollingPriority.HIGH
96
+ elif proc_priority == psutil.REALTIME_PRIORITY_CLASS:
97
+ return PollingPriority.REALTIME
98
+
99
+ else:
100
+ if proc_priority <= _REALTIME_PRIORITY_CLASS:
101
+ return PollingPriority.REALTIME
102
+ elif proc_priority <= _HIGH_PRIORITY_CLASS:
103
+ return PollingPriority.HIGH
104
+
105
+ return PollingPriority.NORMAL
106
+
107
+ def set_priority(level, process_id, disable_gc):
108
+
109
+ try:
110
+ process = psutil.Process(process_id)
111
+ except:
112
+ return False
113
+ nice_val = ProcessPriorityManager._normal_nice_value
114
+ level = PollingPriority.get_priority(level)
115
+
116
+ if level == PollingPriority.NORMAL:
117
+ disable_gc = False
118
+ elif level == PollingPriority.HIGH:
119
+ nice_val = _HIGH_PRIORITY_CLASS
120
+ if ProcessPriorityManager.platform == 'win32':
121
+ nice_val = psutil.HIGH_PRIORITY_CLASS
122
+ elif level == PollingPriority.REALTIME:
123
+ nice_val = _REALTIME_PRIORITY_CLASS
124
+ if ProcessPriorityManager.platform == 'win32':
125
+ nice_val = psutil.REALTIME_PRIORITY_CLASS
126
+
127
+ try:
128
+ process.nice(nice_val)
129
+ if disable_gc:
130
+ gc.disable()
131
+ else:
132
+ gc.enable()
133
+ except psutil.AccessDenied:
134
+ logging.warning('Could not set process {} priority '
135
+ 'to {} ({})'.format(process.pid, nice_val, level))
136
+
137
+ return True
138
+
139
+
140
+ # def getProcessAffinities(): TODO?
141
+ #
142
+ # curproc_affinity = SubProcessPriorityManager.current_process.cpu_affinity()
143
+ # return curproc_affinity
144
+
145
+ # @staticmethod
146
+ # def setProcessAffinities(experimentProcessorList, ioHubProcessorList):
147
+ # SubProcessPriorityManager.current_process.cpu_affinity(experimentProcessorList)
148
+
@@ -0,0 +1,45 @@
1
+ """A high-resolution monotonic timer
2
+
3
+ Python 3.3+ provides the time.perf_counter() function, which is a high-resolution monotonic timer.
4
+ """
5
+
6
+ __author__ = 'Florian Krause <florian@expyriment.org>, \
7
+ Oliver Lindemann <oliver@expyriment.org>'
8
+ __version__ = ''
9
+ __revision__ = ''
10
+ __date__ = ''
11
+
12
+ from time import perf_counter, sleep
13
+
14
+
15
+ def get_time_ms() -> int:
16
+ """Get high-resolution time stamp (int) """
17
+ return int(1000 * perf_counter())
18
+
19
+ class Timer(object):#
20
+ """A simple clock class that can be used to measure elapsed time in milliseconds."""
21
+
22
+ def __init__(self, sync_timer=None):
23
+ if sync_timer is None:
24
+ self._init_time = get_time_ms()
25
+ else:
26
+ self._init_time = sync_timer._init_time
27
+
28
+ @property
29
+ def time(self):
30
+ return get_time_ms() - self._init_time
31
+
32
+
33
+ def wait(waiting_time):
34
+ """Wait for a certain amount of milliseconds.
35
+ """
36
+
37
+ start = get_time_ms()
38
+ looptime = 200
39
+ if waiting_time > looptime:
40
+ sleep((waiting_time - looptime) / 1000)
41
+ while get_time_ms() < start + waiting_time:
42
+ pass
43
+
44
+
45
+ app_clock = Timer()