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,484 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import enum
|
|
3
|
+
import math
|
|
4
|
+
from typing import Optional, List, Set, Callable
|
|
5
|
+
|
|
6
|
+
from autotrainer.api import ApiEventKind, ApiAlarmKind
|
|
7
|
+
|
|
8
|
+
from autotrainer.core import get_perf_now
|
|
9
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
10
|
+
from autotrainer.core.multiproc import make_daemon_timer
|
|
11
|
+
from autotrainer.core.pose_elements import ScenePartsPresenceContext
|
|
12
|
+
from autotrainer.core.video_detection import PresenceDetectionAttrs
|
|
13
|
+
from autotrainer.core.configuration.alarm_configuration import EmergencyAlarmConfiguration
|
|
14
|
+
from autotrainer.core.analysis.detector import BaseDetector
|
|
15
|
+
from autotrainer.core.analysis.external_doors_monitor import ExternalDoorsMonitor
|
|
16
|
+
from autotrainer.core.analysis.global_animal_presence_monitor import GlobalAnimalPresenceMonitor
|
|
17
|
+
from autotrainer.core.analysis.audio_spectrum_monitor import AudioSpectrumThrashMonitor
|
|
18
|
+
from autotrainer.core.analysis.load_cell_monitor import LoadCellMonitor
|
|
19
|
+
from autotrainer.core.analysis.load_cell_tare_monitor import LoadCellTareMonitor
|
|
20
|
+
from autotrainer.core.analysis.system_maintenance_monitor import SystemMaintenanceMonitor
|
|
21
|
+
from autotrainer.core.analysis.system_fault_monitor import SystemFaultMonitor
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = get_verbose_logger(__name__)
|
|
25
|
+
|
|
26
|
+
timer_update_state = make_daemon_timer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EmergencyReason(str, enum.Enum):
|
|
30
|
+
|
|
31
|
+
MOUSE_THRASHING = "MOUSE_THRASHING"
|
|
32
|
+
IN_CAGE_AFTER_EXIT_TUNNEL = "IN_CAGE_AFTER_EXIT_TUNNEL"
|
|
33
|
+
DOORS_OPEN = "DOORS_OPEN"
|
|
34
|
+
GLOBAL_ANIMAL_PRESENCE = "GLOBAL_ANIMAL_PRESENCE"
|
|
35
|
+
DEVICE_COMM_ERROR = "DEVICE_COMM_ERROR"
|
|
36
|
+
SYSTEM_MAINTENANCE = "SYSTEM_MAINTENANCE"
|
|
37
|
+
SYSTEM_FAULT = "SYSTEM_FAULT"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_map_emergency_reason_2_api_alarm_kind = {
|
|
41
|
+
EmergencyReason.MOUSE_THRASHING: ApiAlarmKind.thrashing,
|
|
42
|
+
EmergencyReason.IN_CAGE_AFTER_EXIT_TUNNEL: ApiAlarmKind.animalMissing,
|
|
43
|
+
EmergencyReason.GLOBAL_ANIMAL_PRESENCE: ApiAlarmKind.animalImmobile,
|
|
44
|
+
EmergencyReason.DEVICE_COMM_ERROR: ApiAlarmKind.deviceCommunication,
|
|
45
|
+
EmergencyReason.SYSTEM_FAULT: ApiAlarmKind.systemFault,
|
|
46
|
+
EmergencyReason.SYSTEM_MAINTENANCE: ApiAlarmKind.systemMaintenance,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def emergency_reason_2_api_alarm_kind(reason: EmergencyReason) -> ApiAlarmKind:
|
|
50
|
+
return _map_emergency_reason_2_api_alarm_kind[reason]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class EmergencyAlarmMonitor(BaseDetector):
|
|
55
|
+
IS_ENGAGED = "is_engaged"
|
|
56
|
+
CONFIG = "config"
|
|
57
|
+
|
|
58
|
+
PRESENCE_IN_CAGE_AFTER_EXIT_TUNNEL_ENGAGED = "presence_in_cage_after_exit_tunnel_engaged"
|
|
59
|
+
AUDIO_LOAD_CELL_THRASHING_ENGAGED = "audio_load_cell_thrashing_engaged"
|
|
60
|
+
EXT_DOORS_OPEN_ENGAGED = "ext_doors_open_engaged"
|
|
61
|
+
GLOBAL_ANIMAL_PRESENCE_ENGAGED = "global_animal_presence_engaged"
|
|
62
|
+
DEVICE_COMM_ERROR_ENGAGED = "device_comm_error_engaged"
|
|
63
|
+
SYSTEM_MAINTENANCE_ENGAGED = "system_maintenance_engaged"
|
|
64
|
+
SYSTEM_FAULT_ENGAGED = "system_fault_engaged"
|
|
65
|
+
|
|
66
|
+
use_daemon = True
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
config: EmergencyAlarmConfiguration,
|
|
72
|
+
load_cell_monitor: LoadCellMonitor,
|
|
73
|
+
load_cell_tare_monitor: LoadCellTareMonitor,
|
|
74
|
+
audio_monitor: AudioSpectrumThrashMonitor,
|
|
75
|
+
external_doors_monitor: ExternalDoorsMonitor,
|
|
76
|
+
global_animal_presence_monitor: GlobalAnimalPresenceMonitor,
|
|
77
|
+
system_maintenance_monitor: SystemMaintenanceMonitor,
|
|
78
|
+
system_fault_monitor: SystemFaultMonitor,
|
|
79
|
+
topcam_presence_attrs: Optional[PresenceDetectionAttrs] = None,
|
|
80
|
+
):
|
|
81
|
+
super().__init__()
|
|
82
|
+
self._all_scene_parts_ctx = ScenePartsPresenceContext() # both/all cams seen
|
|
83
|
+
self._config = config
|
|
84
|
+
self._load_cell_monitor = load_cell_monitor
|
|
85
|
+
self._load_cell_tare_monitor = load_cell_tare_monitor
|
|
86
|
+
self._audio_monitor = audio_monitor
|
|
87
|
+
self._external_doors_monitor = external_doors_monitor
|
|
88
|
+
self._global_animal_presence_monitor = global_animal_presence_monitor
|
|
89
|
+
self._system_maintenance_monitor = system_maintenance_monitor
|
|
90
|
+
self._system_fault_monitor = system_fault_monitor
|
|
91
|
+
self._topcam_presence_attrs = topcam_presence_attrs
|
|
92
|
+
self._load_cell_thrash_values = []
|
|
93
|
+
self._load_cell_engaged_values = []
|
|
94
|
+
self._audio_thrash_values = []
|
|
95
|
+
self._engaged_reasons: Set[EmergencyReason] = set()
|
|
96
|
+
#
|
|
97
|
+
self._audio_load_cell_thrashing_engaged = False
|
|
98
|
+
self._presence_in_cage_after_exit_tunnel_engaged = False
|
|
99
|
+
self._ext_doors_open_engaged = False
|
|
100
|
+
self._global_animal_presence_engaged = False
|
|
101
|
+
self._device_comm_error_engaged = False
|
|
102
|
+
self._system_maintenance_engaged = False
|
|
103
|
+
self._system_fault_engaged = False
|
|
104
|
+
#
|
|
105
|
+
load_cell_monitor.property_changed += self._on_load_cell_monitor_prop_changed
|
|
106
|
+
audio_monitor.property_changed += self._on_audio_prop_changed
|
|
107
|
+
external_doors_monitor.property_changed += self._on_ext_doors_prop_changed
|
|
108
|
+
global_animal_presence_monitor.property_changed += self._on_global_animal_presence_prop_changed
|
|
109
|
+
system_maintenance_monitor.property_changed += self._on_system_maintenance_prop_changed
|
|
110
|
+
system_fault_monitor.property_changed += self._on_system_fault_prop_changed
|
|
111
|
+
|
|
112
|
+
def update_parts_context(self, context: ScenePartsPresenceContext):
|
|
113
|
+
self._all_scene_parts_ctx = context
|
|
114
|
+
|
|
115
|
+
def add_alarm_condition(self, name, check):
|
|
116
|
+
... # TODO
|
|
117
|
+
|
|
118
|
+
def _start(self):
|
|
119
|
+
super()._start()
|
|
120
|
+
self._engaged_reasons.clear()
|
|
121
|
+
|
|
122
|
+
def post_alarm_event(self, detector_id: int, active: bool, enabled: bool):
|
|
123
|
+
self._event_manager.post_event_content(
|
|
124
|
+
ApiEventKind.alarmChanged,
|
|
125
|
+
data={
|
|
126
|
+
"detector_id": detector_id,
|
|
127
|
+
"is_active": active,
|
|
128
|
+
"is_enabled": enabled,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def config(self) -> EmergencyAlarmConfiguration:
|
|
134
|
+
return self._config
|
|
135
|
+
|
|
136
|
+
@config.setter
|
|
137
|
+
def config(self, value: EmergencyAlarmConfiguration):
|
|
138
|
+
prev, self._config = self._config, value
|
|
139
|
+
self.property_changed(self.CONFIG, value, prev)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def engaged_reasons(self) -> List[EmergencyReason]:
|
|
143
|
+
return sorted(self._engaged_reasons)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def audio_load_cell_thrashing_engaged(self):
|
|
147
|
+
return self._audio_load_cell_thrashing_engaged
|
|
148
|
+
|
|
149
|
+
@audio_load_cell_thrashing_engaged.setter
|
|
150
|
+
def audio_load_cell_thrashing_engaged(self, value):
|
|
151
|
+
prev, self._audio_load_cell_thrashing_engaged = self._audio_load_cell_thrashing_engaged, value
|
|
152
|
+
if value == prev:
|
|
153
|
+
return
|
|
154
|
+
self.property_changed(self.AUDIO_LOAD_CELL_THRASHING_ENGAGED, value, prev)
|
|
155
|
+
self.post_alarm_event(ApiAlarmKind.thrashing, value, self._config.use_audio_load_cell_thrash)
|
|
156
|
+
self.check_state_if_not_detector_thread()
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def presence_in_cage_after_exit_tunnel_engaged(self):
|
|
160
|
+
return self._presence_in_cage_after_exit_tunnel_engaged
|
|
161
|
+
|
|
162
|
+
@presence_in_cage_after_exit_tunnel_engaged.setter
|
|
163
|
+
def presence_in_cage_after_exit_tunnel_engaged(self, value):
|
|
164
|
+
prev, self._presence_in_cage_after_exit_tunnel_engaged = self._presence_in_cage_after_exit_tunnel_engaged, value
|
|
165
|
+
if value == prev:
|
|
166
|
+
return
|
|
167
|
+
self.property_changed(self.PRESENCE_IN_CAGE_AFTER_EXIT_TUNNEL_ENGAGED, value, prev)
|
|
168
|
+
self.post_alarm_event(ApiAlarmKind.animalMissing, value, self._config.use_presence_missing_after_exit_tunnel)
|
|
169
|
+
self.check_state_if_not_detector_thread()
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def ext_doors_open_engaged(self):
|
|
173
|
+
return self._ext_doors_open_engaged
|
|
174
|
+
|
|
175
|
+
@ext_doors_open_engaged.setter
|
|
176
|
+
def ext_doors_open_engaged(self, value):
|
|
177
|
+
prev, self._ext_doors_open_engaged = self._ext_doors_open_engaged, value
|
|
178
|
+
if value == prev:
|
|
179
|
+
return
|
|
180
|
+
self.property_changed(self.EXT_DOORS_OPEN_ENGAGED, value, prev)
|
|
181
|
+
self.post_alarm_event(ApiAlarmKind.externalDoors, value, self._config.use_external_doors_open)
|
|
182
|
+
self.check_state_if_not_detector_thread()
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def global_animal_presence_engaged(self):
|
|
186
|
+
return self._global_animal_presence_engaged
|
|
187
|
+
|
|
188
|
+
@global_animal_presence_engaged.setter
|
|
189
|
+
def global_animal_presence_engaged(self, value):
|
|
190
|
+
prev, self._global_animal_presence_engaged = self._global_animal_presence_engaged, value
|
|
191
|
+
if value == prev:
|
|
192
|
+
return
|
|
193
|
+
self.property_changed(self.GLOBAL_ANIMAL_PRESENCE_ENGAGED, value, prev)
|
|
194
|
+
self.post_alarm_event(ApiAlarmKind.animalImmobile, value, self._config.use_global_animal_presence)
|
|
195
|
+
self.check_state_if_not_detector_thread()
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def device_comm_error_engaged(self):
|
|
199
|
+
return self._device_comm_error_engaged
|
|
200
|
+
|
|
201
|
+
@device_comm_error_engaged.setter
|
|
202
|
+
def device_comm_error_engaged(self, value):
|
|
203
|
+
# is set/unset from hardware model
|
|
204
|
+
prev, self._device_comm_error_engaged = self._device_comm_error_engaged, value
|
|
205
|
+
if value == prev:
|
|
206
|
+
return
|
|
207
|
+
self.property_changed(self.DEVICE_COMM_ERROR_ENGAGED, value, prev)
|
|
208
|
+
self.post_alarm_event(ApiAlarmKind.deviceCommunication, value, self._config.use_device_comm_error)
|
|
209
|
+
self.check_state_if_not_detector_thread()
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def system_maintenance_engaged(self):
|
|
213
|
+
return self._system_maintenance_engaged
|
|
214
|
+
|
|
215
|
+
@system_maintenance_engaged.setter
|
|
216
|
+
def system_maintenance_engaged(self, value):
|
|
217
|
+
prev, self._system_maintenance_engaged = self._system_maintenance_engaged, value
|
|
218
|
+
if value == prev:
|
|
219
|
+
return
|
|
220
|
+
self.property_changed(self.SYSTEM_MAINTENANCE_ENGAGED, value, prev)
|
|
221
|
+
self.post_alarm_event(ApiAlarmKind.systemMaintenance, value, self._config.use_system_maintenance)
|
|
222
|
+
self.check_state_if_not_detector_thread()
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def system_fault_engaged(self):
|
|
226
|
+
return self._system_fault_engaged
|
|
227
|
+
|
|
228
|
+
@system_fault_engaged.setter
|
|
229
|
+
def system_fault_engaged(self, value):
|
|
230
|
+
prev, self._system_fault_engaged = self._system_fault_engaged, value
|
|
231
|
+
if value == prev:
|
|
232
|
+
return
|
|
233
|
+
self.property_changed(self.SYSTEM_FAULT_ENGAGED, value, prev)
|
|
234
|
+
self.post_alarm_event(ApiAlarmKind.systemFault, value, self._config.use_system_fault)
|
|
235
|
+
self.check_state_if_not_detector_thread()
|
|
236
|
+
|
|
237
|
+
#
|
|
238
|
+
def _expire_audio_load_cell(self, perf_now):
|
|
239
|
+
cfg = self._config
|
|
240
|
+
for values in (self._load_cell_thrash_values, self._audio_thrash_values):
|
|
241
|
+
idx = len(values) - 1
|
|
242
|
+
while idx >= 0:
|
|
243
|
+
t_perf = values[idx][0]
|
|
244
|
+
if perf_now - t_perf > cfg.audio_load_cell_thrash_aggregate_delay:
|
|
245
|
+
del values[:idx + 1]
|
|
246
|
+
break
|
|
247
|
+
idx -= 1
|
|
248
|
+
|
|
249
|
+
def _check_audio_load_cell(self, perf_now):
|
|
250
|
+
self._expire_audio_load_cell(perf_now)
|
|
251
|
+
cfg = self._config
|
|
252
|
+
load_cell = self._load_cell_monitor.context
|
|
253
|
+
count_load_cell_thrash_triggers = 0 # count
|
|
254
|
+
tot_load_cell_thrash_engaged = 0 # seconds
|
|
255
|
+
v = None
|
|
256
|
+
perf_c_start = perf_now - cfg.audio_load_cell_thrash_aggregate_delay
|
|
257
|
+
|
|
258
|
+
for idx, v in enumerate(self._load_cell_thrash_values):
|
|
259
|
+
if v[1]:
|
|
260
|
+
count_load_cell_thrash_triggers += 1
|
|
261
|
+
else:
|
|
262
|
+
tot_load_cell_thrash_engaged += (v[0] - perf_c_start) if idx == 0 else v[2]
|
|
263
|
+
if v is not None:
|
|
264
|
+
if v[1]:
|
|
265
|
+
tot_load_cell_thrash_engaged += perf_now - v[0]
|
|
266
|
+
elif load_cell.thrashing_detected:
|
|
267
|
+
tot_load_cell_thrash_engaged += cfg.audio_load_cell_thrash_aggregate_delay
|
|
268
|
+
#
|
|
269
|
+
count_audio_thrash_triggers = 0 # count
|
|
270
|
+
tot_audio_thrash_engaged = 0 # seconds
|
|
271
|
+
v = None
|
|
272
|
+
for idx, v in enumerate(self._audio_thrash_values):
|
|
273
|
+
if v[1]:
|
|
274
|
+
count_audio_thrash_triggers += 1
|
|
275
|
+
else:
|
|
276
|
+
tot_audio_thrash_engaged += (v[0] - perf_c_start) if idx == 0 else v[2]
|
|
277
|
+
if v is not None:
|
|
278
|
+
if v[1]:
|
|
279
|
+
tot_audio_thrash_engaged += perf_now - v[0]
|
|
280
|
+
elif self._audio_monitor.thrashing_detected:
|
|
281
|
+
tot_audio_thrash_engaged += cfg.audio_load_cell_thrash_aggregate_delay
|
|
282
|
+
#
|
|
283
|
+
pc_load_cell_thrash = 100 * tot_load_cell_thrash_engaged / cfg.audio_load_cell_thrash_aggregate_delay
|
|
284
|
+
pc_audio_thrash = 100 * tot_audio_thrash_engaged / cfg.audio_load_cell_thrash_aggregate_delay
|
|
285
|
+
#
|
|
286
|
+
return (
|
|
287
|
+
(
|
|
288
|
+
pc_load_cell_thrash >= cfg.load_cell_thrash_percent_on
|
|
289
|
+
or count_load_cell_thrash_triggers >= cfg.load_cell_thrash_count
|
|
290
|
+
) and (
|
|
291
|
+
pc_audio_thrash >= cfg.audio_thrash_percent_on
|
|
292
|
+
or count_audio_thrash_triggers >= cfg.audio_thrash_count
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def _check_pres_after_exit_tunnel_missing(self, perf_now):
|
|
297
|
+
topcam = self._topcam_presence_attrs
|
|
298
|
+
if topcam is None:
|
|
299
|
+
return False
|
|
300
|
+
topcam = topcam.to_local_value() # to ensure consistent lookups
|
|
301
|
+
load_cell = self._load_cell_monitor.context
|
|
302
|
+
cfg = self._config
|
|
303
|
+
pres_ctx = self._all_scene_parts_ctx
|
|
304
|
+
tun_pres_age = pres_ctx.get_animal_presence_age(perf_now=perf_now)
|
|
305
|
+
tun_miss_age = pres_ctx.get_animal_absence_age(perf_now=perf_now)
|
|
306
|
+
engaged = (
|
|
307
|
+
not load_cell.is_engaged # ~= not in tunnel
|
|
308
|
+
and load_cell.last_disengaged_perf_c > self._p_started
|
|
309
|
+
and perf_now - load_cell.last_disengaged_perf_c > cfg.tunnel_to_cage_presence_missing_delay
|
|
310
|
+
# tunnel exited at least since missing delay threshold
|
|
311
|
+
and tun_pres_age >= 0
|
|
312
|
+
and perf_now - tun_pres_age > load_cell.last_engaged_perf_c
|
|
313
|
+
# animal was seen in tunnel in last tunnel activity/session
|
|
314
|
+
and (
|
|
315
|
+
# last top-cam presence must be before the current load cell disengaged:
|
|
316
|
+
topcam.last_presence_start_perf_c < load_cell.last_disengaged_perf_c
|
|
317
|
+
and topcam.last_absence_start_perf_c < load_cell.last_disengaged_perf_c
|
|
318
|
+
# the previous presence detection in topcam could be right before the exit tunnel,
|
|
319
|
+
# this check ensures the topcam last absence is before last disengage
|
|
320
|
+
and (
|
|
321
|
+
topcam.last_presence_start_perf_c
|
|
322
|
+
< topcam.last_absence_start_perf_c # currently absent from topcam
|
|
323
|
+
< perf_now - cfg.tunnel_to_cage_presence_missing_delay
|
|
324
|
+
# and that absence duration is greater than the missing delay threshold
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
prev = self._presence_in_cage_after_exit_tunnel_engaged
|
|
329
|
+
if engaged and not prev:
|
|
330
|
+
meth = logger.notice
|
|
331
|
+
elif not engaged and prev:
|
|
332
|
+
meth = logger.success
|
|
333
|
+
else:
|
|
334
|
+
meth = None
|
|
335
|
+
if meth is not None:
|
|
336
|
+
meth(
|
|
337
|
+
"Presence-in-cage %s. lc=%s lc.last_eng=%s lc.last_dis=%s "
|
|
338
|
+
"tun_pres_age=%s tun_miss_age=%s "
|
|
339
|
+
"top.last_pres=%s top.last_abs=%s",
|
|
340
|
+
"engaged" if engaged else "disengaged",
|
|
341
|
+
load_cell.is_engaged, load_cell.last_engaged_perf_c, load_cell.last_disengaged_perf_c,
|
|
342
|
+
tun_pres_age, tun_miss_age,
|
|
343
|
+
topcam.last_presence_start_perf_c, topcam.last_absence_start_perf_c)
|
|
344
|
+
return engaged
|
|
345
|
+
|
|
346
|
+
def _check_state(self):
|
|
347
|
+
topcam_attrs = self._topcam_presence_attrs
|
|
348
|
+
load_cell = self._load_cell_monitor.context
|
|
349
|
+
cfg = self._config
|
|
350
|
+
perf_now = get_perf_now()
|
|
351
|
+
#
|
|
352
|
+
reasons = set()
|
|
353
|
+
#
|
|
354
|
+
self.audio_load_cell_thrashing_engaged = self._check_audio_load_cell(perf_now)
|
|
355
|
+
if self._audio_load_cell_thrashing_engaged and cfg.use_audio_load_cell_thrash:
|
|
356
|
+
reasons.add(EmergencyReason.MOUSE_THRASHING)
|
|
357
|
+
#
|
|
358
|
+
self.presence_in_cage_after_exit_tunnel_engaged = self._check_pres_after_exit_tunnel_missing(perf_now)
|
|
359
|
+
if self._presence_in_cage_after_exit_tunnel_engaged and cfg.use_presence_missing_after_exit_tunnel:
|
|
360
|
+
reasons.add(EmergencyReason.IN_CAGE_AFTER_EXIT_TUNNEL)
|
|
361
|
+
#
|
|
362
|
+
self.ext_doors_open_engaged = self._external_doors_monitor.is_engaged
|
|
363
|
+
if self._ext_doors_open_engaged and cfg.use_external_doors_open:
|
|
364
|
+
reasons.add(EmergencyReason.DOORS_OPEN)
|
|
365
|
+
#
|
|
366
|
+
self.global_animal_presence_engaged = self._global_animal_presence_monitor.is_engaged
|
|
367
|
+
if self._global_animal_presence_engaged and cfg.use_global_animal_presence:
|
|
368
|
+
reasons.add(EmergencyReason.GLOBAL_ANIMAL_PRESENCE)
|
|
369
|
+
#
|
|
370
|
+
if self._device_comm_error_engaged and cfg.use_device_comm_error:
|
|
371
|
+
reasons.add(EmergencyReason.DEVICE_COMM_ERROR)
|
|
372
|
+
#
|
|
373
|
+
self.system_maintenance_engaged = self._system_maintenance_monitor.is_engaged
|
|
374
|
+
if self._system_maintenance_engaged and cfg.use_system_maintenance:
|
|
375
|
+
reasons.add(EmergencyReason.SYSTEM_MAINTENANCE)
|
|
376
|
+
#
|
|
377
|
+
self.system_fault_engaged = self._system_fault_monitor.is_engaged
|
|
378
|
+
if self._system_fault_engaged and cfg.use_system_fault:
|
|
379
|
+
reasons.add(EmergencyReason.SYSTEM_FAULT)
|
|
380
|
+
#
|
|
381
|
+
is_emergency = len(reasons) > 0
|
|
382
|
+
#
|
|
383
|
+
if is_emergency and not self._is_engaged:
|
|
384
|
+
logger.notice("Engaging emergency: %s", reasons)
|
|
385
|
+
if __debug__:
|
|
386
|
+
logger.debug("load_cell.disengaged_age=%.1f"
|
|
387
|
+
" load_cell.engaged_age=%.1f presence_start_perf_c=%.1f absence_start_perf_c=%.1f perf_now=%.1f",
|
|
388
|
+
load_cell.disengaged_age, load_cell.engaged_age,
|
|
389
|
+
*((math.nan, math.nan) if topcam_attrs is None else (topcam_attrs.last_presence_start_perf_c, topcam_attrs.last_absence_start_perf_c)),
|
|
390
|
+
perf_now)
|
|
391
|
+
elif __debug__:
|
|
392
|
+
logger.spam(
|
|
393
|
+
"is_emergency=%s load_cell.disengaged_age=%.1f"
|
|
394
|
+
" load_cell.engaged_age=%.1f presence_start_perf_c=%.1f absence_start_perf_c=%.1f perf_now=%.1f",
|
|
395
|
+
is_emergency,
|
|
396
|
+
load_cell.disengaged_age, load_cell.engaged_age,
|
|
397
|
+
*((math.nan, math.nan) if topcam_attrs is None else (topcam_attrs.last_presence_start_perf_c, topcam_attrs.last_absence_start_perf_c)),
|
|
398
|
+
perf_now)
|
|
399
|
+
#
|
|
400
|
+
add_remove_map = {
|
|
401
|
+
EmergencyReason.MOUSE_THRASHING: cfg.auto_resume_on_audio_load_cell_thrash_resume,
|
|
402
|
+
EmergencyReason.IN_CAGE_AFTER_EXIT_TUNNEL: cfg.auto_resume_on_presence_seen_after_exit_tunnel,
|
|
403
|
+
EmergencyReason.DOORS_OPEN: cfg.auto_resume_on_external_doors_close,
|
|
404
|
+
EmergencyReason.GLOBAL_ANIMAL_PRESENCE: cfg.auto_resume_on_global_animal_presence,
|
|
405
|
+
EmergencyReason.DEVICE_COMM_ERROR: cfg.auto_resume_on_device_comm_error,
|
|
406
|
+
EmergencyReason.SYSTEM_MAINTENANCE: cfg.auto_resume_on_system_maintenance,
|
|
407
|
+
EmergencyReason.SYSTEM_FAULT: cfg.auto_resume_on_system_fault,
|
|
408
|
+
}
|
|
409
|
+
#
|
|
410
|
+
if not is_emergency:
|
|
411
|
+
check_reasons = self._engaged_reasons.copy()
|
|
412
|
+
# look if previous engaged reasons (which are now cleared), allowed auto-resume, or not.
|
|
413
|
+
# if any does not allow : don't remove the is_engaged.
|
|
414
|
+
for prev_r in list(check_reasons):
|
|
415
|
+
if add_remove_map[prev_r]:
|
|
416
|
+
check_reasons.remove(prev_r)
|
|
417
|
+
#
|
|
418
|
+
if len(check_reasons) == 0:
|
|
419
|
+
self.is_engaged = False
|
|
420
|
+
self._engaged_reasons = check_reasons # always reset with what remains in check_reasons.
|
|
421
|
+
else:
|
|
422
|
+
check_reasons = self._engaged_reasons.copy()
|
|
423
|
+
# if some possible condition were previously present and are not auto-resume enabled,
|
|
424
|
+
# then re-add them to current reasons of engaged.
|
|
425
|
+
for prev_r in list(check_reasons):
|
|
426
|
+
if not add_remove_map[prev_r]:
|
|
427
|
+
reasons.add(prev_r)
|
|
428
|
+
if reasons != self._engaged_reasons:
|
|
429
|
+
self._is_engaged = None # force trigger again, so that new reasons are seen
|
|
430
|
+
self._engaged_reasons = reasons
|
|
431
|
+
self.is_engaged = True
|
|
432
|
+
return 1 # timer_delay
|
|
433
|
+
|
|
434
|
+
def _on_load_cell_monitor_prop_changed(self, name, value, _):
|
|
435
|
+
if not self._running:
|
|
436
|
+
return
|
|
437
|
+
if not self._config.use_audio_load_cell_thrash:
|
|
438
|
+
return
|
|
439
|
+
if name == LoadCellMonitor.IS_THRASHING_DETECTED_PROPERTY:
|
|
440
|
+
perf_now = get_perf_now()
|
|
441
|
+
load_cell = self._load_cell_monitor.context
|
|
442
|
+
with self._lock:
|
|
443
|
+
self._load_cell_thrash_values.append((perf_now, value,
|
|
444
|
+
load_cell.thrashing_disengaged_age if value
|
|
445
|
+
else load_cell.thrashing_engaged_age))
|
|
446
|
+
self.check_state()
|
|
447
|
+
elif name == LoadCellMonitor.IS_ENGAGED_PROPERTY:
|
|
448
|
+
self.check_state()
|
|
449
|
+
|
|
450
|
+
def _on_audio_prop_changed(self, name, value, _):
|
|
451
|
+
if not self._running:
|
|
452
|
+
return
|
|
453
|
+
if not self._config.use_audio_load_cell_thrash:
|
|
454
|
+
return
|
|
455
|
+
if name == AudioSpectrumThrashMonitor.AUDIO_THRASHING_DETECTED_PROPERTY:
|
|
456
|
+
audio_monitor = self._audio_monitor
|
|
457
|
+
with self._lock:
|
|
458
|
+
self._audio_thrash_values.append((get_perf_now(), value,
|
|
459
|
+
audio_monitor.disengaged_age if value
|
|
460
|
+
else audio_monitor.engaged_age
|
|
461
|
+
))
|
|
462
|
+
self.check_state()
|
|
463
|
+
|
|
464
|
+
def _on_ext_doors_prop_changed(self, name, value, _):
|
|
465
|
+
if not self._running:
|
|
466
|
+
return
|
|
467
|
+
if not self._config.use_external_doors_open:
|
|
468
|
+
return
|
|
469
|
+
if name == ExternalDoorsMonitor.IS_ENGAGED:
|
|
470
|
+
self.check_state()
|
|
471
|
+
|
|
472
|
+
def _on_global_animal_presence_prop_changed(self, name, value, _):
|
|
473
|
+
if not self._running:
|
|
474
|
+
return
|
|
475
|
+
if not self._config.use_global_animal_presence:
|
|
476
|
+
return
|
|
477
|
+
if name == GlobalAnimalPresenceMonitor.IS_ENGAGED:
|
|
478
|
+
self.check_state()
|
|
479
|
+
|
|
480
|
+
def _on_system_maintenance_prop_changed(self, name, value, _):
|
|
481
|
+
self.check_state()
|
|
482
|
+
|
|
483
|
+
def _on_system_fault_prop_changed(self, name, value, _):
|
|
484
|
+
self.check_state()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from ..message.audio_spectrum_message import AudioSpectrumMessage
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class AudioSpectrumData(AudioSpectrumMessage):
|
|
8
|
+
"""
|
|
9
|
+
Default class that implements the AudioSpectrumMessage protocol. Actual hardware devices will likely deliver their
|
|
10
|
+
own object that implements the protocol. However, this class can be used to read back data from the output files
|
|
11
|
+
for playback, reprocessing, or other tasks.
|
|
12
|
+
"""
|
|
13
|
+
when_val: float
|
|
14
|
+
index_val: int
|
|
15
|
+
magnitudes_val: List[float] # values are dB unit
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def when(self) -> float:
|
|
19
|
+
return self.when_val
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def index(self) -> int:
|
|
23
|
+
return self.index_val
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def magnitudes(self) -> List[float]:
|
|
27
|
+
return self.magnitudes_val
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
|
|
2
|
+
import dataclasses
|
|
3
|
+
import itertools
|
|
4
|
+
import math
|
|
5
|
+
import operator
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from collections import deque
|
|
9
|
+
from functools import reduce, partial
|
|
10
|
+
from typing import Optional, List
|
|
11
|
+
|
|
12
|
+
from autotrainer.api import ApiDetectorKind
|
|
13
|
+
from autotrainer.core import get_perf_now
|
|
14
|
+
from autotrainer.core.analysis.detector import BaseDetector
|
|
15
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = get_verbose_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclasses.dataclass
|
|
22
|
+
class AudioSpectrumThrashMonitorConfig:
|
|
23
|
+
|
|
24
|
+
time_window: float = 0.5
|
|
25
|
+
threshold_percent: float = 50
|
|
26
|
+
threshold_db: float = 130
|
|
27
|
+
# NB: the values we read from CAN bus are supposedly (or we consider them as is) in dB unit.
|
|
28
|
+
# but the current value range we get/read is ~80-85 up to ~140-145, generally around ~100 for non-noisy.
|
|
29
|
+
bins_list: List[int] = dataclasses.field(default_factory=lambda : [3, 4, 5, 6])
|
|
30
|
+
# NB:
|
|
31
|
+
# with 6kHz: 3/4/5 goes from ~110 -> ~140
|
|
32
|
+
# with 8kHz: 3 goes from ~100-100 to ~135 and 5/6 goes from ~110 -> ~140
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AudioSpectrumThrashMonitor(BaseDetector):
|
|
36
|
+
|
|
37
|
+
AUDIO_THRASHING_DETECTED_PROPERTY = 'audio_thrashing_detected'
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
config: AudioSpectrumThrashMonitorConfig = AudioSpectrumThrashMonitorConfig()
|
|
43
|
+
):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self._config = config
|
|
46
|
+
self._values_history = deque()
|
|
47
|
+
perf_now = get_perf_now()
|
|
48
|
+
self._when_start_detecting: Optional[float] = None
|
|
49
|
+
self._t_perf_next_report: float = perf_now
|
|
50
|
+
self._when_next_check: float = 0
|
|
51
|
+
|
|
52
|
+
def _check_state(self) -> Optional[float]:
|
|
53
|
+
# all handled by .update()
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def config(self):
|
|
58
|
+
return self._config
|
|
59
|
+
|
|
60
|
+
@config.setter
|
|
61
|
+
def config(self, value):
|
|
62
|
+
self._config = value
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def thrashing_detected(self): # synonym for is_engaged
|
|
66
|
+
return self.is_engaged
|
|
67
|
+
|
|
68
|
+
@thrashing_detected.setter
|
|
69
|
+
def thrashing_detected(self, value):
|
|
70
|
+
self.is_engaged = value
|
|
71
|
+
|
|
72
|
+
@BaseDetector.is_engaged.setter
|
|
73
|
+
def is_engaged(self, value):
|
|
74
|
+
prev = self._is_engaged
|
|
75
|
+
# super().is_engaged = value # use .fset on property for this:
|
|
76
|
+
BaseDetector.is_engaged.fset(self, value)
|
|
77
|
+
if prev == value:
|
|
78
|
+
return
|
|
79
|
+
self.property_changed(self.AUDIO_THRASHING_DETECTED_PROPERTY, value, prev)
|
|
80
|
+
self.post_detector_event(ApiDetectorKind.audioThrash, value)
|
|
81
|
+
|
|
82
|
+
def _update_history(self, values, when, index):
|
|
83
|
+
hist = self._values_history
|
|
84
|
+
dropped = 0
|
|
85
|
+
while len(hist) > 0:
|
|
86
|
+
h0_when = hist[0][1]
|
|
87
|
+
# we could actually use a dichotomic lookup/search, assuming the "when" are consistent (always increasing)
|
|
88
|
+
# and drop all up to the "middle" point in 1 operation.
|
|
89
|
+
if when - h0_when <= self._config.time_window:
|
|
90
|
+
break
|
|
91
|
+
hist.popleft()
|
|
92
|
+
dropped += 1
|
|
93
|
+
hist.append((values, when, index))
|
|
94
|
+
if __debug__:
|
|
95
|
+
t_perf_now = get_perf_now()
|
|
96
|
+
if t_perf_now > self._t_perf_next_report:
|
|
97
|
+
self._t_perf_next_report = t_perf_now + float(os.getenv("AUDIO_THRASHING_LOG_REPORT_DELAY", "60"))
|
|
98
|
+
logger.debug("hist size=%s cur_dropped=%s cur_when=%.3f cur_index=%s ; values=%s",
|
|
99
|
+
len(hist), dropped, when, index, [[f"{vv:.1f}" for vv in v[0]] for v in hist])
|
|
100
|
+
|
|
101
|
+
def update(self, values: List[float], when: float = 0.0, index: int = 0):
|
|
102
|
+
cfg = self._config
|
|
103
|
+
v2 = [values[idx] for idx in cfg.bins_list]
|
|
104
|
+
self._update_history(v2, when, index)
|
|
105
|
+
is_above_thresh = partial(operator.le, cfg.threshold_db)
|
|
106
|
+
cur_above_pc = sum(map(is_above_thresh, v2)) * 100 / len(cfg.bins_list)
|
|
107
|
+
cur_value_avg = sum(v2) / len(cfg.bins_list)
|
|
108
|
+
t_start = self._when_start_detecting
|
|
109
|
+
if cur_value_avg >= cfg.threshold_db or cur_above_pc >= cfg.threshold_percent:
|
|
110
|
+
if t_start is None:
|
|
111
|
+
self._when_start_detecting = when
|
|
112
|
+
self._when_next_check = when + cfg.time_window / 2
|
|
113
|
+
return
|
|
114
|
+
if when < self._when_next_check:
|
|
115
|
+
return
|
|
116
|
+
self._when_next_check = when + cfg.time_window / 2
|
|
117
|
+
values_history = self._values_history
|
|
118
|
+
avg_value = (
|
|
119
|
+
sum(itertools.chain(*(v[0] for v in values_history)))
|
|
120
|
+
/ len(values_history)
|
|
121
|
+
/ len(cfg.bins_list)
|
|
122
|
+
)
|
|
123
|
+
above_threshold = list(map(is_above_thresh, itertools.chain(*(v[0] for v in self._values_history))))
|
|
124
|
+
percent = 100 * sum(map(int, above_threshold)) / len(above_threshold)
|
|
125
|
+
detected = percent >= cfg.threshold_percent or avg_value >= cfg.threshold_db
|
|
126
|
+
if detected != self.thrashing_detected:
|
|
127
|
+
logger.verbose("Thrashing change detected: %s avg=%.1f above_pc=%.1f ; %s",
|
|
128
|
+
detected, avg_value, percent, values_history)
|
|
129
|
+
self.thrashing_detected = detected
|
|
130
|
+
if detected:
|
|
131
|
+
while len(values_history) > 1:
|
|
132
|
+
values_history.popleft()
|
|
133
|
+
else:
|
|
134
|
+
self._when_start_detecting = None
|