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.
- auto_trainer_core-2.0.1.dev349.dist-info/METADATA +19 -0
- auto_trainer_core-2.0.1.dev349.dist-info/RECORD +73 -0
- auto_trainer_core-2.0.1.dev349.dist-info/WHEEL +5 -0
- auto_trainer_core-2.0.1.dev349.dist-info/top_level.txt +1 -0
- autotrainer/core/__init__.py +319 -0
- autotrainer/core/analysis/__init__.py +6 -0
- autotrainer/core/analysis/alarm_monitor.py +484 -0
- autotrainer/core/analysis/audio_spectrum_data.py +27 -0
- autotrainer/core/analysis/audio_spectrum_monitor.py +134 -0
- autotrainer/core/analysis/auto_tunnel_fan_monitor.py +92 -0
- autotrainer/core/analysis/detector.py +179 -0
- autotrainer/core/analysis/external_doors_monitor.py +124 -0
- autotrainer/core/analysis/global_animal_presence_monitor.py +69 -0
- autotrainer/core/analysis/head_fix_measurement.py +15 -0
- autotrainer/core/analysis/headbar_pressure_monitor.py +158 -0
- autotrainer/core/analysis/load_cell_monitor.py +377 -0
- autotrainer/core/analysis/load_cell_tare_monitor.py +217 -0
- autotrainer/core/analysis/measurement_data.py +80 -0
- autotrainer/core/analysis/pellet_position_monitor.py +108 -0
- autotrainer/core/analysis/sensor_analysis.py +390 -0
- autotrainer/core/analysis/system_fault_monitor.py +81 -0
- autotrainer/core/analysis/system_maintenance_monitor.py +88 -0
- autotrainer/core/animal/__init__.py +1 -0
- autotrainer/core/animal/animal_subject.py +208 -0
- autotrainer/core/capture.py +25 -0
- autotrainer/core/configuration/__init__.py +88 -0
- autotrainer/core/configuration/alarm_configuration.py +55 -0
- autotrainer/core/configuration/animal_presence_configuration.py +17 -0
- autotrainer/core/configuration/behavior_configuration.py +323 -0
- autotrainer/core/configuration/camera_configuration.py +108 -0
- autotrainer/core/configuration/external_doors_monitor_configuration.py +9 -0
- autotrainer/core/configuration/hardware_configuration.py +30 -0
- autotrainer/core/configuration/inference_configuration.py +26 -0
- autotrainer/core/configuration/load_cell_config.py +61 -0
- autotrainer/core/configuration/persistence_configuration.py +29 -0
- autotrainer/core/configuration/presence_detection_configuration.py +9 -0
- autotrainer/core/configuration/system_configuration.py +211 -0
- autotrainer/core/configuration/system_fault_config.py +9 -0
- autotrainer/core/configuration/system_maintenance_config.py +17 -0
- autotrainer/core/diamond_triangle_config.py +150 -0
- autotrainer/core/event/__init__.py +54 -0
- autotrainer/core/event/api_event_kind.py +5 -0
- autotrainer/core/event/api_event_plugin.py +57 -0
- autotrainer/core/event/event_info.py +53 -0
- autotrainer/core/event/event_manager.py +296 -0
- autotrainer/core/event/event_manager_plugin.py +45 -0
- autotrainer/core/event/file_event_plugin.py +91 -0
- autotrainer/core/event/logger_event_plugin.py +43 -0
- autotrainer/core/fixed_array_multiqueue.py +263 -0
- autotrainer/core/fixed_array_queue.py +124 -0
- autotrainer/core/frame_index.py +34 -0
- autotrainer/core/interfaces.py +347 -0
- autotrainer/core/logging.py +714 -0
- autotrainer/core/message/__init__.py +26 -0
- autotrainer/core/message/audio_spectrum_message.py +21 -0
- autotrainer/core/message/measurement_message.py +61 -0
- autotrainer/core/message/message_handler.py +158 -0
- autotrainer/core/message/motor_configuration_message.py +128 -0
- autotrainer/core/message/system_command_kind.py +74 -0
- autotrainer/core/message/system_message_handler.py +122 -0
- autotrainer/core/message/system_status_message.py +112 -0
- autotrainer/core/multiproc.py +50 -0
- autotrainer/core/notification/__init__.py +2 -0
- autotrainer/core/notification/notification_center.py +80 -0
- autotrainer/core/notification/trigger_notification.py +27 -0
- autotrainer/core/observable_object.py +105 -0
- autotrainer/core/perf_monitor.py +77 -0
- autotrainer/core/pose_elements.py +196 -0
- autotrainer/core/project/__init__.py +19 -0
- autotrainer/core/project/project_info.py +486 -0
- autotrainer/core/queue_util.py +60 -0
- autotrainer/core/reach_event.py +38 -0
- 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
|