auto-trainer-core 2.0.1.dev349__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 (73) hide show
  1. auto_trainer_core-2.0.1.dev349.dist-info/METADATA +19 -0
  2. auto_trainer_core-2.0.1.dev349.dist-info/RECORD +73 -0
  3. auto_trainer_core-2.0.1.dev349.dist-info/WHEEL +5 -0
  4. auto_trainer_core-2.0.1.dev349.dist-info/top_level.txt +1 -0
  5. autotrainer/core/__init__.py +319 -0
  6. autotrainer/core/analysis/__init__.py +6 -0
  7. autotrainer/core/analysis/alarm_monitor.py +484 -0
  8. autotrainer/core/analysis/audio_spectrum_data.py +27 -0
  9. autotrainer/core/analysis/audio_spectrum_monitor.py +134 -0
  10. autotrainer/core/analysis/auto_tunnel_fan_monitor.py +92 -0
  11. autotrainer/core/analysis/detector.py +179 -0
  12. autotrainer/core/analysis/external_doors_monitor.py +124 -0
  13. autotrainer/core/analysis/global_animal_presence_monitor.py +69 -0
  14. autotrainer/core/analysis/head_fix_measurement.py +15 -0
  15. autotrainer/core/analysis/headbar_pressure_monitor.py +158 -0
  16. autotrainer/core/analysis/load_cell_monitor.py +377 -0
  17. autotrainer/core/analysis/load_cell_tare_monitor.py +217 -0
  18. autotrainer/core/analysis/measurement_data.py +80 -0
  19. autotrainer/core/analysis/pellet_position_monitor.py +108 -0
  20. autotrainer/core/analysis/sensor_analysis.py +390 -0
  21. autotrainer/core/analysis/system_fault_monitor.py +81 -0
  22. autotrainer/core/analysis/system_maintenance_monitor.py +88 -0
  23. autotrainer/core/animal/__init__.py +1 -0
  24. autotrainer/core/animal/animal_subject.py +208 -0
  25. autotrainer/core/capture.py +25 -0
  26. autotrainer/core/configuration/__init__.py +88 -0
  27. autotrainer/core/configuration/alarm_configuration.py +55 -0
  28. autotrainer/core/configuration/animal_presence_configuration.py +17 -0
  29. autotrainer/core/configuration/behavior_configuration.py +323 -0
  30. autotrainer/core/configuration/camera_configuration.py +108 -0
  31. autotrainer/core/configuration/external_doors_monitor_configuration.py +9 -0
  32. autotrainer/core/configuration/hardware_configuration.py +30 -0
  33. autotrainer/core/configuration/inference_configuration.py +26 -0
  34. autotrainer/core/configuration/load_cell_config.py +61 -0
  35. autotrainer/core/configuration/persistence_configuration.py +29 -0
  36. autotrainer/core/configuration/presence_detection_configuration.py +9 -0
  37. autotrainer/core/configuration/system_configuration.py +211 -0
  38. autotrainer/core/configuration/system_fault_config.py +9 -0
  39. autotrainer/core/configuration/system_maintenance_config.py +17 -0
  40. autotrainer/core/diamond_triangle_config.py +150 -0
  41. autotrainer/core/event/__init__.py +54 -0
  42. autotrainer/core/event/api_event_kind.py +5 -0
  43. autotrainer/core/event/api_event_plugin.py +57 -0
  44. autotrainer/core/event/event_info.py +53 -0
  45. autotrainer/core/event/event_manager.py +296 -0
  46. autotrainer/core/event/event_manager_plugin.py +45 -0
  47. autotrainer/core/event/file_event_plugin.py +91 -0
  48. autotrainer/core/event/logger_event_plugin.py +43 -0
  49. autotrainer/core/fixed_array_multiqueue.py +263 -0
  50. autotrainer/core/fixed_array_queue.py +124 -0
  51. autotrainer/core/frame_index.py +34 -0
  52. autotrainer/core/interfaces.py +347 -0
  53. autotrainer/core/logging.py +714 -0
  54. autotrainer/core/message/__init__.py +26 -0
  55. autotrainer/core/message/audio_spectrum_message.py +21 -0
  56. autotrainer/core/message/measurement_message.py +61 -0
  57. autotrainer/core/message/message_handler.py +158 -0
  58. autotrainer/core/message/motor_configuration_message.py +128 -0
  59. autotrainer/core/message/system_command_kind.py +74 -0
  60. autotrainer/core/message/system_message_handler.py +122 -0
  61. autotrainer/core/message/system_status_message.py +112 -0
  62. autotrainer/core/multiproc.py +50 -0
  63. autotrainer/core/notification/__init__.py +2 -0
  64. autotrainer/core/notification/notification_center.py +80 -0
  65. autotrainer/core/notification/trigger_notification.py +27 -0
  66. autotrainer/core/observable_object.py +105 -0
  67. autotrainer/core/perf_monitor.py +77 -0
  68. autotrainer/core/pose_elements.py +196 -0
  69. autotrainer/core/project/__init__.py +19 -0
  70. autotrainer/core/project/project_info.py +486 -0
  71. autotrainer/core/queue_util.py +60 -0
  72. autotrainer/core/reach_event.py +38 -0
  73. autotrainer/core/video_detection.py +326 -0
