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,296 @@
1
+ import time
2
+ from threading import Thread
3
+ from datetime import datetime
4
+ from queue import Queue, Empty
5
+ from typing import Optional, List, Any
6
+
7
+ from autotrainer.core.logging import get_verbose_logger
8
+ from autotrainer.core.project import ProjectInfo
9
+
10
+ from .event_info import EventInfo
11
+ from .event_manager_plugin import EventManagerPlugin
12
+ from .file_event_plugin import FileEventPlugin
13
+ from .logger_event_plugin import LoggerEventPlugin
14
+
15
+ logger = get_verbose_logger(__name__)
16
+
17
+
18
+ class EventManager:
19
+ """
20
+ Events, in the context of the autotrainer local application, are a set of messages that track specific changes and
21
+ actions of interest. They are a subset of information that is likely to be logged overall, and are made to conform
22
+ to a specific structure for reading in conjunction with data files during analysis.
23
+
24
+ By default, events are also sent to the default logger. It is generally not necessary to post an event _and_ send
25
+ the same information explicitly to the logger.
26
+
27
+ Repeat behavior: The objective with repeat events (same event without interruption of another event type) is to
28
+ * Immediately output the first instance of the event (as would normally happen)
29
+ * When a new event type is received, output the number of repeats (not counting that first instance already output)
30
+ * Per the first bullet, this new event type will also be immediately output
31
+ """
32
+
33
+ _instance: "EventManager"
34
+
35
+ @staticmethod
36
+ def _remove_cls_instance():
37
+ try:
38
+ del EventManager._instance
39
+ except AttributeError:
40
+ pass
41
+
42
+ @staticmethod
43
+ def default() -> "EventManager":
44
+ """
45
+ Creates (if needed) and returns the default instance.
46
+ """
47
+ cls = EventManager
48
+ if not hasattr(cls, "_instance"):
49
+ instance = cls("EventManagerInstance")
50
+ instance.register_plugin(LoggerEventPlugin())
51
+ instance.register_plugin(FileEventPlugin())
52
+ cls._instance = instance
53
+ return cls._instance
54
+
55
+ @staticmethod
56
+ def try_close_default():
57
+ """
58
+ Close the default instance if it exists. Do not spin one up just to check (e.g.,
59
+ EventManager.default().close() if one was never created).
60
+ """
61
+ cls = EventManager
62
+ cls_inst = getattr(cls, "_instance", None)
63
+ if cls_inst is not None:
64
+ cls_inst.close()
65
+ cls._remove_cls_instance()
66
+
67
+ def __del__(self):
68
+ # in case of
69
+ cls_inst = getattr(EventManager, "_instance", None)
70
+ if cls_inst is self:
71
+ self.try_close_default()
72
+ else:
73
+ self.close()
74
+
75
+ def __init__(self, key=""):
76
+ if key != "EventManagerInstance":
77
+ raise Exception("Use EventManager.default() to access and instance.")
78
+
79
+ self._plugins: List[EventManagerPlugin] = []
80
+
81
+ self._project_info = None
82
+
83
+ self._last_event_info: Optional[EventInfo] = None
84
+ self._repeat_event_count = 0
85
+
86
+ # Callers should expect requests to post an event return as quickly as possible. Events are pushed to a queue
87
+ # so that processing can be done in a separate thread as resources allow.
88
+ self._write_queue = Queue()
89
+ self._write_thread = Thread(
90
+ target=self._process_queue,
91
+ name=f"{self.__class__.__name__}",
92
+ daemon=True, # allow the main thread/process to exit even if this thread is still alive
93
+ )
94
+ self._write_thread.start()
95
+
96
+ @property
97
+ def is_valid(self):
98
+ return self._write_thread is not None and self._write_queue is not None
99
+
100
+ @property
101
+ def project(self) -> ProjectInfo:
102
+ return self._project_info
103
+
104
+ @project.setter
105
+ def project(self, value: ProjectInfo) -> None:
106
+ """
107
+ ProjectInfo is an optional property. If set, it is used to generate the location and name of the event file in
108
+ the expected format.
109
+
110
+ Args
111
+ value: ProjectInfo object. This is used to determine the location of the event file.
112
+ """
113
+ self._project_info = value
114
+
115
+ for plugin in self._plugins:
116
+ plugin.set_project(value)
117
+
118
+ @property
119
+ def plugins(self) -> List[EventManagerPlugin]:
120
+ # Do not let callers modify ths list. register/unregister are available for this. This is already dangerous
121
+ # enough - giving them access to the plugins themselves.
122
+ return self._plugins.copy()
123
+
124
+ def register_plugin(self, plugin: EventManagerPlugin) -> None:
125
+ """
126
+ Registers a plugin with the event manager.
127
+
128
+ Args:
129
+ plugin: The plugin to register.
130
+ """
131
+ if plugin not in self._plugins:
132
+ self._plugins.append(plugin)
133
+ plugin.set_project(self.project)
134
+
135
+ def unregister_plugin(self, plugin: EventManagerPlugin) -> None:
136
+ """
137
+ Unregisters a plugin with the event manager.
138
+
139
+ Args:
140
+ plugin: The plugin to unregister.
141
+ """
142
+ if plugin in self._plugins:
143
+ self._plugins.remove(plugin)
144
+ # Even though the caller must have a reference to the plugin, take responsibility to close, if needed.
145
+ # In the future, there may be a way to unregister by some kind of key/type/identifier that doesn't require
146
+ # explicit access to the plugin instance by the caller.
147
+ plugin.close()
148
+
149
+ def flush(self):
150
+ for plugin in self._plugins:
151
+ plugin.flush()
152
+
153
+ def close(self):
154
+ """
155
+ Closes the event manager. This is required to stop any internal threads and allow a clean exit. This should
156
+ only be called when the application or script is closing or otherwise finished with the event manager as an
157
+ instance, including the `default` cannot be restarted.
158
+ """
159
+ cls_inst = getattr(EventManager, "_instance", None)
160
+ for plugin in self._plugins:
161
+ plugin.set_enable(False)
162
+ wt = self._write_thread
163
+ wq = self._write_queue
164
+ if wt is not None:
165
+ if wq is not None:
166
+ wq.put(None)
167
+ wt.join()
168
+ self._write_thread = None
169
+ # queue needs be flushed so that we can join it:
170
+ if wq is not None:
171
+ self._write_queue = None # set it directly, so that no other thread can now put through this instance
172
+ while True:
173
+ try:
174
+ item = wq.get_nowait()
175
+ logger.warning("dropped unhandled %s: %s", type(item), item)
176
+ wq.task_done()
177
+ except Empty:
178
+ break
179
+ wq.join()
180
+ if cls_inst is self:
181
+ self._remove_cls_instance()
182
+
183
+ def post_event_content(self, kind: int, data: Optional[Any] = None, when: Optional[datetime] = None,
184
+ index: int = None):
185
+ """
186
+ Add an event info instance to the event manager output queue. This is a convenience method that creates an
187
+ EventInfo object and optionally populates timestamp related fields as the time this method is called. If timing
188
+ information is critical, those arguments should be explicitly set, or `post_event()` should be used with a
189
+ preconstructed `EventInfo` instance with the desired timestamp fields.
190
+
191
+ Args:
192
+ kind: See EventInfo.kind for a detailed description.
193
+ data: See EventInfo.kind for a detailed description.
194
+ when: See EventInfo.kind for a detailed description.
195
+ index: See EventInfo.kind for a detailed description.
196
+
197
+ """
198
+ info = EventInfo(kind,
199
+ when=datetime.now() if when is None else when,
200
+ index=time.perf_counter_ns() if index is None else index,
201
+ context=data)
202
+
203
+ self.post_event(info)
204
+
205
+ def post_event(self, info: EventInfo):
206
+ """
207
+ Posts an event info instance to the event manager.
208
+
209
+ Args:
210
+ info:
211
+
212
+ Returns:
213
+
214
+ """
215
+ if info is None:
216
+ # "~paranoid" check but that will prevent the non-desired stop of the work thread.
217
+ raise RuntimeError("post_event(None) refused")
218
+ wq = self._write_queue
219
+ if wq is None:
220
+ logger.debug("post_event(%s) but write queue already removed", info.kind)
221
+ else:
222
+ wq.put(info)
223
+
224
+ def has_pending(self) -> bool:
225
+ """
226
+ This is primarily for testing and diagnostics. It should not be relied upon absolutely as this class will not
227
+ guarantee that whatever implementation is used to queue events for processing will accurately report the state
228
+ of empty at all times.
229
+
230
+ Returns: True if there are pending events to process by the handlers. Might be accurate, might not.
231
+ """
232
+ wq = self._write_queue
233
+ if wq is None:
234
+ return False
235
+ return not wq.empty()
236
+
237
+ def _process_queue(self):
238
+ # Because we a) use plugins and b) allow for EventInfo->is_same to be overridden, we may be given a plugin that
239
+ # errors on every process_event, or an EventInfo that errors on every is_same. This would bury the log if we
240
+ # reported it every time. It may also be a one time error for the plugin, so we don't want to just skip/remove
241
+ # if there is an error.
242
+ # This will log the first occurrence of each of the above, but not spam the log if it is a recurring issue.
243
+
244
+ is_same_error_reported = False
245
+ process_event_error_reported = False
246
+
247
+ while True:
248
+ try:
249
+ # Workaround or current Jetson behavior w/ queue.get(timeout=).
250
+ info = self._write_queue.get(timeout=0.5)
251
+ if info is None:
252
+ logger.verbose("got exit sentinel, exiting main loop")
253
+ self._write_queue.task_done()
254
+ break
255
+ except Empty:
256
+ continue
257
+
258
+ if not isinstance(info, EventInfo):
259
+ logger.warning("unexpected event info instance")
260
+ self._write_queue.task_done()
261
+ continue
262
+
263
+ try:
264
+ is_same = info.is_same(self._last_event_info)
265
+ except Exception as err: # Possibly coming from EventInfo subclass - cannot predict type of error.
266
+ if not is_same_error_reported:
267
+ logger.error("is_same failed: %s", err)
268
+ is_same_error_reported = True
269
+ else:
270
+ if is_same:
271
+ self._repeat_event_count += 1
272
+ self._write_queue.task_done()
273
+ continue
274
+
275
+ try:
276
+ if self._repeat_event_count > 0:
277
+ self._process_event(self._last_event_info, self._repeat_event_count)
278
+ self._repeat_event_count = 0
279
+
280
+ self._last_event_info = info
281
+ self._process_event(info)
282
+ except Exception as err: # Coming from an arbitrary plugin process_event() - cannot predict type of error.
283
+ # TODO (maybe): track exceptions per plugin. After some number N exceptions, disable the plugin.
284
+ if not process_event_error_reported:
285
+ logger.exception("process queue info (%s) failed: %s", info, err)
286
+ process_event_error_reported = True
287
+
288
+ self._write_queue.task_done()
289
+
290
+ for plugin in self._plugins:
291
+ plugin.close()
292
+
293
+ def _process_event(self, info: EventInfo, repeat_count: int = 0):
294
+ for plugin in self._plugins:
295
+ logger.spam("plugin %s: processing event %s", plugin, info)
296
+ plugin.process_event(info, repeat_count)
@@ -0,0 +1,45 @@
1
+ from typing import Protocol, Optional
2
+
3
+ from ..project import ProjectInfo
4
+ from .event_info import EventInfo
5
+
6
+
7
+ class EventManagerPlugin(Protocol):
8
+ """
9
+ Interface for event manager plugins.
10
+ """
11
+
12
+ def set_project(self, project: Optional[ProjectInfo]) -> None:
13
+ """
14
+ Called when the project changes.
15
+
16
+ Args:
17
+ project: The new project info (or none).
18
+ """
19
+
20
+ def set_enable(self, enable: bool) -> None:
21
+ """
22
+ Called when the event manager is enabled or disabled.
23
+
24
+ Args:
25
+ enable: True if the event manager is enabled, false otherwise.
26
+ """
27
+
28
+ def process_event(self, info: EventInfo, repeat_count: int) -> None:
29
+ """
30
+ Called when an event occurs.
31
+
32
+ Args:
33
+ info: The event info.
34
+ repeat_count: Number of times this event has been repeated until a different event was received.
35
+ """
36
+
37
+ def flush(self):
38
+ """
39
+ For event sinks that may buffer output, this should forcibly flush any pending output.
40
+ """
41
+
42
+ def close(self) -> None:
43
+ """
44
+ Called when the event manager is closed. Any pending output should be flushed, resources released, etc.
45
+ """
@@ -0,0 +1,91 @@
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from ..logging import get_verbose_logger
6
+ from ..project import ProjectInfo, ProjectInterval
7
+ from .event_info import EventInfo
8
+
9
+ from .event_manager_plugin import EventManagerPlugin
10
+
11
+ logger = get_verbose_logger(__name__)
12
+
13
+
14
+ class FileEventPlugin(EventManagerPlugin):
15
+ """
16
+ An event manager plugin directs events to a project file. This plugin will nore record an event history if
17
+ `project` is set to `None`.
18
+ """
19
+
20
+ def __init__(self):
21
+ self._project_info = None
22
+ self._event_file = None
23
+ self._write_active = True
24
+ self._current_record_interval = -1
25
+ self._bad_file_attempt = False
26
+
27
+ def set_project(self, project: Optional[ProjectInfo]) -> None:
28
+ self._project_info = project
29
+ self._bad_file_attempt = False
30
+ self._update_event_file()
31
+
32
+ def set_enable(self, enable: bool) -> None:
33
+ self._write_active = enable
34
+
35
+ def process_event(self, info: EventInfo, repeat_count: int):
36
+ if not self._write_active:
37
+ logger.debug("write not active, skipping %s", info)
38
+ return
39
+ file_timestamp = datetime.now()
40
+ needs_update = file_timestamp.hour != self._current_record_interval
41
+ if needs_update:
42
+ self._update_event_file()
43
+ event_file = self._event_file
44
+ if event_file is None:
45
+ logger.warning("event_file None, skipping %s", info)
46
+ return
47
+ output = f"{info.when}, {info.index}, {info.kind}, {str(info.kind)}, {str(info.context)}, {repeat_count}\n"
48
+ event_file.write(output)
49
+
50
+ def flush(self):
51
+ if self._event_file is not None:
52
+ self._event_file.flush()
53
+
54
+ def close(self) -> None:
55
+ event_file = self._event_file
56
+ if event_file is not None:
57
+ event_file.close()
58
+ self._event_file = None
59
+
60
+ def _update_event_file(self):
61
+ self.close()
62
+
63
+ if not self._write_active:
64
+ return
65
+
66
+ if self._project_info is not None:
67
+ event_file_info = self._project_info.get_monitor_file(name="events", interval=ProjectInterval.HOUR,
68
+ when=datetime.now())
69
+ if event_file_info is None:
70
+ logger.error("unable to write to expected event file location")
71
+ return
72
+
73
+ try:
74
+ file_path = Path(event_file_info.file)
75
+ file_existed = file_path.exists()
76
+ file_path.parent.mkdir(exist_ok=True)
77
+ fh = file_path.open("a")
78
+ if not file_existed:
79
+ fh.write("Time, Index, EventId, EventName, Data, Repeat\n")
80
+
81
+ self._current_record_interval = event_file_info.current_interval
82
+ self._event_file = fh
83
+
84
+ logger.info(f"event file opened at {event_file_info.file}")
85
+ except Exception as ex:
86
+ if not self._bad_file_attempt:
87
+ # Don't spam log if there are write/access issues with wherever project info is pointing. Can reset
88
+ # the flag the next tile the project changes.
89
+ logger.error(f"unable to write to {event_file_info.file}")
90
+ logger.error(ex)
91
+ self._bad_file_attempt = True
@@ -0,0 +1,43 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from ..logging import get_verbose_logger
5
+ from ..project import ProjectInfo
6
+ from .event_info import EventInfo
7
+
8
+ from .event_manager_plugin import EventManagerPlugin
9
+
10
+ logger = get_verbose_logger(__name__)
11
+
12
+
13
+ class LoggerEventPlugin(EventManagerPlugin):
14
+ """
15
+ An EventManager plugin that sends event to the default logger facility.
16
+ """
17
+
18
+ def __init__(self, level: int = logging.DEBUG):
19
+ self._level = level
20
+
21
+ @property
22
+ def level(self) -> int:
23
+ return self._level
24
+
25
+ @level.setter
26
+ def level(self, value: int) -> None:
27
+ self._level = value
28
+
29
+ def set_project(self, project: Optional[ProjectInfo]) -> None:
30
+ pass
31
+
32
+ def set_enable(self, enable: bool) -> None:
33
+ pass
34
+
35
+ def process_event(self, info: EventInfo, repeat_count: int) -> None:
36
+ output = f"{info.when}, {info.index}, {info.kind}, {str(info.kind)}, {str(info.context)}, {repeat_count}"
37
+ logger.log(self._level, output)
38
+
39
+ def flush(self) -> None:
40
+ pass
41
+
42
+ def close(self) -> None:
43
+ pass