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.
Files changed (73) hide show
  1. auto_trainer_core-2.0.1.dev353.dist-info/METADATA +18 -0
  2. auto_trainer_core-2.0.1.dev353.dist-info/RECORD +73 -0
  3. auto_trainer_core-2.0.1.dev353.dist-info/WHEEL +5 -0
  4. auto_trainer_core-2.0.1.dev353.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,208 @@
1
+ import dataclasses
2
+ import datetime
3
+ import json
4
+ import logging
5
+ import os
6
+ import uuid
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from tempfile import NamedTemporaryFile
10
+ from typing import Optional, Dict, Any, List, Type
11
+ from typing_extensions import Self
12
+
13
+ from autotrainer.core import Offset3DTuple, get_verbose_logger
14
+
15
+ logger = get_verbose_logger(__name__)
16
+
17
+
18
+ _date_format = "%Y%m%d"
19
+
20
+
21
+ def _load_old_format(data: Dict[str, Any]) -> "AnimalSubject":
22
+ kw = {}
23
+ if "name" in data:
24
+ kw['name'] = data["name"]
25
+ animal = AnimalSubject(**kw)
26
+ if "pellet_x" in data and "pellet_y" in data and "pellet_z" in data:
27
+ animal.pellet_x = data["pellet_x"]
28
+ animal.pellet_y = data["pellet_y"]
29
+ animal.pellet_z = data["pellet_z"]
30
+ return animal
31
+
32
+
33
+ @dataclass
34
+ class AnimalTraining:
35
+ """Animal Training configuration"""
36
+
37
+ # NB: protocol == plan ; todo: could/should better be moved to auto-trainer-training repo
38
+
39
+ current_protocol: Optional[str] = None
40
+ protocols: List[Dict[str, Any]] = dataclasses.field(default_factory=list)
41
+
42
+ def get_plan_progress(self, plan_id: str) -> Optional[Dict[str, Any]]:
43
+ # {"plan_id": self.plan_id,
44
+ # "progress_state": self.progress_state,
45
+ # "current_phase_id": None if self.current_phase is None else self.current_phase.phase_id,
46
+ # "progress": progress
47
+ # }
48
+ for prot in self.protocols:
49
+ if prot.get('plan_id') == plan_id:
50
+ return prot
51
+ return None
52
+
53
+ def set_plan_progress(self, plan_id: str, progress: Dict[str, Any]):
54
+ for idx, prog in enumerate(self.protocols):
55
+ if prog['plan_id'] == plan_id:
56
+ self.protocols[idx] = progress
57
+ return
58
+ self.protocols.append(progress)
59
+
60
+
61
+ @dataclass
62
+ class AnimalPelletCounts:
63
+ presented: int = 0 # == successfully sent/delivered to the animal
64
+ reaches: int = 0
65
+ success_reaches: int = 0
66
+ consumed: int = 0
67
+
68
+
69
+ @dataclass
70
+ class _AnimalSubject:
71
+ """A subject in an animal experiment."""
72
+
73
+ version: int = 2
74
+
75
+ name: str = ""
76
+ id: str = None # handled in post_init
77
+
78
+ is_pellet_dcs: bool = False
79
+ pellet_x: float = 0
80
+ pellet_y: float = 0
81
+ pellet_z: float = 0
82
+
83
+ training: AnimalTraining = dataclasses.field(default_factory=AnimalTraining)
84
+
85
+ pellet_counts_day_date: datetime.date = dataclasses.field(default_factory=datetime.date.today)
86
+ pellet_counts_day: AnimalPelletCounts = dataclasses.field(default_factory=AnimalPelletCounts)
87
+ pellet_counts_total: AnimalPelletCounts = dataclasses.field(default_factory=AnimalPelletCounts)
88
+
89
+ target_y_limit: Optional[float] = None # in DCS
90
+
91
+ def __post_init__(self):
92
+ if self.id is None:
93
+ self.id = str(uuid.uuid4())
94
+ if not self.name:
95
+ self.name = f"Mouse-{self.id}"
96
+
97
+
98
+ @dataclass
99
+ class AnimalSubject(_AnimalSubject):
100
+
101
+ def __init__(self, **kwargs):
102
+ kwargs.pop("baseline_magnet_intensity", None)
103
+ super().__init__(**kwargs)
104
+
105
+ def __repr__(self):
106
+ return f"{self.__class__.__name__}(name={self.name!r}, id={self.id!r})"
107
+
108
+ @classmethod
109
+ def from_file(cls: Type[Self], file_path: Path) -> Optional[Self]:
110
+ with file_path.open("r") as file:
111
+ try:
112
+ data = json.load(file)
113
+ if not isinstance(data, dict):
114
+ if not data:
115
+ # assuming empty file
116
+ return cls()
117
+ raise ValueError(f"Invalid file format in {file_path} ; not a dict: %s", type(data))
118
+ file_version = data.get("version", None)
119
+ if file_version is None or file_version != AnimalSubject.version:
120
+ logger.notice("Loaded animal version %s vs current Animal version %s",
121
+ file_version, AnimalSubject.version)
122
+ if "id" not in data:
123
+ # old format
124
+ animal = _load_old_format(data)
125
+ else:
126
+ reach = data.pop('reach')
127
+ pellet_dev = reach.pop('pelletDevice', None)
128
+ pellet_dcs = reach.pop('pelletDcs', None)
129
+ if pellet_dcs is None:
130
+ src = pellet_dev
131
+ else:
132
+ src = pellet_dcs
133
+ pellet_x, pellet_y, pellet_z = src['x'], src['y'], src['z']
134
+ training = data.pop('training')
135
+ pellet_counts_day_dct = data.pop("pelletCountsDay", {})
136
+ pellet_counts_total_dct = data.pop("pelletCountsTotal", {})
137
+ count_day_date_str: Optional[str] = data.pop('pelletCountsDayDate', None)
138
+ if count_day_date_str is None:
139
+ pellet_counts_day_date = datetime.date.today()
140
+ else:
141
+ pellet_counts_day_date = datetime.datetime.strptime(count_day_date_str, _date_format).date()
142
+ animal = cls(
143
+ id=data.pop('id'),
144
+ name=data.pop('name'),
145
+ is_pellet_dcs=pellet_dcs is not None,
146
+ target_y_limit=data.pop('targetYLimit', None),
147
+ pellet_x=pellet_x,
148
+ pellet_y=pellet_y,
149
+ pellet_z=pellet_z,
150
+ training=AnimalTraining(
151
+ current_protocol=training.pop('currentProtocol'),
152
+ protocols=training.pop('protocols'),
153
+ ),
154
+ pellet_counts_day_date=pellet_counts_day_date,
155
+ pellet_counts_day=AnimalPelletCounts(**pellet_counts_day_dct),
156
+ pellet_counts_total=AnimalPelletCounts(**pellet_counts_total_dct),
157
+ )
158
+ except Exception as err:
159
+ logger.error("Error loading animal subject from %s: %s", file_path, err)
160
+ return None
161
+
162
+ logger.debug("loaded animal id=%r name=%r pellet=%s is_dcs=%s current_protocol=%s",
163
+ animal.id, animal.name,
164
+ (animal.pellet_x, animal.pellet_y, animal.pellet_z), animal.is_pellet_dcs,
165
+ animal.training.current_protocol)
166
+
167
+ return animal
168
+
169
+ def to_file(self, file_path: Path):
170
+ reach: Dict[str, Any] = {}
171
+ key = "pelletDcs" if self.is_pellet_dcs else "pelletDevice"
172
+ reach[key] = {
173
+ 'x': self.pellet_x,
174
+ 'y': self.pellet_y,
175
+ 'z': self.pellet_z,
176
+ }
177
+ data = {
178
+ "version": self.version,
179
+ "id": self.id,
180
+ "name": self.name,
181
+ "reach": reach,
182
+ "training": {
183
+ 'currentProtocol': self.training.current_protocol,
184
+ 'protocols': self.training.protocols,
185
+ },
186
+ "targetYLimit": self.target_y_limit,
187
+ "pelletCountsDayDate": self.pellet_counts_day_date.strftime(_date_format),
188
+ "pelletCountsDay": dataclasses.asdict(self.pellet_counts_day),
189
+ "pelletCountsTotal": dataclasses.asdict(self.pellet_counts_total),
190
+ }
191
+ xyz = Offset3DTuple(self.pellet_x, self.pellet_y, self.pellet_z)
192
+ logger.debug("Saving %s to %s ; xyz=%s", self.name, file_path.as_posix(), xyz.humanize())
193
+ file_path.parent.mkdir(parents=True, exist_ok=True)
194
+ with NamedTemporaryFile("w", delete=False, dir=file_path.parent) as fh:
195
+ json.dump(data, fh, indent=4)
196
+ os.replace(fh.name, file_path)
197
+
198
+ def check_today_date(self) -> bool:
199
+ """Return True if changed"""
200
+ today = datetime.date.today()
201
+ prev = self.pellet_counts_day_date
202
+ if prev != today:
203
+ logger.debug("today (%s) != animal prev day date (%s), resetting day counts to 0",
204
+ today, prev)
205
+ self.pellet_counts_day_date = today
206
+ self.pellet_counts_day = AnimalPelletCounts()
207
+ return True
208
+ return False
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import IntEnum
4
+
5
+
6
+ class CaptureProcessStatus(IntEnum):
7
+ """Valid VideoCaptureProcess states available through the status Value"""
8
+
9
+ TERMINATED = -2
10
+ """The capture loop is terminated"""
11
+
12
+ FAILED = -1
13
+ """Failed to configure or run process"""
14
+
15
+ UNKNOWN = 0
16
+ """Uninitialized value not yet set by capture process"""
17
+
18
+ INITIALIZED = 1
19
+ """The process is created, but not started"""
20
+
21
+ RUNNING = 2
22
+ """The process is running the capture loop"""
23
+
24
+ RECORDING = 3
25
+ """The process is recording the stream to disk"""
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import yaml
4
+
5
+ from autotrainer.core.logging import get_verbose_logger
6
+ from autotrainer.core import Offset3DTuple
7
+
8
+ logger = get_verbose_logger(__name__)
9
+
10
+
11
+ DEFAULT_3D_CALIB_DIR_NAME = "4mm_6r_8c_4x"
12
+
13
+
14
+ def generic_constructor(loader, tag, node):
15
+ cls_name = node.__class__.__name__
16
+ if cls_name == "SequenceNode":
17
+ return loader.construct_sequence(node)
18
+ elif cls_name == "MappingNode":
19
+ return loader.construct_mapping(node)
20
+ else:
21
+ return loader.construct_scalar(node)
22
+
23
+
24
+ class GenericSafeLoader(yaml.SafeLoader):
25
+ pass
26
+
27
+
28
+ GenericSafeLoader.add_multi_constructor("", generic_constructor)
29
+
30
+ #
31
+
32
+ class SystemConfigurationLoader(yaml.SafeLoader):
33
+ """Dedicated yaml loader for SystemConfiguration"""
34
+ safe_load: bool = False
35
+
36
+
37
+ class SystemConfigurationSafeLoader(SystemConfigurationLoader):
38
+
39
+ safe_load = True
40
+
41
+ def ignore_unknown(self, suffix, node):
42
+ logger.warning("Skipping/ignoring tag/section %s", suffix)
43
+ return None
44
+
45
+
46
+ def _ignore_unknown(loader, suffix, node):
47
+ return loader.ignore_unknown(suffix, node)
48
+
49
+
50
+ # allow to ignore unknown tags:
51
+ SystemConfigurationSafeLoader.add_multi_constructor('', _ignore_unknown)
52
+ SystemConfigurationSafeLoader.add_multi_constructor('!', _ignore_unknown)
53
+
54
+
55
+ #
56
+ _offset3dtag = "!Offset3DTuple"
57
+ _shift_xyz_tag = "!ShiftXYZTarget"
58
+
59
+ def construct_offset3d_tuple(loader: yaml.Loader, node: yaml.nodes.MappingNode):
60
+ content = loader.construct_mapping(node)
61
+ return Offset3DTuple(**content)
62
+
63
+
64
+ SystemConfigurationLoader.add_constructor(_offset3dtag, construct_offset3d_tuple)
65
+ SystemConfigurationLoader.add_constructor(_shift_xyz_tag, construct_offset3d_tuple)
66
+
67
+ #
68
+
69
+ class SystemConfigurationDumper(yaml.SafeDumper):
70
+ """Dedicated yaml dumper for SystemConfiguration"""
71
+
72
+
73
+ def repr_offset3d_tuple(dumper: SystemConfigurationDumper, obj: Offset3DTuple):
74
+ x, y, z = obj
75
+ return dumper.represent_mapping(_offset3dtag, dict(x=x, y=y, z=z))
76
+
77
+
78
+ SystemConfigurationDumper.add_representer(Offset3DTuple, repr_offset3d_tuple)
79
+
80
+ # above must be before below
81
+
82
+
83
+ from .behavior_configuration import BehaviorConfiguration, ShiftXYZTarget
84
+ from .camera_configuration import CameraConfiguration, CameraId
85
+ from .hardware_configuration import HardwareConfiguration
86
+ from .inference_configuration import InferenceConfiguration
87
+ from .persistence_configuration import PersistenceConfiguration
88
+ from .system_configuration import SystemConfiguration
@@ -0,0 +1,55 @@
1
+
2
+ import dataclasses
3
+
4
+
5
+ @dataclasses.dataclass
6
+ class _EmergencyAlarmConfiguration:
7
+
8
+ # 1st possible alarm condition
9
+ use_audio_load_cell_thrash: bool = False
10
+ auto_resume_on_audio_load_cell_thrash_resume: bool = False
11
+
12
+ audio_load_cell_thrash_aggregate_delay: float = 5 # up to how long ago to look at previous results
13
+ #
14
+ # ( ( if count of thrashing triggers greater than this (during last aggregate_delay)
15
+ load_cell_thrash_count: int = 3
16
+ # or percent of time it is ON during aggregate_delay
17
+ load_cell_thrash_percent_on: float = 50
18
+ # )
19
+ # and
20
+ # (
21
+ audio_thrash_count: int = 3 # spectrum thrash count greater than this (during last aggregate_delay)
22
+ audio_thrash_percent_on: float = 50 # or percent of time it is ON during aggregate_delay
23
+ # ) )
24
+
25
+ # 2nd possible alarm condition
26
+ use_presence_missing_after_exit_tunnel: bool = False
27
+ auto_resume_on_presence_seen_after_exit_tunnel: bool = False
28
+ tunnel_to_cage_presence_missing_delay: float = 5
29
+
30
+ # 3rd
31
+ use_external_doors_open: bool = False
32
+ auto_resume_on_external_doors_close: bool = False
33
+
34
+ # 4rd
35
+ use_global_animal_presence: bool = False
36
+ auto_resume_on_global_animal_presence: bool = False
37
+
38
+ # 5th
39
+ use_device_comm_error: bool = True
40
+ auto_resume_on_device_comm_error: bool = True
41
+
42
+ # 6th
43
+ use_system_maintenance: bool = True
44
+ auto_resume_on_system_maintenance: bool = True
45
+
46
+ # 7th
47
+ use_system_fault: bool = True
48
+ auto_resume_on_system_fault: bool = True
49
+
50
+
51
+ @dataclasses.dataclass
52
+ class EmergencyAlarmConfiguration(_EmergencyAlarmConfiguration):
53
+
54
+ def __init__(self, **kwargs): # force kwargs
55
+ super().__init__(**kwargs)
@@ -0,0 +1,17 @@
1
+ import dataclasses
2
+
3
+
4
+ @dataclasses.dataclass
5
+ class _GlobalAnimalPresenceConfig:
6
+
7
+ presence_missing_delay_hours: float = 12
8
+ # if mouse not seen in tunnel AND not seen in cage longer than the delay, then trigger
9
+
10
+
11
+ class GlobalAnimalPresenceConfig(_GlobalAnimalPresenceConfig):
12
+
13
+ def __init__(self, *,
14
+ presence_missing_delay: float = None, # noqa
15
+ **kwargs):
16
+ del presence_missing_delay # old param name # todo: remove some when later
17
+ super().__init__(**kwargs)