auto-trainer-core 2.0.1.dev353__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.dev353.dist-info/METADATA +18 -0
- auto_trainer_core-2.0.1.dev353.dist-info/RECORD +73 -0
- auto_trainer_core-2.0.1.dev353.dist-info/WHEEL +5 -0
- auto_trainer_core-2.0.1.dev353.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,112 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SystemStatusMessageKind(IntEnum):
|
|
6
|
+
"""
|
|
7
|
+
This class defines the messages types that are sent by the device. If there is a request/command that generates
|
|
8
|
+
a response, the command will be defined in the SystemCommandKind class.
|
|
9
|
+
|
|
10
|
+
A message type is sent to the caller with an optional context/data value. This value is dependent on the message
|
|
11
|
+
type.
|
|
12
|
+
|
|
13
|
+
This is implemented and represented in the send_message() method in the DeviceApi class.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
ACKNOWLEDGE = 1
|
|
17
|
+
"""
|
|
18
|
+
This message should be send when a command completes along with the context that is provided with the command. It
|
|
19
|
+
should not be sent with status messages that are generated independently by the hardware. The is the mechanism by
|
|
20
|
+
which callers determine if and when specific commands are completed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
FIRMWARE_VERSION = 101
|
|
24
|
+
"""Message will contain the firmware version of the device as a string."""
|
|
25
|
+
# TODO: This needs to be more complex than a string now that different modules are part of the same device and
|
|
26
|
+
# connection.
|
|
27
|
+
|
|
28
|
+
PELLET_MOTOR_X = 201
|
|
29
|
+
"""Message will contain the pellet X motor status as a StepperStatusMessage."""
|
|
30
|
+
|
|
31
|
+
PELLET_MOTOR_Y = 202
|
|
32
|
+
"""Message will contain the pellet Y motor status as a StepperStatusMessage."""
|
|
33
|
+
|
|
34
|
+
PELLET_MOTOR_Z = 203
|
|
35
|
+
"""Message will contain the pellet Z motor status as a StepperStatusMessage."""
|
|
36
|
+
|
|
37
|
+
PELLET_LOAD = 204
|
|
38
|
+
"""Message will contain the pellet load arm servo status as a ServoStatusMessage."""
|
|
39
|
+
|
|
40
|
+
PELLET_COVER = 205
|
|
41
|
+
"""Message will contain the pellet cover arm servo status as a ServoStatusMessage."""
|
|
42
|
+
|
|
43
|
+
HEAD_MAGNET = 206
|
|
44
|
+
"""Message will contain the head fixation magnet servo status as a ServoStatusMessage."""
|
|
45
|
+
|
|
46
|
+
TUNNEL_GATE_SERVO = 207
|
|
47
|
+
"""Message will contain the tunnel gate servo status as a ServoStatusMessage."""
|
|
48
|
+
|
|
49
|
+
PELLET_X = 208
|
|
50
|
+
"""Message will contain the pellet X motor position as a float."""
|
|
51
|
+
|
|
52
|
+
PELLET_Y = 209
|
|
53
|
+
"""Message will contain the pellet Y motor position as a float."""
|
|
54
|
+
|
|
55
|
+
PELLET_Z = 210
|
|
56
|
+
"""Message will contain the pellet Z motor position as a float."""
|
|
57
|
+
|
|
58
|
+
FRONT_DOOR = 301
|
|
59
|
+
"""Message will contain front door status as True (open) or False (closed)."""
|
|
60
|
+
|
|
61
|
+
DRAWER_DOOR = 302
|
|
62
|
+
"""Message will contain drawer door status as True (open) or False (closed)."""
|
|
63
|
+
|
|
64
|
+
SPARE_DOOR = 303
|
|
65
|
+
"""Message will contain spare door status as True (open) or False (closed)."""
|
|
66
|
+
|
|
67
|
+
EXT_BUTTON = 304
|
|
68
|
+
"""Message will contain external button status as True (pressed) or False (released)"""
|
|
69
|
+
|
|
70
|
+
STIMULUS_INPUTS = 305
|
|
71
|
+
"""Message will contain a list of 4 states as True/False"""
|
|
72
|
+
|
|
73
|
+
TUNNEL_FAN = 220
|
|
74
|
+
"""Tunnel Fan will contain the tunnel fan status as a ServoStatusMessage"""
|
|
75
|
+
|
|
76
|
+
MEASUREMENTS = 401
|
|
77
|
+
"""Message will contain a list of MeasurementMessage objects."""
|
|
78
|
+
|
|
79
|
+
AUDIO_SPECTRUM = 402
|
|
80
|
+
"""Message will contain the audio spectrum data as an array list of float values."""
|
|
81
|
+
|
|
82
|
+
MOTOR_CONFIGURATION = 501
|
|
83
|
+
|
|
84
|
+
CAMERA_STATUS_CHANGE = 1001
|
|
85
|
+
|
|
86
|
+
MEASUREMENT = -101
|
|
87
|
+
"""This value is deprecated. Use MEASUREMENTS if the object you are passing with this identifier conforms to the
|
|
88
|
+
MeasurementMessage protocol."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class StepperStatusMessage(Protocol):
|
|
92
|
+
@property
|
|
93
|
+
def position(self) -> float:
|
|
94
|
+
"""Current motor position in mm."""
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def send_position(self) -> float:
|
|
98
|
+
"""Current motor delivery position in mm."""
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_at_limit(self) -> bool:
|
|
102
|
+
"""True if the limit switch has been hit, otherwise False."""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ServoStatusMessage(Protocol):
|
|
106
|
+
@property
|
|
107
|
+
def location(self) -> float:
|
|
108
|
+
"""Current servo position in degrees."""
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def status(self) -> int:
|
|
112
|
+
"""Current servo status value."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging.config
|
|
2
|
+
import multiprocessing
|
|
3
|
+
import signal
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_mp_ctx():
|
|
8
|
+
return multiprocessing.get_context("spawn")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EmptyWithContext:
|
|
12
|
+
|
|
13
|
+
def __enter__(self):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DaemonTimer(threading.Timer):
|
|
21
|
+
"""A Timer that does not block main process exit"""
|
|
22
|
+
|
|
23
|
+
finished: threading.Event
|
|
24
|
+
|
|
25
|
+
def __init__(self, delay, func, args=None, kwargs=None):
|
|
26
|
+
super().__init__(delay, func, args=args, kwargs=kwargs)
|
|
27
|
+
self.daemon = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make_daemon_timer(delay, func, *args, **kwargs):
|
|
31
|
+
return DaemonTimer(delay, func, *args, **kwargs)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pool_init(log_dict_cfg=None):
|
|
35
|
+
"""For process pool"""
|
|
36
|
+
# this is to prevent keyboard interrupted being delivered to all child processes of the main process:
|
|
37
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
38
|
+
# given by default this is what is done.
|
|
39
|
+
# inner-import on purpose: prevent import loop:
|
|
40
|
+
from autotrainer.core.logging import setup_logging, install_log_exception_hook
|
|
41
|
+
if log_dict_cfg is None:
|
|
42
|
+
setup_logging()
|
|
43
|
+
else:
|
|
44
|
+
logging.config.dictConfig(log_dict_cfg)
|
|
45
|
+
install_log_exception_hook()
|
|
46
|
+
logging.root.info("Initialized pool worker")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
no_op_timer = make_daemon_timer(0, lambda: None)
|
|
50
|
+
no_op_timer.finished.set()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Callable, Optional, Dict, List, TypeVar
|
|
3
|
+
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Notification:
|
|
9
|
+
"""
|
|
10
|
+
A class representing a notification with a type and optional source and context objects.
|
|
11
|
+
"""
|
|
12
|
+
event_type: Any
|
|
13
|
+
"""
|
|
14
|
+
The type of the event. This is used to identify the event and determine which observers should be notified.
|
|
15
|
+
"""
|
|
16
|
+
source: Optional[Any] = None
|
|
17
|
+
"""
|
|
18
|
+
The source of the event. This is typically the object that generated the event.
|
|
19
|
+
"""
|
|
20
|
+
context: Optional[Any] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
TCallable = TypeVar("TCallable", bound=Callable[[Notification], None])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NotificationCenter:
|
|
27
|
+
"""
|
|
28
|
+
A publish and subscribe implementation for general message passing where publishers can remain unaware of if there
|
|
29
|
+
are listeners and who they are.
|
|
30
|
+
|
|
31
|
+
Each process has a default notification center. Additional instances may be created to organize notifications in
|
|
32
|
+
specific contexts.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def default_center(cls) -> Self:
|
|
37
|
+
"""
|
|
38
|
+
Returns the default instance of NotificationCenter.
|
|
39
|
+
"""
|
|
40
|
+
if not hasattr(cls, "_default_center"):
|
|
41
|
+
cls._default_center = cls()
|
|
42
|
+
|
|
43
|
+
return cls._default_center
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
self._subscribers: Dict[str, List[TCallable]] = {}
|
|
47
|
+
|
|
48
|
+
def add_observer(self, event_type: Any, callback: TCallable):
|
|
49
|
+
"""
|
|
50
|
+
Subscribe to a specific event type with a callback function.
|
|
51
|
+
"""
|
|
52
|
+
if event_type not in self._subscribers:
|
|
53
|
+
self._subscribers[event_type] = []
|
|
54
|
+
|
|
55
|
+
if callback not in self._subscribers[event_type]:
|
|
56
|
+
self._subscribers[event_type].append(callback)
|
|
57
|
+
|
|
58
|
+
def remove_observer(self, event_type: Any, callback: TCallable):
|
|
59
|
+
"""
|
|
60
|
+
Unsubscribe from a specific event type.
|
|
61
|
+
"""
|
|
62
|
+
if event_type in self._subscribers:
|
|
63
|
+
if callback in self._subscribers[event_type]:
|
|
64
|
+
self._subscribers[event_type].remove(callback)
|
|
65
|
+
|
|
66
|
+
def post_notification(self, **kwargs):
|
|
67
|
+
"""
|
|
68
|
+
Notify all subscribers of a specific event type.
|
|
69
|
+
"""
|
|
70
|
+
if "notification" in kwargs:
|
|
71
|
+
notification = kwargs["notification"]
|
|
72
|
+
assert isinstance(notification, Notification), "notification must be of type Notification"
|
|
73
|
+
else:
|
|
74
|
+
event_type = kwargs["event_type"]
|
|
75
|
+
assert event_type is not None, "event_type must not be None"
|
|
76
|
+
notification = Notification(event_type, kwargs.get("source"), kwargs.get("context"))
|
|
77
|
+
|
|
78
|
+
if notification.event_type in self._subscribers:
|
|
79
|
+
for callback in self._subscribers[notification.event_type]:
|
|
80
|
+
callback(notification)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from .notification_center import NotificationCenter, Notification
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TriggerNotification(Enum):
|
|
8
|
+
CAPTURE_ID = "TriggerNotificationCaptureId"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Convenience methods
|
|
12
|
+
|
|
13
|
+
def post_trigger_enable(sender: Any, enabled: bool):
|
|
14
|
+
"""
|
|
15
|
+
Post a trigger enable notification.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
sender: The sender of the notification.
|
|
19
|
+
enabled: Whether the trigger is enabled or not.
|
|
20
|
+
"""
|
|
21
|
+
NotificationCenter.default_center().post_notification(
|
|
22
|
+
notification=Notification(
|
|
23
|
+
event_type=TriggerNotification.CAPTURE_ID,
|
|
24
|
+
source=sender,
|
|
25
|
+
context=enabled
|
|
26
|
+
)
|
|
27
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Protocol, Callable, Any, TypeVar
|
|
3
|
+
from typing_extensions import Self
|
|
4
|
+
|
|
5
|
+
import verboselogs
|
|
6
|
+
from events import Events
|
|
7
|
+
|
|
8
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
9
|
+
|
|
10
|
+
logger = get_verbose_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_debug_properties_change = {
|
|
14
|
+
v.strip() for v in os.getenv("AUTOTRAINER_DEBUG_PROPERTIES", "").split(",")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
HandlerT = TypeVar("HandlerT", bound=Callable[..., None])
|
|
19
|
+
|
|
20
|
+
AnyNewValue = Any
|
|
21
|
+
AnyOldValue = Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EventHandler(Protocol[HandlerT]):
|
|
25
|
+
"""Single event handler"""
|
|
26
|
+
|
|
27
|
+
__call__: HandlerT
|
|
28
|
+
|
|
29
|
+
def __iadd__(self, handler: HandlerT) -> Self:
|
|
30
|
+
"""Register event handler"""
|
|
31
|
+
|
|
32
|
+
def __isub__(self, handler: HandlerT) -> Self:
|
|
33
|
+
"""Unregister event handler"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ObservableObjectProtocol(Protocol):
|
|
37
|
+
"""Allow classes that do not inherit from ObservableObject to indicate they support the property_changed event."""
|
|
38
|
+
|
|
39
|
+
property_changed: EventHandler[Callable[[str, AnyNewValue, AnyOldValue], None]]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ObservableObject(Events):
|
|
43
|
+
"""
|
|
44
|
+
Defines a class with observable property change events. This is a common pattern for UI frameworks in
|
|
45
|
+
particular where some form of a view layer and some type of model layer need to communicate changes in a decoupled
|
|
46
|
+
way. However, it is applicable in many other situations as well.
|
|
47
|
+
|
|
48
|
+
The property_changed event is used by one or more listeners to respond to property changes.
|
|
49
|
+
|
|
50
|
+
_on_property_changed is a convenience method that will only generate an event if the new value does not pass
|
|
51
|
+
the == test with the old value. This prevents needless events and more importantly endless recursion when two
|
|
52
|
+
objects are listening to each other to stay in sync (e.g., UI and model where a change may originate in either).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# type hint for dynamic event function (added by Events):
|
|
56
|
+
property_changed: EventHandler[Callable[[str, AnyNewValue, AnyOldValue], None]]
|
|
57
|
+
|
|
58
|
+
def __init__(self, event_names=()):
|
|
59
|
+
super().__init__(event_names + ("property_changed",))
|
|
60
|
+
|
|
61
|
+
# def __bool__(self):
|
|
62
|
+
# # NB: was used to make the `if model:` in Machine.__init__ method works,
|
|
63
|
+
# # when tried mixing an ObservableObject with transitions.Machine
|
|
64
|
+
# # but there were other issues too, so keeping commented, as ref. at worst.
|
|
65
|
+
# # NB2: this is because the __bool__ method is otherwise overridden by the Events.__len__ method
|
|
66
|
+
# # which returns the nbr of events handled by the given instance.
|
|
67
|
+
# return True
|
|
68
|
+
|
|
69
|
+
def _on_property_changed(self, name: str, new_value: AnyNewValue, old_value: AnyOldValue):
|
|
70
|
+
"""
|
|
71
|
+
Generate a property-changed event if the new value does not pass the == test with the old value.
|
|
72
|
+
|
|
73
|
+
This is a convenience method for only generating an event when the value has actually changed (as defined by
|
|
74
|
+
`==`). If `==` is not an appropriate test for a given property, a subclass should override this method for
|
|
75
|
+
handling that property, or the caller should use the property_changed event directly.
|
|
76
|
+
|
|
77
|
+
A common pattern for subclasses would be:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
def set_age(self, value: int):
|
|
81
|
+
prev, self._age = self._age, value # assign the value before call the _on_property_changed event
|
|
82
|
+
self._on_property_changed("age", value, prev)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This will only generate an event if the new value is different from the old value and update the member variable
|
|
86
|
+
with the new value if it changed.
|
|
87
|
+
|
|
88
|
+
An important note for event handlers is that the property on the object will be the old value at the time the
|
|
89
|
+
event is handled. Handlers must use the `new_value` argument to get the new value.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
name (str): Name of the property that changed.
|
|
93
|
+
new_value: New value of the property.
|
|
94
|
+
old_value: Old/current value of the property.
|
|
95
|
+
"""
|
|
96
|
+
if old_value == new_value:
|
|
97
|
+
return old_value
|
|
98
|
+
|
|
99
|
+
if __debug__:
|
|
100
|
+
if name in _debug_properties_change:
|
|
101
|
+
logger.debug("%s: property %r: from %s to %s", self, name, old_value, new_value,
|
|
102
|
+
stack_info=logger.level == verboselogs.SPAM)
|
|
103
|
+
|
|
104
|
+
self.property_changed(name, new_value, old_value)
|
|
105
|
+
return new_value
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from math import floor
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PerfMonitor:
|
|
12
|
+
"""
|
|
13
|
+
Convenience class to measure and report performance metrics. There are likely more robust or feature complete
|
|
14
|
+
packages available. This is intended as a simple implementation to not add to the dependency list of the project.
|
|
15
|
+
|
|
16
|
+
The monitor tracks `cycles` or number of times something happens. What this means is defined by the caller.
|
|
17
|
+
"""
|
|
18
|
+
name: str = ""
|
|
19
|
+
"""User-friendly name for the performance monitor."""
|
|
20
|
+
units: str = ""
|
|
21
|
+
"""Units for the performance monitor."""
|
|
22
|
+
report_window: int = 15
|
|
23
|
+
"""Duration between each report"""
|
|
24
|
+
enable_log: bool = True
|
|
25
|
+
"""Allow toggling reporting while running."""
|
|
26
|
+
log_level: int = logging.DEBUG
|
|
27
|
+
"""Log level for reporting performance metrics."""
|
|
28
|
+
cps: float = 0.0
|
|
29
|
+
"""Most recent cycles per second (CPS) value."""
|
|
30
|
+
|
|
31
|
+
_cycle_count: Optional[int] = field(default=None, init=False, repr=False, compare=False)
|
|
32
|
+
_start: float = field(default=0, init=False, repr=False, compare=False)
|
|
33
|
+
_next_refresh: float = field(default=0, init=False, repr=False, compare=False)
|
|
34
|
+
|
|
35
|
+
def reset(self):
|
|
36
|
+
"""
|
|
37
|
+
Resets all cycle counts and timing. Calculations will resume on the next `add_cycle` or `add_cycles` call.
|
|
38
|
+
"""
|
|
39
|
+
logger.debug("%s: reset", self.name)
|
|
40
|
+
self._cycle_count = None
|
|
41
|
+
|
|
42
|
+
def add_cycle(self) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Add a single cycle to the count.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
bool: True if the CPS value was reported. False otherwise.
|
|
48
|
+
"""
|
|
49
|
+
return self.add_cycles(1)
|
|
50
|
+
|
|
51
|
+
def add_cycles(self, cycles: int) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Add a block of cycles to the count.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
bool: True if the CPS value was reported. False otherwise.
|
|
57
|
+
"""
|
|
58
|
+
if cycles <= 0:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
t_perf_now = time.perf_counter()
|
|
62
|
+
|
|
63
|
+
if self._cycle_count is None:
|
|
64
|
+
self._cycle_count = 0
|
|
65
|
+
self._next_refresh = self._start = t_perf_now
|
|
66
|
+
self._next_refresh += self.report_window
|
|
67
|
+
|
|
68
|
+
elif t_perf_now > self._next_refresh:
|
|
69
|
+
self.cps = self._cycle_count / (t_perf_now - self._next_refresh + self.report_window)
|
|
70
|
+
if self.enable_log:
|
|
71
|
+
logger.log(self.log_level, f"{self.name}: {self.cps:.1f} {self.units}")
|
|
72
|
+
self._next_refresh += self.report_window
|
|
73
|
+
self._cycle_count = cycles
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
self._cycle_count += cycles
|
|
77
|
+
return False
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
|
|
2
|
+
import dataclasses
|
|
3
|
+
import math
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
from autotrainer.core import get_perf_now
|
|
8
|
+
|
|
9
|
+
_cache_scene_elements: Dict[str, "_BaseSceneElement"] = {}
|
|
10
|
+
_cache_scene_elements_str: Dict[str, str] = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _BaseSceneElement(str): # , Enum):
|
|
14
|
+
"""Dedicated str subclass for scene elements,
|
|
15
|
+
main feature is to have the created elements be all cached/singleton,
|
|
16
|
+
allowing use of "is" operator comparison with previous reference to any of them.
|
|
17
|
+
"""
|
|
18
|
+
# we might want to use something more flexible, if the model would not use same names than here,
|
|
19
|
+
# like with a mapping dict, and some dynamic attributes + type hints on the class to still help development.
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def __init_subclass__(cls, **kwargs):
|
|
23
|
+
super().__init_subclass__(**kwargs)
|
|
24
|
+
# set all string values to SceneElement/cls instance,
|
|
25
|
+
# which pre-loads it in _cache_scene_elements:
|
|
26
|
+
for name, value in vars(cls).items():
|
|
27
|
+
if cls.__annotations__.get(name) != 'SceneElement' or not isinstance(value, str):
|
|
28
|
+
continue
|
|
29
|
+
setattr(cls, name, cls(value))
|
|
30
|
+
|
|
31
|
+
def __new__(cls, value: str):
|
|
32
|
+
cached = _cache_scene_elements.get(value, None)
|
|
33
|
+
if cached is None:
|
|
34
|
+
tentative = super().__new__(cls, value)
|
|
35
|
+
assert isinstance(tentative, _BaseSceneElement)
|
|
36
|
+
cached = _cache_scene_elements.setdefault(value, tentative) # threads-safe
|
|
37
|
+
# return the raw str value:
|
|
38
|
+
# otherwise, when used as column(multiindex or not) and in h5 files that will make
|
|
39
|
+
# the column to be saved with pickle,
|
|
40
|
+
# which when unpickled fails if the pickled object type has been moved meanwhile.
|
|
41
|
+
cached_str = _cache_scene_elements_str.setdefault(value, str(cached))
|
|
42
|
+
return cached_str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SceneElement(_BaseSceneElement):
|
|
46
|
+
|
|
47
|
+
Pellet: "SceneElement" = 'Pellet'
|
|
48
|
+
|
|
49
|
+
R_Hand: "SceneElement" = 'R_Hand' # composite element part
|
|
50
|
+
RH_flat: "SceneElement" = 'RH_flat'
|
|
51
|
+
RH_spread: "SceneElement" = 'RH_spread'
|
|
52
|
+
RH_grab: "SceneElement" = 'RH_grab'
|
|
53
|
+
|
|
54
|
+
L_Hand: "SceneElement" = 'L_Hand' # composite element part
|
|
55
|
+
LH_flat: "SceneElement" = 'LH_flat'
|
|
56
|
+
LH_spread: "SceneElement" = 'LH_spread'
|
|
57
|
+
LH_grab: "SceneElement" = 'LH_grab'
|
|
58
|
+
|
|
59
|
+
Nose: "SceneElement" = 'Nose'
|
|
60
|
+
Mouth: "SceneElement" = 'Mouth'
|
|
61
|
+
Tongue_mid: "SceneElement" = 'Tongue_mid'
|
|
62
|
+
Tongue_tip: "SceneElement" = 'Tongue_tip'
|
|
63
|
+
|
|
64
|
+
Star: "SceneElement" = 'Star'
|
|
65
|
+
Diamond: "SceneElement" = 'Diamond'
|
|
66
|
+
Triangle: "SceneElement" = 'Triangle'
|
|
67
|
+
|
|
68
|
+
AnyAnimalPart: "SceneElement" = 'AnyAnimalPart'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
AllHandsParts = {
|
|
72
|
+
SceneElement.RH_flat, SceneElement.RH_spread, SceneElement.RH_grab,
|
|
73
|
+
SceneElement.LH_flat, SceneElement.LH_spread, SceneElement.LH_grab,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
AllAnimalParts = {
|
|
78
|
+
SceneElement.Nose,
|
|
79
|
+
SceneElement.Mouth,
|
|
80
|
+
SceneElement.Tongue_tip,
|
|
81
|
+
SceneElement.Tongue_mid,
|
|
82
|
+
} | AllHandsParts
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
AllNonAnimalParts = {
|
|
86
|
+
SceneElement.Pellet,
|
|
87
|
+
SceneElement.Diamond,
|
|
88
|
+
SceneElement.Triangle,
|
|
89
|
+
SceneElement.Star,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
AllSceneParts = AllAnimalParts | AllNonAnimalParts
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclasses.dataclass
|
|
97
|
+
class ScenePartsPresenceContext:
|
|
98
|
+
"""Maintain a context of presence/absence status & age of parts/elements"""
|
|
99
|
+
|
|
100
|
+
# TODO: for now only considering presence in either of the 2 possible cameras,
|
|
101
|
+
# but we could handle 3 cases of presence/absence:
|
|
102
|
+
# 1) on any of the possible cams
|
|
103
|
+
# > can be/is used for checking pellet presence at the end of load sequence
|
|
104
|
+
# 2) on all of the possible cams simultaneously
|
|
105
|
+
# > should be used to report pellet successfully loaded (more reliably)
|
|
106
|
+
# 3) on all of the possible cams non-simultaneously
|
|
107
|
+
# > not entirely sure could be useful if we have already 1 + 2
|
|
108
|
+
|
|
109
|
+
last_perf_c: float = -math.inf # last perf counter received, basically the "freshness" of the data
|
|
110
|
+
|
|
111
|
+
present_last_perf_c: Dict[str, float] = dataclasses.field(default_factory=dict)
|
|
112
|
+
missing_last_perf_c: Dict[str, float] = dataclasses.field(default_factory=dict)
|
|
113
|
+
|
|
114
|
+
def get_part_seen(self, part: str) -> bool:
|
|
115
|
+
"""Say if the part was seen on last update"""
|
|
116
|
+
p_present = self.present_last_perf_c.get(part, -math.inf)
|
|
117
|
+
p_missing = self.missing_last_perf_c.get(part, -math.inf)
|
|
118
|
+
if math.isinf(p_present):
|
|
119
|
+
return p_present > 0
|
|
120
|
+
return p_present > p_missing
|
|
121
|
+
|
|
122
|
+
def get_presence_age(self, part: str, *, perf_now: Optional[float] = None) -> float:
|
|
123
|
+
"""Returns the "presence age" of the part if it's currently present, otherwise -math.inf"""
|
|
124
|
+
if perf_now is None:
|
|
125
|
+
perf_now = self.last_perf_c
|
|
126
|
+
presence_p = self.present_last_perf_c.get(part, None)
|
|
127
|
+
if presence_p is None:
|
|
128
|
+
return -math.inf
|
|
129
|
+
return perf_now - presence_p
|
|
130
|
+
|
|
131
|
+
def get_absence_age(self, part: str, *, perf_now: Optional[float] = None) -> float:
|
|
132
|
+
"""Returns the "absence age" of the part if it's currently absent, otherwise -math.inf.
|
|
133
|
+
Age can also be math.inf if never seen yet"""
|
|
134
|
+
if perf_now is None:
|
|
135
|
+
perf_now = self.last_perf_c
|
|
136
|
+
presence_p = self.present_last_perf_c.get(part, None)
|
|
137
|
+
absence_p = self.missing_last_perf_c.get(part, None)
|
|
138
|
+
if absence_p is None:
|
|
139
|
+
return math.inf if presence_p is None else -math.inf
|
|
140
|
+
if presence_p is not None and presence_p > absence_p:
|
|
141
|
+
return -math.inf
|
|
142
|
+
return perf_now - absence_p
|
|
143
|
+
|
|
144
|
+
def get_recently_seen(self, part: str, missing_delay: float, *, perf_now: Optional[float] = None) -> bool:
|
|
145
|
+
"""Returns if part was seen before missing_delay, relatively to perf_now, or self.last_perf_c"""
|
|
146
|
+
if perf_now is None:
|
|
147
|
+
perf_now = self.last_perf_c
|
|
148
|
+
prev_miss = self.missing_last_perf_c.get(part, -math.inf)
|
|
149
|
+
prev_pres = self.present_last_perf_c.get(part, -math.inf)
|
|
150
|
+
return (
|
|
151
|
+
not math.isinf(prev_pres)
|
|
152
|
+
and (prev_pres > prev_miss or perf_now - prev_miss < missing_delay)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def update_part_seen(self, part: str, seen: bool, *, perf_now: Optional[float] = None):
|
|
156
|
+
if perf_now is None:
|
|
157
|
+
perf_now = get_perf_now()
|
|
158
|
+
prev_seen = self.present_last_perf_c.get(part, -math.inf)
|
|
159
|
+
prev_miss = self.missing_last_perf_c.get(part, -math.inf)
|
|
160
|
+
if seen:
|
|
161
|
+
if prev_seen < prev_miss or math.isinf(prev_seen):
|
|
162
|
+
self.present_last_perf_c[part] = perf_now
|
|
163
|
+
self.missing_last_perf_c.setdefault(part, -math.inf)
|
|
164
|
+
else:
|
|
165
|
+
if prev_miss < prev_seen or math.isinf(prev_miss):
|
|
166
|
+
self.missing_last_perf_c[part] = perf_now
|
|
167
|
+
self.present_last_perf_c.setdefault(part, -math.inf)
|
|
168
|
+
if perf_now > self.last_perf_c:
|
|
169
|
+
self.last_perf_c = perf_now
|
|
170
|
+
|
|
171
|
+
def get_animal_absence_age(self, *, perf_now: Optional[float] = None):
|
|
172
|
+
"""Returns the animal "absence" age, how old/elapsed time since last absence started,
|
|
173
|
+
can be math.inf if never been absent"""
|
|
174
|
+
if perf_now is None:
|
|
175
|
+
perf_now = get_perf_now()
|
|
176
|
+
max_perf_c = -math.inf
|
|
177
|
+
parts = set(self.missing_last_perf_c) | set(self.present_last_perf_c)
|
|
178
|
+
for part in parts:
|
|
179
|
+
if part in AllAnimalParts:
|
|
180
|
+
perf_c = self.missing_last_perf_c.get(part, -math.inf)
|
|
181
|
+
if perf_c > max_perf_c:
|
|
182
|
+
max_perf_c = perf_c
|
|
183
|
+
return perf_now - max_perf_c
|
|
184
|
+
|
|
185
|
+
def get_animal_presence_age(self, *, perf_now: Optional[float] = None):
|
|
186
|
+
"""Returns the animal most recent "presence" age. ie: how old since it has been seen last time"""
|
|
187
|
+
if perf_now is None:
|
|
188
|
+
perf_now = get_perf_now()
|
|
189
|
+
max_perf_c = -math.inf
|
|
190
|
+
parts = set(self.missing_last_perf_c) | set(self.present_last_perf_c)
|
|
191
|
+
for part in parts:
|
|
192
|
+
if part in AllAnimalParts:
|
|
193
|
+
perf_c = self.present_last_perf_c.get(part, -math.inf)
|
|
194
|
+
if perf_c > max_perf_c:
|
|
195
|
+
max_perf_c = perf_c
|
|
196
|
+
return perf_now - max_perf_c
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from .project_info import ProjectInfo
|
|
4
|
+
from .project_info import ProjectInterval
|
|
5
|
+
from .project_info import video_write_ext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProjectDependentProtocol(Protocol):
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def project(self) -> ProjectInfo:
|
|
12
|
+
"""The associated current project-info"""
|
|
13
|
+
|
|
14
|
+
@project.setter
|
|
15
|
+
def project(self, value: ProjectInfo):
|
|
16
|
+
"""Project-info setter"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ProjectDependentProtol = ProjectDependentProtocol # previous typo alias
|