@@ -0,0 +1,263 @@
1
+ import ctypes
2
+ import logging
3
+ import multiprocessing
4
+ import time
5
+
6
+ from multiprocessing import RawArray, Value
7
+ from multiprocessing import synchronize
8
+ from multiprocessing.context import BaseContext
9
+ from typing import Tuple, List, Optional
10
+
11
+ import numpy
12
+
13
+ from autotrainer.core.logging import get_verbose_logger
14
+ from autotrainer.core.frame_index import FrameIndexCategory
15
+ from autotrainer.core.fixed_array_queue import BufferResult
16
+ from autotrainer.core.multiproc import get_mp_ctx
17
+
18
+ logger = get_verbose_logger(__name__)
19
+
20
+
21
+ class FixedArrayMultiQueue:
22
+ """
23
+ Alternative to multiprocessing.Queue.
24
+
25
+ FixedArrayMultiQueue is an alternative to multiprocessing.Queue that
26
+ * Explicitly queues batches of 2D data from one or more sources
27
+ * Pre-allocates RawArray instances to hold the data for performance
28
+
29
+ The class does not share the exact put/get_xyz interface of the standard Queue classes as it is not a drop-in
30
+ replacement for Queue in terms of behavior.
31
+ """
32
+ def __init__(self,
33
+ depth: int,
34
+ cam_count: int,
35
+ frames_per_camera: int,
36
+ shape: Tuple[int, int],
37
+ *,
38
+ primary: int = 0,
39
+ name: str="noname",
40
+ mp_ctx: Optional[BaseContext] = None,
41
+ ):
42
+ """
43
+ :param depth: queue length before discarding old data if consumers can not keep up
44
+ :param cam_count: number of sources (e.g., cameras) providing 2D data (frames)
45
+ :param frames_per_camera: number of frames per source
46
+ :param shape: size of each frame
47
+ :param primary: indicates which source (index) defines when frames_per_camera is met and a buffer rotates
48
+ """
49
+ if mp_ctx is None:
50
+ mp_ctx = get_mp_ctx()
51
+
52
+ self._name = name
53
+ self._barrier = mp_ctx.Barrier(cam_count)
54
+ # indexing: [buffer][camera][batch_frame]
55
+ self._buffers: List[List[List[RawArray]]] = []
56
+
57
+ self._depth = depth
58
+ self._cam_count = cam_count
59
+ self._frames_per_camera = frames_per_camera
60
+ self._primary = primary
61
+
62
+ # writers:
63
+ self._buffer_index = [0] * self._cam_count
64
+ self._batch_index = [0] * self._cam_count
65
+
66
+ # reader:
67
+ self._read_index = 0
68
+ self._read_sem_acquired = [False] * self._cam_count
69
+
70
+ self._shape = shape
71
+ self._byte_count = shape[0] * shape[1] # 1 frame byte count, no RGB, only 8-bit gray.
72
+
73
+ self._buff_views = []
74
+ for idx in range(self._depth):
75
+ buffer = []
76
+ view = []
77
+ for cdx in range(self._cam_count):
78
+ cam_buffer = []
79
+ cam_view = []
80
+ for bdx in range(self._frames_per_camera):
81
+ # TODO: could use a single rawarray per camera
82
+ cam_buffer.append(mp_ctx.RawArray(ctypes.c_ubyte, self._byte_count))
83
+ buffer.append(cam_buffer)
84
+ view.append(cam_view)
85
+
86
+ self._buffers.append(buffer)
87
+ self._buff_views.append(view)
88
+
89
+ # "free" buckets:
90
+ self._sem_free = [mp_ctx.Semaphore(self._depth) for _ in range(self._cam_count)]
91
+ # "busy" buckets:
92
+ self._sem_busy = [mp_ctx.Semaphore(0) for _ in range(self._cam_count)]
93
+
94
+ self._cams_tot_frames = [
95
+ mp_ctx.Value(ctypes.c_int64)
96
+ for _ in range(self._cam_count)
97
+ ]
98
+
99
+ # a single shared array for all frames indices of all cameras:
100
+ self._frame_indices = mp_ctx.RawArray(ctypes.c_int64, self._depth * self._frames_per_camera * self._cam_count)
101
+
102
+ self._frame_indexing: List[int] = list(numpy.repeat(range(self._frames_per_camera), self._cam_count))
103
+ self._camera_indexing: List[int] = list(numpy.tile(range(self._cam_count), self._frames_per_camera))
104
+
105
+ self._next_counts_log_time = time.perf_counter()
106
+ self._overflow_count = 0
107
+ self._put_count = 0
108
+
109
+ def __str__(self):
110
+ return f"{self.__class__.__name__}({self._name!r})"
111
+
112
+ @property
113
+ def depth(self):
114
+ return self._depth
115
+
116
+ @property
117
+ def camera_count(self):
118
+ return self._cam_count
119
+
120
+ @property
121
+ def frames_per_camera(self):
122
+ return self._frames_per_camera
123
+
124
+ @property
125
+ def batch_size(self):
126
+ return self.camera_count * self.frames_per_camera
127
+
128
+ @property
129
+ def shape(self):
130
+ return self._shape
131
+
132
+ @property
133
+ def buffer_index(self) -> int:
134
+ return self._buffer_index[0]
135
+
136
+ def set_cam_tot_frames(self, cam_idx: int, tot_frames: int):
137
+ """Set the total nbr of frames for cam_idx for eventual sync between camera writers"""
138
+ self._cams_tot_frames[cam_idx].value = tot_frames
139
+
140
+ def pad_to_batch_size(self, cam_idx: int, empty_frame, cnt_net_q_put, *, timeout: float=10):
141
+ """Pad the queue so that all the cams are on same bucket, and the start of it."""
142
+ barrier_wait = self._barrier.wait
143
+ #
144
+ p_before = time.perf_counter()
145
+ barrier_wait(timeout=timeout)
146
+ p_now = time.perf_counter()
147
+ timeout -= p_now - p_before
148
+ # set the tot_frames in different sync_barrier session than the next get_cam_missing_frames
149
+ self.set_cam_tot_frames(cam_idx, cnt_net_q_put)
150
+ p_before = p_now
151
+ barrier_wait(timeout=timeout)
152
+ p_now = time.perf_counter()
153
+ timeout -= p_now - p_before
154
+ missing = self.get_cam_missing_frames(cam_idx)
155
+ p_before = p_now
156
+ barrier_wait(timeout=timeout)
157
+ logger.verbose("padding %s frames (put=%s) to sync with others writers",
158
+ missing, cnt_net_q_put)
159
+ for _ in range(missing):
160
+ p_now = time.perf_counter()
161
+ timeout -= p_now - p_before
162
+ p_before = p_now
163
+ self.put_block(empty_frame, cam_idx, FrameIndexCategory.PADDING,
164
+ timeout=timeout)
165
+
166
+ def get_cam_missing_frames(self, cam_idx: int) -> int:
167
+ """Returns the number of missing frame for camera idx,
168
+ to fill in the batch up to the max camera writer"""
169
+ tot_puts = [self._cams_tot_frames[cdx].value for cdx in range(self._cam_count)]
170
+ max_tot_put = max(tot_puts)
171
+ reminder_max = max_tot_put % self._frames_per_camera
172
+ missing_max = (self._frames_per_camera - reminder_max) % self._frames_per_camera
173
+ max_tot_put += missing_max
174
+ res = max_tot_put - tot_puts[cam_idx]
175
+ logger.debug("get_cam_missing_frames(%s): res=%s tot_puts=%s", cam_idx, res, tot_puts)
176
+ return res
177
+
178
+ def put_block(self, content: numpy.ndarray, camera: int, frame_idx: int, *, timeout: float=10):
179
+ if self.put(content, camera, frame_idx, timeout=timeout) != BufferResult.Ok:
180
+ raise RuntimeError(f"Timeout waiting space in queue for cam-{camera}")
181
+
182
+ def put(self, content: numpy.ndarray, camera: int, frame_idx: Optional[int],
183
+ *, block=True, timeout=0.01) -> BufferResult:
184
+ batch_index = self._batch_index[camera] # 0 ... up to frames per camera - 1
185
+ if batch_index == 0:
186
+ if not self._sem_free[camera].acquire(block, timeout):
187
+ return BufferResult.Overflow
188
+ buffer_index = self._buffer_index[camera] # 0 ... up to depth - 1
189
+ cur_view = memoryview(self._buffers[buffer_index][camera][batch_index]).cast("B")
190
+ # reshape does not copy if not necessary
191
+ cur_view[:] = content.reshape(-1)
192
+ if frame_idx is not None:
193
+ b = numpy.frombuffer(
194
+ memoryview(self._frame_indices).cast("B"), "int64", len(self._frame_indices)
195
+ ).reshape((self._cam_count, self._depth, self._frames_per_camera))[camera]
196
+ b[buffer_index][batch_index] = frame_idx
197
+ self._put_count += 1
198
+ #
199
+ batch_index = self._batch_index[camera] = (batch_index + 1) % self._frames_per_camera
200
+ if batch_index == 0:
201
+ self._sem_busy[camera].release()
202
+ self._buffer_index[camera] = (1 + buffer_index) % self._depth
203
+
204
+ return BufferResult.Ok # if not is_overflow else BufferResult.Overflow
205
+
206
+ def get_output(self, output: numpy.ndarray, frames_indices: Optional[numpy.ndarray] = None, *, timeout: float=0.01) -> bool:
207
+ """Get the next available "output" : i.e: 1 batch of frames_per_camera * nbr_cameras"""
208
+ for cdx in range(self._cam_count):
209
+ if not self._read_sem_acquired[cdx]:
210
+ p0 = time.perf_counter()
211
+ if not self._sem_busy[cdx].acquire(timeout=timeout):
212
+ return False
213
+ p1 = time.perf_counter()
214
+ timeout -= p1 - p0
215
+ self._read_sem_acquired[cdx] = True
216
+ read_idx_value = self._read_index
217
+ buffer = self._buffers[read_idx_value]
218
+ for idx, cdx in enumerate(self._camera_indexing): # cdx: 0 1 0 1 0 1 0 1
219
+ v = numpy.frombuffer(
220
+ buffer[cdx][self._frame_indexing[idx]], "uint8", self._byte_count
221
+ ).reshape(self.shape)
222
+ # NB: current predict model expects an RGB frame,
223
+ # we have so to copy 3 times the current gray image/frame into the 3 planes:
224
+ # for fn in range(3):
225
+ # output[idx, :, :, fn] = v
226
+ # we do that after releasing the sema free below
227
+ output[idx, :, :, 0] = v
228
+
229
+ if frames_indices is not None:
230
+ # copy frames indices
231
+ frames_indices[:, :] = numpy.frombuffer(
232
+ memoryview(self._frame_indices).cast("B"), "int64", len(self._frame_indices)
233
+ ).reshape(
234
+ (self._cam_count, self._depth, self._frames_per_camera)
235
+ )[:, read_idx_value, :]
236
+
237
+ # put back the used/copied bucket as free:
238
+ for cdx in range(self._cam_count):
239
+ self._sem_free[cdx].release()
240
+ self._read_sem_acquired[cdx] = False
241
+
242
+ # make the copy for rgb after having released sema_free
243
+ for idx, cdx in enumerate(self._camera_indexing):
244
+ for fn in (1, 2):
245
+ output[idx, :, :, fn] = output[idx, :, :, 0]
246
+
247
+ # don't forget to:
248
+ self._read_index = (read_idx_value + 1) % self._depth
249
+ return True
250
+
251
+ def put_frame_index_category(self, frame, frame_idx: FrameIndexCategory, *,
252
+ cam_idx: Optional[int] = None, timeout: float = 10):
253
+ logger.verbose("putting frame index category %s", frame_idx)
254
+ for _ in range(self._frames_per_camera):
255
+ for cdx in range(self._cam_count):
256
+ if cam_idx is None or cam_idx == cdx:
257
+ t0 = time.perf_counter()
258
+ # put_block() will take care to raise if timeout occurs
259
+ self.put_block(frame, cdx, frame_idx, timeout=timeout)
260
+ timeout -= time.perf_counter() - t0
261
+
262
+ def get_frame_indices(self):
263
+ return list(self._frame_indices)
@@ -0,0 +1,124 @@
1
+ import ctypes
2
+ import logging
3
+ import multiprocessing
4
+ import queue
5
+ import time
6
+ from enum import IntEnum
7
+ from multiprocessing import RawArray, Value
8
+ from multiprocessing.context import BaseContext
9
+ from typing import List, Optional, Tuple
10
+
11
+ import numpy
12
+
13
+ from autotrainer.core.multiproc import get_mp_ctx
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class BufferResult(IntEnum):
19
+ Ok = 0
20
+ Overflow = 1
21
+
22
+
23
+ class FixedArrayQueue:
24
+ def __init__(self, depth: int, shape: Tuple[int, int], name: str="noname", *,
25
+ mp_ctx: Optional[BaseContext] = None,
26
+ ):
27
+ if mp_ctx is None:
28
+ mp_ctx = get_mp_ctx()
29
+
30
+ self._name = name
31
+ # indexing: [buffer]
32
+ self._buffers: List[RawArray] = []
33
+ self._is_dirty: List[Value] = []
34
+
35
+ self._depth = depth
36
+ self._shape = shape
37
+ self._byte_count = shape[0] * shape[1]
38
+
39
+ self._buffer_index = 0
40
+ self._read_index = 0
41
+
42
+ self._buff_views = []
43
+ for idx in range(self._depth):
44
+ self._buffers.append(mp_ctx.RawArray(ctypes.c_ubyte, self._byte_count))
45
+ self._is_dirty.append(mp_ctx.Value(ctypes.c_bool, False))
46
+
47
+ self._next_counts_log_time = time.time()
48
+ self._overflow_count = 0
49
+ self._put_count = 0
50
+
51
+ def __str__(self):
52
+ return f"{self.__class__.__name__}({self._name!r})"
53
+
54
+ @property
55
+ def shape(self):
56
+ return self._shape
57
+
58
+ @property
59
+ def buffer_index(self) -> int:
60
+ return self._buffer_index
61
+
62
+ def put(self, content: numpy.ndarray):
63
+ return self._put(content)
64
+
65
+ def _put(self, content: numpy.ndarray):
66
+ self._buff_views.clear()
67
+ for idx in range(self._depth):
68
+ self._buff_views.append(memoryview(self._buffers[idx]).cast("B"))
69
+ self.put = self._put = self._put_view
70
+ return self._put_view(content)
71
+
72
+ def _put_view(self, content: numpy.ndarray) -> BufferResult:
73
+ buffer_index = self._buffer_index
74
+ is_overflow = self._is_dirty[buffer_index].value
75
+ if is_overflow:
76
+ self._overflow_count += 1
77
+ return BufferResult.Overflow
78
+
79
+ self._put_count += 1
80
+
81
+ # t = time.time()
82
+ # if t > self._next_counts_log_time:
83
+ # self._next_counts_log_time += 10
84
+ # logger.info("%s: put=%s overflow=%s", self, self._put_count, self._overflow_count)
85
+ # self._put_count = self._overflow_count = 0
86
+
87
+ self._buff_views[buffer_index][:] = content.reshape(-1) # content.flatten()
88
+
89
+ self._is_dirty[buffer_index].value = True
90
+
91
+ buffer_index += 1
92
+ buffer_index %= self._depth
93
+ self._buffer_index = buffer_index
94
+
95
+ return BufferResult.Ok if not is_overflow else BufferResult.Overflow
96
+
97
+ def get(self, block: bool = True, timeout: float = 0.01) -> numpy.ndarray:
98
+ perf_timeout = time.perf_counter() + timeout
99
+ while True:
100
+ read_index = self._read_index
101
+ if self._is_dirty[read_index].value:
102
+ break
103
+ if time.perf_counter() > perf_timeout:
104
+ raise queue.Empty
105
+ time.sleep(0.005)
106
+
107
+ buffer = self._buffers[read_index]
108
+
109
+ v = numpy.frombuffer(buffer, "uint8", self._byte_count).reshape(self.shape)
110
+ output = v.copy() # numpy.frombuffer() returns a view
111
+
112
+ self._is_dirty[read_index].value = False
113
+
114
+ self._read_index = (read_index + 1) % self._depth
115
+
116
+ return output
117
+
118
+ # noinspection PyMethodMayBeStatic
119
+ def empty(self) -> bool:
120
+ return True
121
+
122
+ # noinspection PyMethodMayBeStatic
123
+ def qsize(self) -> int:
124
+ return 0
@@ -0,0 +1,34 @@
1
+ import enum
2
+
3
+
4
+ class FrameIndexCategory(enum.IntEnum):
5
+
6
+ SWITCH_TO_ONLINE = -5 # not anymore used, replaced by InferenceCommandMessageKind.SetOfflineToLive
7
+ PADDING = -4
8
+ EOF_OFFLINE_PROCESSING = -3
9
+ EOF_RECORDING = -2
10
+
11
+ ONLINE_NO_RECORDING = -1 # NB: DO NOT CHANGE VALUE OF THIS ONE
12
+
13
+ RECORDING_OR_OFFLINE_PROCESSING = 1
14
+ # NB: positive or zero frane index means recording online or offline reprocessing.
15
+
16
+ @classmethod
17
+ def is_signaling_index(cls, frame_idx) -> bool:
18
+ return frame_idx < cls.ONLINE_NO_RECORDING
19
+
20
+ @classmethod
21
+ def _missing_(cls, value):
22
+ if isinstance(value, str):
23
+ value = int(value)
24
+ if isinstance(value, float):
25
+ v = int(value)
26
+ if v != value:
27
+ return super()._missing_(value)
28
+ value = v
29
+ if value >= 0:
30
+ return FrameIndexCategory.RECORDING_OR_OFFLINE_PROCESSING
31
+ for k, m in cls._member_map_.items():
32
+ if m.value == value:
33
+ return m
34
+ return super()._missing_(value)