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,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