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,486 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import dataclasses
5
+ import logging
6
+ import multiprocessing.managers
7
+ import multiprocessing as mp
8
+ import os
9
+ import os.path
10
+ import sys
11
+ from dataclasses import dataclass
12
+ from datetime import datetime
13
+ from enum import IntEnum
14
+ from multiprocessing.sharedctypes import Synchronized
15
+ from pathlib import Path
16
+ from typing import Tuple, NamedTuple, Union, Optional, ClassVar
17
+
18
+ from typing_extensions import Self
19
+
20
+ from autotrainer.core import (
21
+ ValueHolderDescriptor,
22
+ RawValueHolder,
23
+ get_verbose_logger,
24
+ Offset3DTuple,
25
+ )
26
+ from autotrainer.core.multiproc import get_mp_ctx
27
+
28
+ logger = get_verbose_logger(__name__)
29
+
30
+
31
+ DATE_FORMAT = "%Y%m%d"
32
+
33
+ TIME_FORMAT = "%H%M%S"
34
+
35
+ DATE_TIME_FORMAT = f"{DATE_FORMAT}_{TIME_FORMAT}"
36
+
37
+ MINUTE_INTERVAL_FORMAT = "h%Hm%M"
38
+
39
+ HOUR_INTERVAL_FORMAT = "h%H"
40
+
41
+ IMAGE_CAPTURE_SUFFIX = "_images"
42
+
43
+ REACH_EVENT_SUFFIX = "_reach_events.h5"
44
+
45
+
46
+ class ProjectInterval(IntEnum):
47
+ NONE = -1
48
+ MINUTE = 0
49
+ HOUR = 1
50
+
51
+
52
+ class ProjectPath(NamedTuple):
53
+ location: str
54
+ prefix: str
55
+ full_path: str
56
+
57
+
58
+ class IntervalSource(NamedTuple):
59
+ location: str
60
+ prefix: str
61
+ interval: int
62
+
63
+
64
+ class SessionSource(NamedTuple):
65
+ location: str
66
+ prefix: str
67
+ session_index: int # trial index
68
+
69
+
70
+ class IntervalFileInfo(NamedTuple):
71
+ location: str
72
+ file: str
73
+ current_interval: int
74
+
75
+
76
+ def _ensure_location(location: str):
77
+ try:
78
+ path = Path(location)
79
+ path.mkdir(parents=True, exist_ok=True)
80
+ except Exception as err:
81
+ logger.error("Could not create dir %r: %s", location, err)
82
+ raise
83
+
84
+
85
+ # Windows does not like .mp4 extension when opencv is technically saving to an mkv container.
86
+ video_write_ext = "mp4" if sys.platform.startswith("linux") else "mkv"
87
+
88
+
89
+ # NB: so that we can easily patch from test
90
+ def _get_datetime_now() -> datetime:
91
+ return datetime.now()
92
+
93
+
94
+ @dataclass
95
+ class _ProjectInfo:
96
+ """Contains all details needed to uniquely identify a session project.
97
+
98
+ The ProjectInfo is used/shared possibly to multiple processes.
99
+ It will only have this multiprocess capability if-and-only-if it's created without specifying the `when` and
100
+ `session` attributes. Or if they are both set to None.
101
+ If `when` is provided but not `session` then `session` defaults to 1.
102
+ Providing `session` but not `when` is an error.
103
+ """
104
+
105
+ root: str = ""
106
+ device_id: str = ""
107
+ _when: Union[Synchronized[ctypes.c_double], RawValueHolder] = None
108
+ when: ClassVar[datetime] = ValueHolderDescriptor( # noqa
109
+ convert_from=datetime.fromtimestamp,
110
+ convert_to=lambda v: v.timestamp(),
111
+ )
112
+ ensure_exists: bool = False
113
+ camera_1: str = ""
114
+ camera_2: str = ""
115
+ _session: Union[Synchronized[ctypes.c_uint32], RawValueHolder] = None
116
+ session: ClassVar[int] = ValueHolderDescriptor() # noqa
117
+ send_position: Optional[Offset3DTuple] = None
118
+ dcs_send_position: Optional[Offset3DTuple] = None
119
+
120
+
121
+ @dataclass
122
+ class ProjectInfo(_ProjectInfo):
123
+
124
+ # custom/overloaded init to allow normal/exact same than previous argument names
125
+ def __init__(
126
+ self,
127
+ *,
128
+ root: str = _ProjectInfo.root,
129
+ device_id: str = _ProjectInfo.device_id,
130
+ when: Optional[datetime] = None,
131
+ ensure_exists: bool = _ProjectInfo.ensure_exists,
132
+ camera_1: str = _ProjectInfo.camera_1,
133
+ camera_2: str = _ProjectInfo.camera_2,
134
+ session: Optional[int] = None,
135
+ send_position: Optional[Offset3DTuple] = _ProjectInfo.send_position,
136
+ dcs_send_position: Optional[Offset3DTuple] = _ProjectInfo.dcs_send_position,
137
+ mp_manager: Optional[multiprocessing.managers.BaseManager]=None,
138
+ ):
139
+ super().__init__()
140
+ if when is None:
141
+ _when = None
142
+ else:
143
+ _when = RawValueHolder(when.timestamp())
144
+ if session is None:
145
+ session = 1
146
+ if session is None:
147
+ _session = None
148
+ else:
149
+ _session = RawValueHolder(session)
150
+ if when is None:
151
+ raise ValueError("Cannot create ProjectInfo with session but without when")
152
+ self.root = root
153
+ self.device_id = device_id
154
+ self._when = _when
155
+ self.ensure_exists = ensure_exists
156
+ self.camera_1 = camera_1
157
+ self.camera_2 = camera_2
158
+ self.send_position = send_position
159
+ self.dcs_send_position = dcs_send_position
160
+ self._session = _session
161
+ if self._session is None and self._when is None:
162
+ ctx = get_mp_ctx() if mp_manager is None else mp_manager
163
+ self._session = ctx.Value(ctypes.c_uint32, 1)
164
+ # use the same lock for both session and when mp shared values:
165
+ self._when = ctx.Value(ctypes.c_double, # double required, not float !!
166
+ _get_datetime_now().timestamp(),
167
+ # with mp_manager on 3.8 we would need to access private _getvalue
168
+ # lock=(session_shared_obj if mp_manager is None
169
+ # else session_shared_obj._getvalue()).get_lock()
170
+ # see:
171
+ )
172
+
173
+ def __repr__(self):
174
+ return (
175
+ f"{self.__class__.__name__}(device={self.device_id!r}, session={self.session!r}, when={self.when!r}, "
176
+ f"send_pos={self.send_position}, dcs_send_pos={self.dcs_send_position})"
177
+ )
178
+
179
+ def __eq__(self, other):
180
+ if isinstance(other, ProjectInfo):
181
+ return (
182
+ self.root == other.root
183
+ and self.device_id == other.device_id
184
+ and self.when == other.when
185
+ and self.camera_1 == other.camera_1
186
+ and self.camera_2 == other.camera_2
187
+ and self.session == other.session
188
+ )
189
+ return super().__eq__(other)
190
+
191
+ # for multiprocess capability:
192
+ def __enter__(self):
193
+ if hasattr(self._session, "acquire"):
194
+ self._session.acquire()
195
+
196
+ def __exit__(self, ex, ex_type, ex_stack):
197
+ if hasattr(self._session, "acquire"):
198
+ self._session.release()
199
+
200
+ def _get_when_or_now(self, when: Optional[datetime] = None) -> datetime:
201
+ if when is None:
202
+ when = self.when
203
+ if when is None:
204
+ when = _get_datetime_now()
205
+ assert when is not None
206
+ return when
207
+
208
+ def is_valid(self):
209
+ return self.root is not None and len(self.root) > 0
210
+
211
+ def get_day_path(self, skip_ensure: bool = False, when: Optional[datetime]=None) -> Tuple[str, str]:
212
+ """Get the location and related datetime for given arguments.
213
+ If when is None then self.when is used, which can eventually be None, in which case now() is used.
214
+ """
215
+ when: datetime = self._get_when_or_now(when)
216
+ today = when.strftime(DATE_FORMAT)
217
+ location = os.path.join(self.root, today)
218
+ if self.device_id:
219
+ location = os.path.join(location, self.device_id)
220
+
221
+ if not skip_ensure and self.ensure_exists:
222
+ _ensure_location(location)
223
+
224
+ return location, today
225
+
226
+ def get_interval(self, interval: ProjectInterval = ProjectInterval.NONE, when: Optional[datetime] = None) -> int:
227
+ if interval == ProjectInterval.NONE:
228
+ return -1
229
+ r_when = self._get_when_or_now(when)
230
+ return r_when.hour if interval == ProjectInterval.HOUR else r_when.minute
231
+
232
+ def get_interval_path(
233
+ self,
234
+ name: str = "",
235
+ interval: ProjectInterval = ProjectInterval.HOUR,
236
+ skip_ensure: bool = False,
237
+ when: Optional[datetime] = None,
238
+ ) -> IntervalSource:
239
+ r_when = self._get_when_or_now(when)
240
+ time_format = HOUR_INTERVAL_FORMAT if interval == ProjectInterval.HOUR else MINUTE_INTERVAL_FORMAT
241
+ when_str = f"_{r_when.strftime(time_format)}"
242
+ location, today = self.get_day_path(skip_ensure=skip_ensure, when=r_when)
243
+ s = f"_{name}" if name else ""
244
+ d = f"_{self.device_id}" if self.device_id else ""
245
+ prefix = f"{today}{d}{when_str}{s}"
246
+ return IntervalSource(location, prefix, r_when.hour if interval == ProjectInterval.HOUR else r_when.minute)
247
+
248
+ def get_session_path(self, name: str = "", session: int = -1, skip_ensure: bool = False,
249
+ when: Optional[datetime] = None) -> SessionSource:
250
+ (location, today) = self.get_day_path(True, when=when)
251
+ if session < 0:
252
+ session = self.session
253
+ session_str = f"trial{session:03}"
254
+ location = os.path.join(location, session_str)
255
+ if not skip_ensure and self.ensure_exists:
256
+ _ensure_location(location)
257
+ d = f"_{self.device_id}" if self.device_id else ""
258
+ prefix = f"{today}{d}_{session_str}"
259
+ s = f"_{name}" if name else ""
260
+ prefix = f"{prefix}{s}"
261
+ return SessionSource(location, prefix, session)
262
+
263
+ def get_source_path(
264
+ self,
265
+ name: str = "",
266
+ interval: ProjectInterval = ProjectInterval.NONE,
267
+ session: int = -1,
268
+ skip_ensure: bool = False,
269
+ when: Optional[datetime] = None,
270
+ ) -> ProjectPath:
271
+ if interval is None or interval == ProjectInterval.NONE:
272
+ path = self.get_session_path(name, session=session, skip_ensure=skip_ensure, when=when)
273
+ else:
274
+ path = self.get_interval_path(name, interval=interval, skip_ensure=skip_ensure, when=when)
275
+ return ProjectPath(path.location, path.prefix, os.path.join(path.location, path.prefix))
276
+
277
+ def get_metadata_file(self, session: Optional[int] = -1, when: Optional[datetime] = None) -> str:
278
+ """Returns the metadata file path,
279
+ if session is None it's non-trial based.
280
+ if < -1 then self.session is used
281
+ """
282
+ when: datetime = self._get_when_or_now(when)
283
+ timestamp = when.strftime(TIME_FORMAT)
284
+ if session is None:
285
+ location, prefix = self.get_day_path(when=when)
286
+ d = f"_{self.device_id}" if self.device_id else ""
287
+ return os.path.join(location, f"{prefix}{d}_{timestamp}_metadata")
288
+ else:
289
+ source = self.get_session_path("metadata", session=session)
290
+ return os.path.join(source.location, f"{source.prefix}")
291
+
292
+ def get_monitor_file(self, name: str = "monitor", ext: str = "csv",
293
+ interval: ProjectInterval = ProjectInterval.HOUR,
294
+ when: Optional[datetime] = None) -> Optional[IntervalFileInfo]:
295
+ iv_path = self.get_interval_path(name, interval, when=when)
296
+ return IntervalFileInfo(iv_path.location,
297
+ os.path.join(iv_path.location, f"{iv_path.prefix}.{ext}"),
298
+ iv_path.interval)
299
+
300
+ def get_audio_spectrum_file(
301
+ self,
302
+ name: str = "spectrum",
303
+ ext: str = "csv",
304
+ interval: ProjectInterval = ProjectInterval.HOUR,
305
+ when: Optional[datetime] = None
306
+ ) -> IntervalFileInfo:
307
+ audio_path = self.get_interval_path(name, interval, when=when)
308
+ return IntervalFileInfo(audio_path.location,
309
+ os.path.join(audio_path.location, f"{audio_path.prefix}.{ext}"),
310
+ audio_path.interval)
311
+
312
+ def get_webcam_presence_file(
313
+ self,
314
+ name: str = "cage",
315
+ ext: str = "csv",
316
+ interval: ProjectInterval = ProjectInterval.HOUR,
317
+ when: Optional[datetime] = None,
318
+ ) -> IntervalFileInfo:
319
+ web_path = self.get_interval_path(name, interval, when=when)
320
+ return IntervalFileInfo(web_path.location,
321
+ os.path.join(web_path.location, f"{web_path.prefix}.{ext}"),
322
+ web_path.interval)
323
+
324
+ def get_video_path(
325
+ self,
326
+ name: str = "",
327
+ interval: ProjectInterval = ProjectInterval.NONE,
328
+ session: int = -1,
329
+ allow_overwrite: bool = False,
330
+ ) -> Tuple[str, str, str]:
331
+ """Get the 3-tuple of video paths for given arguments"""
332
+ vid_path = self.get_source_path(name, interval=interval, session=session)
333
+
334
+ file_name = f"{vid_path.full_path}.{video_write_ext}"
335
+ index = 0
336
+
337
+ if not allow_overwrite:
338
+ while os.path.exists(file_name):
339
+ index += 1
340
+ file_name = f"{vid_path.full_path}_{index}.{video_write_ext}"
341
+
342
+ modifier = "" if index == 0 else "_" + str(index)
343
+
344
+ ts_file = f"{vid_path.full_path}_timestamps{modifier}.txt"
345
+ frames_processed_indices_file = f"{vid_path.full_path}_processed_frames.txt"
346
+
347
+ return file_name, ts_file, frames_processed_indices_file
348
+
349
+ def get_image_capture_path(self,
350
+ name: str = "",
351
+ interval: ProjectInterval = ProjectInterval.NONE,
352
+ session: int = -1,
353
+ when: Optional[datetime] = None,
354
+ ) -> Tuple[Path, str]:
355
+ """Get the 2-tuple of image paths for given arguments"""
356
+ base = self.get_source_path(name, interval=interval, session=session, skip_ensure=True, when=when)
357
+ image_location = os.path.join(base.location, f"{base.prefix}{IMAGE_CAPTURE_SUFFIX}")
358
+ if self.ensure_exists:
359
+ _ensure_location(image_location)
360
+ image_file_format_str = base.prefix + "_{when}" + ".png"
361
+ return Path(image_location), image_file_format_str
362
+
363
+ def get_reach_event_path(self) -> Path:
364
+ base = self.get_source_path("")
365
+ return Path(base.location).joinpath(f"{base.prefix}{REACH_EVENT_SUFFIX}")
366
+
367
+ def get_intersession_pose_path(
368
+ self,
369
+ name: str = "",
370
+ session: int = -1,
371
+ allow_overwrite: bool = False,
372
+ *,
373
+ suffix: str = "",
374
+ when: Optional[datetime] = None,
375
+ ) -> str:
376
+ source = self.get_source_path(name, session=session, when=when)
377
+ file_name = os.path.join(source.location, f"{source.prefix}_raw2D{suffix}.h5")
378
+ index = 0
379
+ if not allow_overwrite:
380
+ while os.path.exists(file_name):
381
+ index += 1
382
+ file_name = os.path.join(source.location, f"{source.prefix}_{index}.h5")
383
+ return file_name
384
+
385
+ def calculate_next_session_index(self, when: Optional[datetime] = None):
386
+ """Calculate the next session index & date and store it locally"""
387
+ self._calculate_next_session_index(when)
388
+ logger.success("Calculated next session index=%s when=%s",
389
+ self.session, self.when)
390
+
391
+ def _calculate_next_session_index(self, when: Optional[datetime] = None):
392
+ """Calculate the next session index & date and store it locally"""
393
+ if when is None:
394
+ when = _get_datetime_now()
395
+ assert when is not None
396
+ prev_when = self.when
397
+ assert prev_when is not None
398
+ location, _ = self.get_day_path(when=when)
399
+ logger.debug("calculating next session index in %s", location)
400
+ path = Path(location)
401
+ existed = path.exists()
402
+ was_dir = path.is_dir()
403
+ path.mkdir(parents=True, exist_ok=True) # this ensures 2 consecutive won't get same
404
+ if not existed or not was_dir:
405
+ tentative_p, _, _ = self.get_session_path(session=1, when=when, skip_ensure=True)
406
+ try:
407
+ Path(tentative_p).mkdir(parents=True)
408
+ except FileExistsError:
409
+ pass
410
+ else:
411
+ with self:
412
+ self.session = 1
413
+ self.when = when
414
+ return
415
+ if prev_when.date() < when.date():
416
+ # actually the day directory could be already created from possible other writers to it,
417
+ # this ensures that we should get the correct new session nbr
418
+ new_session_nbr = 1
419
+ else:
420
+ new_session_nbr = self.session + 1
421
+ tentative_p, _, _ = self.get_session_path(session=new_session_nbr, when=when, skip_ensure=True)
422
+ tentative_p = Path(tentative_p)
423
+ if not tentative_p.exists():
424
+ try:
425
+ Path(tentative_p).mkdir(parents=True)
426
+ except FileExistsError:
427
+ pass
428
+ else:
429
+ logger.info("found fast next session: %s", tentative_p)
430
+ with self:
431
+ self.session = new_session_nbr
432
+ self.when = when
433
+ return
434
+
435
+ # slower code way
436
+ session_dirs = [x.name[-3:] for x in path.iterdir() if x.is_dir() and "trial" in x.name]
437
+
438
+ def int_map_fcn(value: str):
439
+ try:
440
+ return int(value)
441
+ except (ValueError, TypeError):
442
+ return None
443
+
444
+ session_vals = [int(x) for x in session_dirs if int_map_fcn(x) is not None]
445
+ if len(session_vals) == 0:
446
+ logger.debug("no existing sessions found")
447
+ with self:
448
+ self.session = 1
449
+ self.when = when
450
+ else:
451
+ logger.debug("found %s existing session directories", len(session_vals))
452
+ session_vals.sort(reverse=True)
453
+ greater_val = session_vals[0]
454
+ logger.info("last session index for day: %s", greater_val)
455
+ with self:
456
+ self.session = greater_val + 1
457
+ self.when = when
458
+
459
+ def get_log_file_path(self, when: Optional[datetime] = None, *, auto_new: bool=True) -> Path:
460
+ when = self._get_when_or_now(when)
461
+ loc, today = self.get_day_path(when=when)
462
+ today = when.strftime(DATE_FORMAT)
463
+ dev_id = self.device_id
464
+ s_dev = f"_{dev_id}" if dev_id else ""
465
+ loc = Path(loc)
466
+ fmt = f"{today}{s_dev}_{{idx}}.log"
467
+ tot_prev_log_files = len(tuple(loc.glob(fmt.format(idx="*"))))
468
+ idx = tot_prev_log_files
469
+ if auto_new:
470
+ idx += 1
471
+ return Path(loc).joinpath(fmt.format(idx=f"{idx:03d}"))
472
+
473
+ def to_local_value(self) -> Self:
474
+ """Detach, if it was, from the possible shared memory values used for `when` & `session`.
475
+ This ensures that the "detached" instance won't have its `when` and `session` values updated
476
+ in the background by any other process attached to the shared values.
477
+ """
478
+ with self:
479
+ dct = {
480
+ field_name: getattr(self, field_name)
481
+ for field_name, field in
482
+ ((field.name.lstrip("_"), field)
483
+ for field in dataclasses.fields(self)
484
+ )
485
+ }
486
+ return self.__class__(**dct)
@@ -0,0 +1,60 @@
1
+ import sys
2
+ from queue import Empty
3
+ from multiprocessing import Queue
4
+ from typing import Union
5
+
6
+ from autotrainer.core import FixedArrayQueue
7
+ from autotrainer.core.logging import get_verbose_logger
8
+
9
+ logger = get_verbose_logger(__name__)
10
+
11
+
12
+ def clear_queue(queue: Union[Queue, FixedArrayQueue],
13
+ *, log_dumped: bool=False, name: str=""):
14
+ """Empty a queue."""
15
+ if queue is None or sys.platform == "darwin":
16
+ return
17
+
18
+ if not name:
19
+ name = str(queue)
20
+
21
+ flushed = 0
22
+ task_done = getattr(queue, "task_done", lambda: None)
23
+ try:
24
+ while not queue.empty() or queue.qsize() > 0:
25
+ try:
26
+ obj = queue.get_nowait()
27
+ flushed += 1
28
+ task_done()
29
+ if log_dumped:
30
+ logger.debug("queue %s: flushed %s: %s", name, type(obj), obj)
31
+ except Empty:
32
+ empty = queue.empty()
33
+ qsize = queue.qsize()
34
+ if not empty or qsize > 0:
35
+ logger.warning("queue %s: raised Empty but empty()=%s and qsize()=%s",
36
+ name, empty, qsize)
37
+ # continue
38
+ break
39
+ except Exception as err:
40
+ logger.error("Could not clear queue %s: %s", name, err)
41
+
42
+ logger.debug("queue %s: flushed %s items", name, flushed)
43
+
44
+
45
+ def trim_queue(queue: Queue, limit: int) -> bool:
46
+ """Trim queue length to the specified limit."""
47
+ # unused atm
48
+ if queue is None or sys.platform == "darwin":
49
+ return False
50
+
51
+ trimmed = False
52
+
53
+ while queue.qsize() > limit:
54
+ try:
55
+ queue.get_nowait()
56
+ trimmed = True
57
+ except Empty:
58
+ pass
59
+
60
+ return trimmed
@@ -0,0 +1,38 @@
1
+ import dataclasses
2
+ import enum
3
+
4
+
5
+ class ReachEventMethod: # (str, enum.Enum):
6
+ # NB: not enum subclass, as the members are pickled in the saved h5 files,
7
+ # and this requires the readers to have access to the original enum class.
8
+ # which might not always be possible or desirable.
9
+
10
+ NONE = "none"
11
+ OTHER = "other"
12
+ RIGHT_HAND = "right_hand"
13
+ LEFT_HAND = "left_hand"
14
+ TONGUE = "tongue"
15
+
16
+
17
+ class ReachEventOutcome: # (str, enum.Enum): # see above.
18
+
19
+ NONE = "none"
20
+ STALLED = "stalled"
21
+ MISSED = "missed"
22
+ DROPPED = "dropped"
23
+ GRABBED = "grabbed"
24
+ EATEN = "eaten"
25
+
26
+
27
+ @dataclasses.dataclass
28
+ class ReachEvent:
29
+ init: int # frame index
30
+ end: int # frame index
31
+ max: int = -1 # frame index
32
+
33
+ # Notice the "str" type hint for both method and outcome,
34
+ # *not* the 'ReachEventMethod' or 'ReachEventOutcome' type hint.
35
+ method: str = ReachEventMethod.NONE
36
+ outcome: str = ReachEventOutcome.NONE
37
+
38
+ delay_since_presented: float = 0 # is basically: init / fps