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.
- auto_trainer_core-2.0.1.dev349.dist-info/METADATA +19 -0
- auto_trainer_core-2.0.1.dev349.dist-info/RECORD +73 -0
- auto_trainer_core-2.0.1.dev349.dist-info/WHEEL +5 -0
- auto_trainer_core-2.0.1.dev349.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,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from dataclasses import dataclass, field, asdict
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Dict, Optional, Union, ClassVar, TextIO, Type, Any
|
|
9
|
+
from typing_extensions import Self
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
import humps
|
|
13
|
+
|
|
14
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
15
|
+
from . import GenericSafeLoader, SystemConfigurationLoader, SystemConfigurationDumper, SystemConfigurationSafeLoader
|
|
16
|
+
from .. import make_camelize_representer, make_decamelize_constructor
|
|
17
|
+
from .behavior_configuration import BehaviorConfiguration, add_behavior_configuration_representers, \
|
|
18
|
+
add_behavior_configuration_constructors
|
|
19
|
+
from .camera_configuration import CameraConfiguration, CameraId, camera_configuration_representer, \
|
|
20
|
+
camera_configuration_constructor
|
|
21
|
+
from .hardware_configuration import HardwareConfiguration, hardware_configuration_representer, \
|
|
22
|
+
hardware_configuration_constructor
|
|
23
|
+
from .inference_configuration import InferenceConfiguration, inference_configuration_representer, \
|
|
24
|
+
inference_configuration_constructor
|
|
25
|
+
from .persistence_configuration import PersistenceConfiguration, persistence_configuration_representer, \
|
|
26
|
+
persistence_configuration_constructor
|
|
27
|
+
from ..project.project_info import DATE_FORMAT, TIME_FORMAT
|
|
28
|
+
|
|
29
|
+
logger = get_verbose_logger(__name__)
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class SystemConfiguration:
|
|
36
|
+
"""
|
|
37
|
+
The user-visible and editable elements of the system configuration are evolving. This class is largely intended
|
|
38
|
+
manage transitions and maintain a consistent interface to applications.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
DEFAULT_CONFIG_DIR: ClassVar[Path] = Path("~/Autotrainer") # caller/user must expanduser() on it
|
|
42
|
+
DEFAULT_NAME: ClassVar[str] = "system_configuration"
|
|
43
|
+
|
|
44
|
+
DEFAULT_PATH: ClassVar[Path] = DEFAULT_CONFIG_DIR.joinpath(f"{DEFAULT_NAME}.yaml") # caller/user must expanduser() on it
|
|
45
|
+
|
|
46
|
+
version: int = 36
|
|
47
|
+
|
|
48
|
+
cameras: List[CameraConfiguration] = field(default_factory=list)
|
|
49
|
+
hardware: HardwareConfiguration = field(default_factory=HardwareConfiguration)
|
|
50
|
+
inference: InferenceConfiguration = field(default_factory=InferenceConfiguration)
|
|
51
|
+
behavior: BehaviorConfiguration = field(default_factory=BehaviorConfiguration)
|
|
52
|
+
persistence: PersistenceConfiguration = field(default_factory=PersistenceConfiguration)
|
|
53
|
+
|
|
54
|
+
_camera_map = None # do not include in fields
|
|
55
|
+
|
|
56
|
+
def __post_init__(self):
|
|
57
|
+
self._camera_map = {}
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def load_yaml(cls: Type[Self], data: TextIO, *, file_path: Optional[Path] = None) -> Self:
|
|
61
|
+
raw_content: Dict[str, Any] = yaml.load(data, GenericSafeLoader)
|
|
62
|
+
data.seek(0)
|
|
63
|
+
version = raw_content.get("version", 0)
|
|
64
|
+
if version == SystemConfiguration.version:
|
|
65
|
+
# easy case
|
|
66
|
+
configuration: Self = yaml.load(data, SystemConfigurationLoader)
|
|
67
|
+
elif version < SystemConfiguration.version:
|
|
68
|
+
content = humps.decamelize(raw_content)
|
|
69
|
+
configuration = cls()
|
|
70
|
+
if version == 0:
|
|
71
|
+
configuration._deserialize_version_zero(content)
|
|
72
|
+
elif version == 1:
|
|
73
|
+
configuration._deserialize_version_one(content)
|
|
74
|
+
else:
|
|
75
|
+
configuration: Self = yaml.load(data, SystemConfigurationSafeLoader)
|
|
76
|
+
else:
|
|
77
|
+
assert version > SystemConfiguration.version
|
|
78
|
+
logger.warning("Loading configuration version %s while SystemConfiguration.version == %s, "
|
|
79
|
+
"only considering known config attributes/properties.",
|
|
80
|
+
version, SystemConfiguration.version)
|
|
81
|
+
configuration: Self = yaml.load(data, SystemConfigurationSafeLoader)
|
|
82
|
+
|
|
83
|
+
if version != SystemConfiguration.version and file_path is not None:
|
|
84
|
+
now = datetime.now()
|
|
85
|
+
now_str = now.strftime(f"{DATE_FORMAT}_{TIME_FORMAT}")
|
|
86
|
+
new_p = file_path.parent.joinpath(
|
|
87
|
+
f"{file_path.stem}_v{version}_{now_str}{file_path.suffix}")
|
|
88
|
+
logger.notice("Detected config version change/mismatch, saving old config to %s,"
|
|
89
|
+
" and replacing with new after.", new_p)
|
|
90
|
+
shutil.copy2(file_path, new_p)
|
|
91
|
+
configuration.version = SystemConfiguration.version
|
|
92
|
+
# and save new one over previous:
|
|
93
|
+
configuration.save_file(file_path.with_suffix(""), as_yaml=True)
|
|
94
|
+
|
|
95
|
+
return configuration
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def load_yaml_file(cls: Type[Self], path: Union[Path, str], *, save_backup: bool = True) -> Self:
|
|
99
|
+
path: Path = Path(path)
|
|
100
|
+
logger.debug("loading configuration from %r", path)
|
|
101
|
+
with path.open() as fh:
|
|
102
|
+
return cls.load_yaml(fh, file_path=path if save_backup else None)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def make_default_yaml_config_path(cls, dir_path: Path) -> Path:
|
|
106
|
+
return dir_path.joinpath(f"{SystemConfiguration.DEFAULT_NAME}.yaml")
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def load_default(cls: Type[Self], location: Union[str, Path]) -> Optional[Self]:
|
|
110
|
+
path = cls.make_default_yaml_config_path(Path(location))
|
|
111
|
+
if path.is_file():
|
|
112
|
+
return cls.load_yaml_file(path)
|
|
113
|
+
logger.debug("cannot load default from %s ; not a file", path)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def save_default(self, dir_path: Union[Path, str]):
|
|
117
|
+
path = self.make_default_yaml_config_path(Path(dir_path))
|
|
118
|
+
save_path: Path = path.with_suffix("") # noqa
|
|
119
|
+
self.save_file(save_path, as_yaml=True)
|
|
120
|
+
|
|
121
|
+
def dump_yaml(self) -> str:
|
|
122
|
+
return yaml.dump(self, Dumper=SystemConfigurationDumper,
|
|
123
|
+
# we sort/iter by dataclasses.fields() order in our representer function
|
|
124
|
+
sort_keys=False)
|
|
125
|
+
|
|
126
|
+
def save_file(self, path: Union[Path, str], as_yaml: bool = False, as_json: bool = False):
|
|
127
|
+
if not (as_json or as_yaml):
|
|
128
|
+
raise ValueError("Missing one of as_json or as_yaml")
|
|
129
|
+
path: Path = Path(path)
|
|
130
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
if as_json:
|
|
132
|
+
p = path.with_suffix(".json")
|
|
133
|
+
logger.notice("Writing to %r as json", p.as_posix())
|
|
134
|
+
content = json.dumps(asdict(self))
|
|
135
|
+
# dump before writing to file, to prevent empty file on dump issue
|
|
136
|
+
with p.open("w") as file:
|
|
137
|
+
file.write(content)
|
|
138
|
+
if as_yaml:
|
|
139
|
+
p = path.with_suffix(".yaml")
|
|
140
|
+
logger.notice("Writing to %r as yaml", p.as_posix())
|
|
141
|
+
content = self.dump_yaml()
|
|
142
|
+
# dump before writing to file, to prevent empty file on dump issue
|
|
143
|
+
with p.open("w") as file:
|
|
144
|
+
file.write(content)
|
|
145
|
+
|
|
146
|
+
def get_camera(self, camera_id: CameraId) -> Optional[CameraConfiguration]:
|
|
147
|
+
if len(self._camera_map) == 0 or camera_id not in self._camera_map:
|
|
148
|
+
for camera in self.cameras:
|
|
149
|
+
self._camera_map[camera.id] = camera
|
|
150
|
+
|
|
151
|
+
return self._camera_map.get(camera_id, None)
|
|
152
|
+
|
|
153
|
+
def _deserialize_version_zero(self, content: Dict):
|
|
154
|
+
self.cameras.clear()
|
|
155
|
+
|
|
156
|
+
self._try_append_version_zero_camera("camera1", content)
|
|
157
|
+
self._try_append_version_zero_camera("camera2", content)
|
|
158
|
+
self._try_append_version_zero_camera("camera3", content)
|
|
159
|
+
|
|
160
|
+
self.hardware = HardwareConfiguration.from_version_zero(content)
|
|
161
|
+
|
|
162
|
+
# Typo from earlier version of the file.
|
|
163
|
+
self.inference = InferenceConfiguration.from_version_zero(content.get("pellet", {}))
|
|
164
|
+
|
|
165
|
+
self.behavior = BehaviorConfiguration.from_version_zero(content)
|
|
166
|
+
self.persistence = PersistenceConfiguration.from_version_zero(content)
|
|
167
|
+
|
|
168
|
+
def _try_append_version_zero_camera(self, entry: str, content: Dict) -> bool:
|
|
169
|
+
if entry in content:
|
|
170
|
+
camera = CameraConfiguration.from_version_zero(entry, content[entry])
|
|
171
|
+
if camera is not None:
|
|
172
|
+
self.cameras.append(camera)
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def _deserialize_version_one(self, content: Dict):
|
|
178
|
+
self.cameras = [
|
|
179
|
+
CameraConfiguration(**kw)
|
|
180
|
+
for kw in content.get("cameras", [])
|
|
181
|
+
]
|
|
182
|
+
self.hardware = HardwareConfiguration(**content.get("hardware", {}))
|
|
183
|
+
self.inference = InferenceConfiguration(**content.get("inference", {}))
|
|
184
|
+
self.behavior = BehaviorConfiguration.from_version_one(content.get("behavior", {}))
|
|
185
|
+
self.persistence = PersistenceConfiguration(**content.get("persistence", {}))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
#
|
|
189
|
+
|
|
190
|
+
system_configuration_representer = make_camelize_representer("!SystemConfiguration")
|
|
191
|
+
|
|
192
|
+
SystemConfigurationDumper.add_representer(SystemConfiguration, system_configuration_representer)
|
|
193
|
+
SystemConfigurationDumper.add_representer(CameraConfiguration, camera_configuration_representer)
|
|
194
|
+
SystemConfigurationDumper.add_representer(HardwareConfiguration, hardware_configuration_representer)
|
|
195
|
+
SystemConfigurationDumper.add_representer(InferenceConfiguration, inference_configuration_representer)
|
|
196
|
+
SystemConfigurationDumper.add_representer(PersistenceConfiguration, persistence_configuration_representer)
|
|
197
|
+
add_behavior_configuration_representers(SystemConfigurationDumper)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
system_configuration_constructor = make_decamelize_constructor(SystemConfiguration)
|
|
201
|
+
|
|
202
|
+
for cls in SystemConfigurationLoader, :
|
|
203
|
+
# SystemConfigurationSafeLoader:
|
|
204
|
+
# no need also add on SystemConfigurationSafeLoader given it subclass SystemConfigurationLoader
|
|
205
|
+
|
|
206
|
+
cls.add_constructor("!SystemConfiguration", system_configuration_constructor)
|
|
207
|
+
cls.add_constructor("!CameraConfiguration", camera_configuration_constructor)
|
|
208
|
+
cls.add_constructor("!HardwareConfiguration", hardware_configuration_constructor)
|
|
209
|
+
cls.add_constructor("!InferenceConfiguration", inference_configuration_constructor)
|
|
210
|
+
cls.add_constructor("!PersistenceConfiguration", persistence_configuration_constructor)
|
|
211
|
+
add_behavior_configuration_constructors(cls)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclasses.dataclass
|
|
5
|
+
class SystemMaintenanceConfig:
|
|
6
|
+
|
|
7
|
+
use_max_pellet_loaded: bool = True
|
|
8
|
+
auto_resume_on_max_pellets_loaded: bool = True
|
|
9
|
+
max_pellets_loaded_count: int = 500
|
|
10
|
+
|
|
11
|
+
use_max_consecutive_failed_load: bool = True
|
|
12
|
+
auto_resume_on_max_consecutive_failed_load: bool = True
|
|
13
|
+
max_consecutive_failed_loaded: int = 10
|
|
14
|
+
|
|
15
|
+
use_free_disk_space: bool = True
|
|
16
|
+
auto_resume_on_free_disk_space: bool = True
|
|
17
|
+
free_disk_space_min_limit_mb: int = 500
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import itertools
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import ClassVar, Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from typing_extensions import Self
|
|
10
|
+
|
|
11
|
+
from autotrainer.core import Offset3DTuple
|
|
12
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = get_verbose_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_DIAMOND_TRIANGLE_CONFIG_PATH = Path(
|
|
19
|
+
os.getenv("AUTOTRAINER_DIAMOND_TRIANGLE_CONFIG", "~/Autotrainer/diamond_triangle_offset.yaml")
|
|
20
|
+
).expanduser()
|
|
21
|
+
|
|
22
|
+
_offset_nans = Offset3DTuple(math.nan, math.nan, math.nan)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclasses.dataclass
|
|
26
|
+
class DiamondTriangleOffsetConfig:
|
|
27
|
+
|
|
28
|
+
used_position: Offset3DTuple # motor coordinate used
|
|
29
|
+
measured_offset: Offset3DTuple # inference coordinate offset between diamond and triangle (diamond - triangle)
|
|
30
|
+
|
|
31
|
+
raw_diamond_coord: Offset3DTuple = _offset_nans # output from triangulate_3d
|
|
32
|
+
diamond_coord: Offset3DTuple = _offset_nans # inference diamond coordinate, output from reorient_and_center
|
|
33
|
+
|
|
34
|
+
version: Optional[int] = None
|
|
35
|
+
current_config_version: ClassVar[int] = 2 # NB: bump me when need to
|
|
36
|
+
|
|
37
|
+
DEFAULT_CONFIG_PATH: ClassVar = DEFAULT_DIAMOND_TRIANGLE_CONFIG_PATH
|
|
38
|
+
|
|
39
|
+
# 3 coordinate systems,
|
|
40
|
+
# but each has some axis direction difference (when one increases, the other decreases)
|
|
41
|
+
flips_inference_motor: ClassVar[Offset3DTuple] = Offset3DTuple(1, -1, -1)
|
|
42
|
+
flips_inference_diamond: ClassVar[Offset3DTuple] = Offset3DTuple(1, 1, 1)
|
|
43
|
+
# we also want inference diamond flips equals (1, 1, 1), given inference == diamond
|
|
44
|
+
|
|
45
|
+
# defining flips between motor and diamond with flips between motor and inference * flips between inference and diamond:
|
|
46
|
+
flips_motor_diamond: ClassVar[Offset3DTuple] = flips_inference_diamond * flips_inference_motor # Offset3DTuple(1, -1, -1)
|
|
47
|
+
# assert flips_inference_diamond * flips_inference_motor == flips_motor_diamond
|
|
48
|
+
|
|
49
|
+
# custom init to ensure kwarg only :
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
used_position: Offset3DTuple,
|
|
54
|
+
measured_offset: Offset3DTuple,
|
|
55
|
+
diamond_coord: Offset3DTuple = _offset_nans,
|
|
56
|
+
raw_diamond_coord: Offset3DTuple = _offset_nans,
|
|
57
|
+
version: Optional[int] = None,
|
|
58
|
+
):
|
|
59
|
+
super().__init__()
|
|
60
|
+
self.used_position = used_position
|
|
61
|
+
self.measured_offset = measured_offset
|
|
62
|
+
self.diamond_coord = diamond_coord
|
|
63
|
+
self.raw_diamond_coord = raw_diamond_coord
|
|
64
|
+
self.version = version
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def fully_valid(self):
|
|
68
|
+
return self.version == self.current_config_version and all(map(math.isfinite,
|
|
69
|
+
itertools.chain(
|
|
70
|
+
self.used_position,
|
|
71
|
+
self.measured_offset,
|
|
72
|
+
self.diamond_coord,
|
|
73
|
+
self.raw_diamond_coord,
|
|
74
|
+
)))
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def load_config(cls, cfg_path: Optional[Path]) -> Optional[Self]:
|
|
78
|
+
if cfg_path is None:
|
|
79
|
+
logger.notice("No diamond-triangle offset config path provided")
|
|
80
|
+
else:
|
|
81
|
+
if not cfg_path.expanduser().is_file():
|
|
82
|
+
logger.warning("Diamond triangle config %r not a file", cfg_path.as_posix())
|
|
83
|
+
else:
|
|
84
|
+
logger.verbose("Loading diamond-triangle file %r", cfg_path.as_posix())
|
|
85
|
+
cfg = DiamondTriangleOffsetConfig.from_file(cfg_path)
|
|
86
|
+
if not all(math.isfinite(c) for c in cfg.diamond_coord):
|
|
87
|
+
logger.warning("Diamond coordinate undefined or not finite in config %r",
|
|
88
|
+
cfg_path.as_posix())
|
|
89
|
+
return cfg
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_file(cls, path: Path) -> Self:
|
|
94
|
+
with path.expanduser().open() as fh:
|
|
95
|
+
dct = yaml.safe_load(fh)
|
|
96
|
+
if dct is None:
|
|
97
|
+
# in case of empty file
|
|
98
|
+
return cls(used_position=_offset_nans, measured_offset=_offset_nans, version=cls.current_config_version)
|
|
99
|
+
return cls(**dict((k, v if k == "version" else Offset3DTuple(v)) for k, v in dct.items()))
|
|
100
|
+
|
|
101
|
+
def to_file(self, path: Path):
|
|
102
|
+
d = dataclasses.asdict(self)
|
|
103
|
+
for k, v in d.items():
|
|
104
|
+
if k != "version":
|
|
105
|
+
d[k] = list(v) # getting yaml type error with Offset3D not natively supported by yaml
|
|
106
|
+
with path.expanduser().open("w") as fh:
|
|
107
|
+
yaml.safe_dump(d, fh)
|
|
108
|
+
|
|
109
|
+
def inference_to_motor(self, inference_xyz: Offset3DTuple) -> Offset3DTuple:
|
|
110
|
+
"""Transform an inference coordinate to motor corresponding coordinate,
|
|
111
|
+
relatively to the diamond-triangle known position & relative offset"""
|
|
112
|
+
# only used by tests
|
|
113
|
+
assert isinstance(inference_xyz, Offset3DTuple), inference_xyz
|
|
114
|
+
return self.flips_inference_motor * (
|
|
115
|
+
self.flips_inference_diamond * self.measured_offset - inference_xyz
|
|
116
|
+
) + self.used_position
|
|
117
|
+
|
|
118
|
+
def motor_to_inference(self, motor_xyz: Offset3DTuple) -> Offset3DTuple:
|
|
119
|
+
# only used by tests
|
|
120
|
+
assert isinstance(motor_xyz, Offset3DTuple), motor_xyz
|
|
121
|
+
# return self.flips_inference_motor * (motor_xyz - self.used_position) - self.flips_inference_diamond * self.measured_offset
|
|
122
|
+
return (
|
|
123
|
+
self.flips_inference_diamond * self.measured_offset
|
|
124
|
+
- self.flips_inference_motor * (motor_xyz - self.used_position)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def motor_to_diamond(self, motor_xyz: Offset3DTuple) -> Offset3DTuple:
|
|
128
|
+
"""Transform the motor coordinate to corresponding triangle coordinate in DCS"""
|
|
129
|
+
assert isinstance(motor_xyz, Offset3DTuple), motor_xyz
|
|
130
|
+
return (
|
|
131
|
+
self.flips_motor_diamond * (motor_xyz - self.used_position)
|
|
132
|
+
+ self.measured_offset
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def diamond_to_motor(self, diamond_xyz: Offset3DTuple) -> Offset3DTuple:
|
|
136
|
+
"""Transform the triangle coordinate from DCS to corresponding motor coordinates"""
|
|
137
|
+
assert isinstance(diamond_xyz, Offset3DTuple), diamond_xyz
|
|
138
|
+
return self.used_position + self.flips_motor_diamond * (
|
|
139
|
+
diamond_xyz - self.measured_offset
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def inference_to_diamond(self, inference_xyz: Offset3DTuple) -> Offset3DTuple:
|
|
143
|
+
assert isinstance(inference_xyz, Offset3DTuple), inference_xyz
|
|
144
|
+
return self.motor_to_diamond(self.inference_to_motor(inference_xyz))
|
|
145
|
+
# return self.flips_inference_diamond * inference_xyz
|
|
146
|
+
|
|
147
|
+
def diamond_to_inference(self, diamond_xyz: Offset3DTuple) -> Offset3DTuple:
|
|
148
|
+
assert isinstance(diamond_xyz, Offset3DTuple), diamond_xyz
|
|
149
|
+
return self.motor_to_inference(self.diamond_to_motor(diamond_xyz))
|
|
150
|
+
# return self.flips_inference_diamond * diamond_xyz
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
|
|
4
|
+
from autotrainer.api.api_event_kind import ApiEventKind
|
|
5
|
+
from autotrainer.api.api_options import create_default_api_options
|
|
6
|
+
|
|
7
|
+
from autotrainer.core.logging import get_verbose_logger
|
|
8
|
+
from autotrainer.core.event.api_event_plugin import ApiEventPlugin
|
|
9
|
+
|
|
10
|
+
from .event_manager import EventManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = get_verbose_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ApiEventKind",
|
|
18
|
+
"try_register_api_event_plugin",
|
|
19
|
+
"post_api_event_content",
|
|
20
|
+
"post_api_detector_event_content",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def try_register_api_event_plugin() -> Optional[ApiEventPlugin]:
|
|
25
|
+
"""
|
|
26
|
+
Attempt to register the Autotrainer External API plugin with the default instance of the event manager.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
bool: True if the plugin was successfully registered, False otherwise.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
# NB: keeping import here,
|
|
33
|
+
# given otherwise it gives app crash at start if it's imported before the main qt app instance is created
|
|
34
|
+
|
|
35
|
+
plugin = ApiEventPlugin(create_default_api_options())
|
|
36
|
+
plugin.set_enable(True)
|
|
37
|
+
EventManager.default().register_plugin(plugin)
|
|
38
|
+
return plugin
|
|
39
|
+
except Exception as err:
|
|
40
|
+
logger.exception("API plugin creation or registration failed: %s", err)
|
|
41
|
+
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def post_api_event_content(kind: int, data: Any):
|
|
46
|
+
EventManager.default().post_event_content(kind, data=data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def post_api_detector_event_content(manager: EventManager, detector_id: int, active: bool, enabled: bool):
|
|
50
|
+
manager.post_event_content(ApiEventKind.detectorChanged, data=dict(
|
|
51
|
+
detector_id=detector_id,
|
|
52
|
+
is_active=active,
|
|
53
|
+
is_enabled=enabled,
|
|
54
|
+
))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from autotrainer.api import ApiOptions, RpcService, create_api_service, ApiTopic
|
|
5
|
+
|
|
6
|
+
from ..logging import get_verbose_logger
|
|
7
|
+
from ..project import ProjectInfo
|
|
8
|
+
from .event_info import EventInfo
|
|
9
|
+
|
|
10
|
+
from .event_manager_plugin import EventManagerPlugin
|
|
11
|
+
|
|
12
|
+
logger = get_verbose_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApiEventPlugin(EventManagerPlugin):
|
|
16
|
+
"""
|
|
17
|
+
An EventManager plugin that sends events to the auto-trainer-api facilities.
|
|
18
|
+
|
|
19
|
+
This plugin requires the auto-trainer-api package to be installed.
|
|
20
|
+
|
|
21
|
+
See auto-trainer-api for additional details.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, options: ApiOptions):
|
|
25
|
+
self._options = options
|
|
26
|
+
self._service: Optional[RpcService] = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def service(self) -> Optional[RpcService]:
|
|
30
|
+
return self._service
|
|
31
|
+
|
|
32
|
+
def set_project(self, project: Optional[ProjectInfo]) -> None:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def set_enable(self, enable: bool) -> None:
|
|
36
|
+
svc = self._service
|
|
37
|
+
if enable and svc is None:
|
|
38
|
+
svc = self._service = create_api_service(self._options)
|
|
39
|
+
if svc is not None:
|
|
40
|
+
svc.start()
|
|
41
|
+
elif not enable and svc is not None:
|
|
42
|
+
svc.stop()
|
|
43
|
+
self._service = None
|
|
44
|
+
|
|
45
|
+
def process_event(self, info: EventInfo, repeat_count: int) -> None:
|
|
46
|
+
svc = self._service
|
|
47
|
+
if svc is not None:
|
|
48
|
+
svc.send_dict(ApiTopic.EVENT, asdict(info))
|
|
49
|
+
|
|
50
|
+
def flush(self) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
svc = self._service
|
|
55
|
+
if svc is not None:
|
|
56
|
+
svc.stop()
|
|
57
|
+
self._service = None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from typing_extensions import Self
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class EventInfo:
|
|
10
|
+
"""
|
|
11
|
+
EventInfo is a structured definition for "events" in the system. The primary objective is to provide a history of
|
|
12
|
+
actions and changes that are consequential to animal training in structured format that can be parsed in
|
|
13
|
+
conjunction with outer data files. This is generally a subset of the information that is needed or useful for
|
|
14
|
+
general logging.
|
|
15
|
+
"""
|
|
16
|
+
kind: int
|
|
17
|
+
"""
|
|
18
|
+
The kind of event. This is a unique identifier for the event type. Some coordination is required across modules
|
|
19
|
+
to prevent id duplication, if desired. For most event sinks, the `str()` value is also used as part of the
|
|
20
|
+
output. Using and `IntEnum` or similar that generates a descriptive string by default is encouraged.
|
|
21
|
+
"""
|
|
22
|
+
when: datetime
|
|
23
|
+
"""
|
|
24
|
+
The time the event occurred. This is typically presented as a date/time string in the output.
|
|
25
|
+
"""
|
|
26
|
+
index: int
|
|
27
|
+
"""
|
|
28
|
+
An time-based index that may be more precise than standard date-time functions. Typically set via
|
|
29
|
+
`time.perf_counter_ns()`. The same method of indexing must be used across all `EventInfo` providers if indexing
|
|
30
|
+
is expected to be consistent across all event types.
|
|
31
|
+
"""
|
|
32
|
+
context: Optional[object] = None
|
|
33
|
+
"""
|
|
34
|
+
An optional object containing event specific information. `EventInfo` collections are typically presented in a
|
|
35
|
+
structured, row-column format. Complex hierarchical data structures may not be represented well in this format.
|
|
36
|
+
The object must be serializable to JSON using the default JSON encoder and be pickleable to ensure support in all
|
|
37
|
+
event manager sinks.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def is_same(self, info: Self) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Provide a deeper than ReferenceEquals comparison of two `EventInfo` objects to determine repeat events. It is
|
|
43
|
+
less stringent than a raw shallow compare as fields like when and index are expected to be different for repeat
|
|
44
|
+
events. There may be specialized `EventInfo` instances require a custom comparison of `context` to determine
|
|
45
|
+
sameness in which case this can be overridden in a subclass.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
info: another `EventInfo` object to compare against.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if the two `EventInfo` objects are the same, False otherwise.
|
|
52
|
+
"""
|
|
53
|
+
return info is not None and self.kind == info.kind and self.context == info.context
|