mxbiflow 0.1.1__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.
- mxbiflow/__init__.py +3 -0
- mxbiflow/assets/__init__.py +5 -0
- mxbiflow/assets/clicker.wav +0 -0
- mxbiflow/config_store.py +68 -0
- mxbiflow/data_logger.py +114 -0
- mxbiflow/default/__init__.py +4 -0
- mxbiflow/default/idle/assets/apple_v1.png +0 -0
- mxbiflow/default/idle/idle.py +57 -0
- mxbiflow/detector_bridge.py +87 -0
- mxbiflow/game.py +84 -0
- mxbiflow/infra/eventbus.py +31 -0
- mxbiflow/main.py +106 -0
- mxbiflow/models/animal.py +130 -0
- mxbiflow/models/reward.py +7 -0
- mxbiflow/models/session.py +145 -0
- mxbiflow/mxbiflow.py +43 -0
- mxbiflow/path.py +41 -0
- mxbiflow/scene/__init__.py +8 -0
- mxbiflow/scene/scene_manager.py +64 -0
- mxbiflow/scene/scene_protocol.py +22 -0
- mxbiflow/scheduler.py +90 -0
- mxbiflow/tasks/GNGSiD/models.py +70 -0
- mxbiflow/tasks/GNGSiD/stages/detect_stage/config.json +116 -0
- mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage.py +161 -0
- mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage_models.py +65 -0
- mxbiflow/tasks/GNGSiD/stages/discriminate_stage/config.json +70 -0
- mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage.py +173 -0
- mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage_models.py +80 -0
- mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/config.json +83 -0
- mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_models.py +58 -0
- mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_stage.py +149 -0
- mxbiflow/tasks/GNGSiD/tasks/artifacts.py +13 -0
- mxbiflow/tasks/GNGSiD/tasks/detect/models.py +21 -0
- mxbiflow/tasks/GNGSiD/tasks/detect/scene.py +271 -0
- mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_models.py +31 -0
- mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_scene.py +336 -0
- mxbiflow/tasks/GNGSiD/tasks/touch/touch_models.py +17 -0
- mxbiflow/tasks/GNGSiD/tasks/touch/touch_scene.py +256 -0
- mxbiflow/tasks/GNGSiD/tasks/utils/targets.py +57 -0
- mxbiflow/tasks/cross_modal/bundle_dir.py +553 -0
- mxbiflow/tasks/cross_modal/config.py +41 -0
- mxbiflow/tasks/cross_modal/media.py +61 -0
- mxbiflow/tasks/cross_modal/models.py +57 -0
- mxbiflow/tasks/cross_modal/scene.py +252 -0
- mxbiflow/tasks/cross_modal/stage.py +218 -0
- mxbiflow/tasks/cross_modal/trial_io.py +23 -0
- mxbiflow/tasks/cross_modal/trial_schema.py +113 -0
- mxbiflow/tasks/default/error_task/error_scene.py +53 -0
- mxbiflow/tasks/default/idle_task/assets/apple_v1.png +0 -0
- mxbiflow/tasks/default/idle_task/idle_scene.py +85 -0
- mxbiflow/tasks/default/initial_habituation_training/README.md +188 -0
- mxbiflow/tasks/default/initial_habituation_training/stages/config.csv +7 -0
- mxbiflow/tasks/default/initial_habituation_training/stages/config.json +67 -0
- mxbiflow/tasks/default/initial_habituation_training/stages/initial_habituation_training_stage.py +172 -0
- mxbiflow/tasks/default/initial_habituation_training/stages/models.py +56 -0
- mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward.py +244 -0
- mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward_models.py +50 -0
- mxbiflow/tasks/task_protocol.py +26 -0
- mxbiflow/tasks/task_table.py +29 -0
- mxbiflow/tasks/two_alternative_choice/assets/starter.py +27 -0
- mxbiflow/tasks/two_alternative_choice/models.py +68 -0
- mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/config.json +118 -0
- mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_models.py +41 -0
- mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_stage.py +122 -0
- mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_models.py +19 -0
- mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_scene.py +249 -0
- mxbiflow/timer/__init__.py +3 -0
- mxbiflow/timer/frame_timer.py +47 -0
- mxbiflow/timer/realtime_timer.py +0 -0
- mxbiflow/tmp_email.py +13 -0
- mxbiflow/ui/components/animal.py +87 -0
- mxbiflow/ui/components/baseconfig.py +68 -0
- mxbiflow/ui/components/card.py +18 -0
- mxbiflow/ui/components/device_card/__init__.py +17 -0
- mxbiflow/ui/components/device_card/detector/beambreak_detector_card.py +29 -0
- mxbiflow/ui/components/device_card/detector/fusion_detector.py +45 -0
- mxbiflow/ui/components/device_card/detector/mock_detector_card.py +20 -0
- mxbiflow/ui/components/device_card/detector/rfid_detector.py +40 -0
- mxbiflow/ui/components/device_card/device_card.py +67 -0
- mxbiflow/ui/components/device_card/rewarder/mock_rewarder_card.py +20 -0
- mxbiflow/ui/components/device_card/rewarder/rpi_gpio_rewarder.py +33 -0
- mxbiflow/ui/components/devices.py +183 -0
- mxbiflow/ui/components/dialog/__init__.py +3 -0
- mxbiflow/ui/components/dialog/add_devices_dialog.py +64 -0
- mxbiflow/ui/components/experiment_groups.py +122 -0
- mxbiflow/ui/experiment_panel.py +91 -0
- mxbiflow/ui/mxbi_panel.py +152 -0
- mxbiflow/utils/logger.py +19 -0
- mxbiflow/utils/serial.py +10 -0
- mxbiflow-0.1.1.dist-info/METADATA +168 -0
- mxbiflow-0.1.1.dist-info/RECORD +93 -0
- mxbiflow-0.1.1.dist-info/WHEEL +4 -0
- mxbiflow-0.1.1.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, PrivateAttr, ValidationError
|
|
8
|
+
|
|
9
|
+
from .animal import Animal, AnimalConfig
|
|
10
|
+
from .reward import RewardEnum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SessionConfig(BaseModel):
|
|
14
|
+
experimenter: str = Field(default="auto", frozen=True)
|
|
15
|
+
reward_type: RewardEnum = Field(default=RewardEnum.AGUM_ONE_FIFTH, frozen=True)
|
|
16
|
+
send_email: bool = Field(default=False, frozen=True)
|
|
17
|
+
sync_data: bool = Field(default=False, frozen=True)
|
|
18
|
+
note: str = Field(default="", max_length=1000)
|
|
19
|
+
|
|
20
|
+
animals: list[AnimalConfig] = Field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DailySessionCounter(BaseModel):
|
|
24
|
+
day: str = Field(
|
|
25
|
+
default_factory=lambda: datetime.now(timezone.utc).date().isoformat()
|
|
26
|
+
)
|
|
27
|
+
last_session_id: int = Field(default=0, ge=0)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DailySessionIdStore:
|
|
32
|
+
path: Path
|
|
33
|
+
|
|
34
|
+
def _today_utc(self):
|
|
35
|
+
return datetime.now(timezone.utc).date().isoformat()
|
|
36
|
+
|
|
37
|
+
def _load(self) -> DailySessionCounter:
|
|
38
|
+
if not self.path.exists():
|
|
39
|
+
return DailySessionCounter()
|
|
40
|
+
|
|
41
|
+
text = self.path.read_text()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
return DailySessionCounter.model_validate_json(text)
|
|
45
|
+
except ValueError, ValidationError:
|
|
46
|
+
return DailySessionCounter()
|
|
47
|
+
|
|
48
|
+
def _atomic_write(self, data: DailySessionCounter) -> None:
|
|
49
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
fd, tmp = tempfile.mkstemp(
|
|
51
|
+
dir=str(self.path.parent),
|
|
52
|
+
prefix=self.path.name,
|
|
53
|
+
suffix=".tmp",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
58
|
+
f.write(data.model_dump_json())
|
|
59
|
+
f.flush()
|
|
60
|
+
os.fsync(f.fileno())
|
|
61
|
+
|
|
62
|
+
os.replace(tmp, self.path)
|
|
63
|
+
finally:
|
|
64
|
+
try:
|
|
65
|
+
os.remove(tmp)
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def session_id(self) -> int:
|
|
71
|
+
today = self._today_utc()
|
|
72
|
+
data = self._load()
|
|
73
|
+
|
|
74
|
+
if data.day != today:
|
|
75
|
+
data.day = today
|
|
76
|
+
data.last_session_id = 0
|
|
77
|
+
|
|
78
|
+
data.last_session_id += 1
|
|
79
|
+
self._atomic_write(data)
|
|
80
|
+
|
|
81
|
+
return data.last_session_id
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Session(BaseModel):
|
|
85
|
+
experimenter: str = Field(default="auto", frozen=True)
|
|
86
|
+
reward_type: RewardEnum = Field(default=RewardEnum.AGUM_ONE_FIFTH, frozen=True)
|
|
87
|
+
send_email: bool = Field(default=False, frozen=True)
|
|
88
|
+
sync_data: bool = Field(default=False, frozen=True)
|
|
89
|
+
|
|
90
|
+
session_id: int = Field(default=0, ge=0)
|
|
91
|
+
start_at: float = Field(default=0, ge=0)
|
|
92
|
+
end_at: float = Field(default=0, ge=0)
|
|
93
|
+
note: str = Field(frozen=True)
|
|
94
|
+
|
|
95
|
+
_current_animal: str | None = PrivateAttr(default=None)
|
|
96
|
+
animals: dict[str, Animal] = Field(default_factory=dict, frozen=True)
|
|
97
|
+
|
|
98
|
+
def start(self):
|
|
99
|
+
self.start_at = datetime.now(timezone.utc).timestamp()
|
|
100
|
+
|
|
101
|
+
def end(self):
|
|
102
|
+
self.end_at = datetime.now(timezone.utc).timestamp()
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def current_animal(self) -> Animal | None:
|
|
106
|
+
key = self._current_animal
|
|
107
|
+
if key is None:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
return self.animals[key]
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def require_current_animal(self) -> Animal:
|
|
114
|
+
key = self._current_animal
|
|
115
|
+
if key is None:
|
|
116
|
+
raise RuntimeError(
|
|
117
|
+
f"Animal: {key} is not set. Please call set_current_animal() first"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return self.animals[key]
|
|
121
|
+
|
|
122
|
+
def clear_current_animal(self):
|
|
123
|
+
a = self.current_animal
|
|
124
|
+
if a is None:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
a.end_animal_session()
|
|
128
|
+
self._current_animal = None
|
|
129
|
+
|
|
130
|
+
def set_current_animal(self, animal: str):
|
|
131
|
+
self.clear_current_animal()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
a = self.animals[animal]
|
|
135
|
+
except KeyError:
|
|
136
|
+
raise ValueError(f"animal {animal} not found")
|
|
137
|
+
|
|
138
|
+
self._current_animal = animal
|
|
139
|
+
a.start_animal_session()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Options(BaseModel):
|
|
143
|
+
mxbis: list[str] = Field(default_factory=list, frozen=True)
|
|
144
|
+
experimenter: list[str] = Field(default_factory=list, frozen=True)
|
|
145
|
+
animals: dict[str, str] = Field(default_factory=dict, frozen=True)
|
mxbiflow/mxbiflow.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from pymxbi import MXBI
|
|
2
|
+
|
|
3
|
+
from .models.session import Session
|
|
4
|
+
from .timer import FrameTimer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MXBIFlow:
|
|
8
|
+
def __init__(self, session: Session, mxbi: MXBI) -> None:
|
|
9
|
+
self._session = session
|
|
10
|
+
self._timer = FrameTimer()
|
|
11
|
+
self._mxbi = mxbi
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def session(self) -> Session:
|
|
15
|
+
return self._session
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def timer(self) -> FrameTimer:
|
|
19
|
+
return self._timer
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def mxbi(self) -> MXBI:
|
|
23
|
+
return self._mxbi
|
|
24
|
+
|
|
25
|
+
def update(self) -> None:
|
|
26
|
+
self._timer.update()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_current_mxbiflow: MXBIFlow | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_mxbiflow() -> MXBIFlow:
|
|
33
|
+
global _current_mxbiflow
|
|
34
|
+
if _current_mxbiflow is None:
|
|
35
|
+
raise RuntimeError("MXBIFlow not initialized")
|
|
36
|
+
return _current_mxbiflow
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def set_mxbiflow(mxbiflow: MXBIFlow) -> None:
|
|
40
|
+
global _current_mxbiflow
|
|
41
|
+
if _current_mxbiflow is not None:
|
|
42
|
+
raise RuntimeError("MXBIFlow already initialized")
|
|
43
|
+
_current_mxbiflow = mxbiflow
|
mxbiflow/path.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
# TODO: XDG Base Directory Specification
|
|
4
|
+
ROOT_DIR_PATH = Path(__file__).parents[2]
|
|
5
|
+
|
|
6
|
+
CONFIG_DIR_PATH = ROOT_DIR_PATH / "config"
|
|
7
|
+
CONFIG_SESSION_FILENAME = "config_session.json"
|
|
8
|
+
CONFIG_SESSION_PATH = CONFIG_DIR_PATH / CONFIG_SESSION_FILENAME
|
|
9
|
+
|
|
10
|
+
OPTIONS_SESSION_FILENAME = "options_session.json"
|
|
11
|
+
OPTIONS_SESSION_PATH = CONFIG_DIR_PATH / OPTIONS_SESSION_FILENAME
|
|
12
|
+
|
|
13
|
+
ANIMAL_DB_FILENAME = "animal_db.json"
|
|
14
|
+
ANIMAL_DB_PATH = CONFIG_DIR_PATH / ANIMAL_DB_FILENAME
|
|
15
|
+
|
|
16
|
+
CROSS_MODAL_CONFIG_FILENAME = "config_cross_modal.json"
|
|
17
|
+
CROSS_MODAL_CONFIG_PATH = CONFIG_DIR_PATH / CROSS_MODAL_CONFIG_FILENAME
|
|
18
|
+
|
|
19
|
+
DATA_DIR_PATH = ROOT_DIR_PATH / "data"
|
|
20
|
+
|
|
21
|
+
LOG_PATH = ROOT_DIR_PATH / "log"
|
|
22
|
+
|
|
23
|
+
SAMBA_MOUNT_PATH = ROOT_DIR_PATH / "samba_mount"
|
|
24
|
+
SAMBA_BACKUP_DIR_NAME = "backup"
|
|
25
|
+
SAMBA_BACKUP_DIR_PATH = SAMBA_MOUNT_PATH / SAMBA_BACKUP_DIR_NAME
|
|
26
|
+
|
|
27
|
+
SERVICE_DIR_NAME = "services"
|
|
28
|
+
SERVICE_DIR_PATH = ROOT_DIR_PATH / SERVICE_DIR_NAME
|
|
29
|
+
MOUNT_SERVICE_NAME = "mount.service"
|
|
30
|
+
MOUNT_SERVICE_PATH = SERVICE_DIR_PATH / MOUNT_SERVICE_NAME
|
|
31
|
+
SYNC_SERVICE_NAME = "sync.service"
|
|
32
|
+
SYNC_SERVICE_PATH = SERVICE_DIR_PATH / SYNC_SERVICE_NAME
|
|
33
|
+
|
|
34
|
+
MXBI_CONFIG_PATH = CONFIG_DIR_PATH / "mxbi.json"
|
|
35
|
+
SESSION_CONFIG_PATH = CONFIG_DIR_PATH / "session.json"
|
|
36
|
+
OPTIONS_PATH = CONFIG_DIR_PATH / "options.json"
|
|
37
|
+
STAGE_PATH = CONFIG_DIR_PATH / "stage.json"
|
|
38
|
+
SESSION_COUNTER_PATH = CONFIG_DIR_PATH / "session_counter.json"
|
|
39
|
+
|
|
40
|
+
ASSETS_DIR_PATH = ROOT_DIR_PATH / "assets"
|
|
41
|
+
ASSET_CLICKER_PATH = ASSETS_DIR_PATH / "clicker.wav"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import RootModel
|
|
4
|
+
from pygame import Event, Surface
|
|
5
|
+
|
|
6
|
+
from .scene_protocol import SceneProtocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Scenes(RootModel):
|
|
10
|
+
root: list[str]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SceneManager:
|
|
14
|
+
scenes: dict[str, type[SceneProtocol]] = {}
|
|
15
|
+
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
self.current: SceneProtocol | None = None
|
|
18
|
+
self._pending: SceneProtocol | None = None
|
|
19
|
+
|
|
20
|
+
def persist(self, path: Path) -> None:
|
|
21
|
+
scenes = Scenes(root=list(self.scenes.keys()))
|
|
22
|
+
json_data = scenes.model_dump_json()
|
|
23
|
+
|
|
24
|
+
with path.open("w") as f:
|
|
25
|
+
f.write(json_data)
|
|
26
|
+
|
|
27
|
+
def register(self, scene: type[SceneProtocol], name: str | None = None) -> None:
|
|
28
|
+
if name is None:
|
|
29
|
+
name = scene.__name__.lower()
|
|
30
|
+
|
|
31
|
+
self.scenes[name] = scene
|
|
32
|
+
|
|
33
|
+
def switch(self, scene: type[SceneProtocol], defer: bool = True) -> None:
|
|
34
|
+
if defer:
|
|
35
|
+
self._pending = scene()
|
|
36
|
+
else:
|
|
37
|
+
self._switch(scene())
|
|
38
|
+
|
|
39
|
+
def _switch(self, next_scene: SceneProtocol) -> None:
|
|
40
|
+
prev = self.current
|
|
41
|
+
if prev is not None and prev.running:
|
|
42
|
+
prev.quit()
|
|
43
|
+
|
|
44
|
+
self.current = next_scene
|
|
45
|
+
next_scene.start()
|
|
46
|
+
|
|
47
|
+
def apply_pending(self) -> None:
|
|
48
|
+
if self._pending is None:
|
|
49
|
+
return
|
|
50
|
+
next_scene = self._pending
|
|
51
|
+
self._pending = None
|
|
52
|
+
self._switch(next_scene)
|
|
53
|
+
|
|
54
|
+
def handle_event(self, event: Event) -> None:
|
|
55
|
+
if self.current:
|
|
56
|
+
self.current.handle_event(event)
|
|
57
|
+
|
|
58
|
+
def update(self, dt_s: float) -> None:
|
|
59
|
+
if self.current:
|
|
60
|
+
self.current.update(dt_s)
|
|
61
|
+
|
|
62
|
+
def draw(self, screen: Surface) -> None:
|
|
63
|
+
if self.current:
|
|
64
|
+
self.current.draw(screen)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Protocol, runtime_checkable
|
|
2
|
+
|
|
3
|
+
from pygame.event import Event
|
|
4
|
+
from pygame.surface import Surface
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@runtime_checkable
|
|
8
|
+
class SceneProtocol(Protocol):
|
|
9
|
+
_running: bool
|
|
10
|
+
|
|
11
|
+
def start(self) -> None: ...
|
|
12
|
+
|
|
13
|
+
def quit(self) -> None: ...
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def running(self) -> bool: ...
|
|
17
|
+
|
|
18
|
+
def handle_event(self, event: Event) -> None: ...
|
|
19
|
+
|
|
20
|
+
def update(self, dt_s: float) -> None: ...
|
|
21
|
+
|
|
22
|
+
def draw(self, screen: Surface) -> None: ...
|
mxbiflow/scheduler.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from pygame import Event
|
|
2
|
+
from pymxbi.detector.detector import DetectorEvent
|
|
3
|
+
|
|
4
|
+
from .default import IDLE
|
|
5
|
+
from .detector_bridge import EVT_DETECTOR, DetectorMsg
|
|
6
|
+
from .models.session import Session
|
|
7
|
+
from .scene import SceneManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Scheduler:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
session: Session,
|
|
14
|
+
scene_manager: SceneManager,
|
|
15
|
+
) -> None:
|
|
16
|
+
self._session = session
|
|
17
|
+
self._scene_manager = scene_manager
|
|
18
|
+
self._scenes = self._scene_manager.scenes
|
|
19
|
+
|
|
20
|
+
self._need_refresh = False
|
|
21
|
+
|
|
22
|
+
def _mark_refresh(self) -> None:
|
|
23
|
+
self._need_refresh = True
|
|
24
|
+
|
|
25
|
+
def _set_current_animal(self, animal: str) -> None:
|
|
26
|
+
self._session.set_current_animal(animal)
|
|
27
|
+
self._need_refresh = True
|
|
28
|
+
|
|
29
|
+
def _clear_current_animal(self) -> None:
|
|
30
|
+
if self._session.current_animal is None:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
self._session.clear_current_animal()
|
|
34
|
+
self._mark_refresh()
|
|
35
|
+
|
|
36
|
+
def _set_next_stage(self, stage: str) -> None:
|
|
37
|
+
if self._session.current_animal is None:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
self._session.current_animal.set_current_stage(stage)
|
|
41
|
+
self._need_refresh = True
|
|
42
|
+
|
|
43
|
+
def _handle_fault_event(self) -> None: ...
|
|
44
|
+
|
|
45
|
+
def handle_event(self, event: Event) -> None:
|
|
46
|
+
if event.type != EVT_DETECTOR:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
msg: DetectorMsg = event.msg
|
|
50
|
+
|
|
51
|
+
match msg.kind:
|
|
52
|
+
case DetectorEvent.FAULT_DETECTED:
|
|
53
|
+
self._handle_fault_event()
|
|
54
|
+
|
|
55
|
+
case DetectorEvent.ANIMAL_ENTERED:
|
|
56
|
+
if msg.animal is None:
|
|
57
|
+
return
|
|
58
|
+
self._set_current_animal(msg.animal)
|
|
59
|
+
|
|
60
|
+
case DetectorEvent.ANIMAL_LEFT:
|
|
61
|
+
self._clear_current_animal()
|
|
62
|
+
|
|
63
|
+
def _shoud_refresh(self) -> bool:
|
|
64
|
+
current = self._scene_manager.current
|
|
65
|
+
return (
|
|
66
|
+
current is not None
|
|
67
|
+
and not isinstance(current, IDLE)
|
|
68
|
+
and not current.running
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def update(self) -> None:
|
|
72
|
+
if self._shoud_refresh():
|
|
73
|
+
self._mark_refresh()
|
|
74
|
+
|
|
75
|
+
if not self._need_refresh:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
self._need_refresh = False
|
|
79
|
+
self._refresh_by_state()
|
|
80
|
+
|
|
81
|
+
def _refresh_by_state(self) -> None:
|
|
82
|
+
animal = self._session.current_animal
|
|
83
|
+
|
|
84
|
+
if animal is None:
|
|
85
|
+
if not isinstance(self._scene_manager.current, IDLE):
|
|
86
|
+
self._scene_manager.switch(self._scenes[IDLE.__name__.lower()])
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
stage = animal.current_stage.stage_name
|
|
90
|
+
self._scene_manager.switch(self._scenes[stage])
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from enum import StrEnum, auto
|
|
2
|
+
from typing import TypeAlias
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
|
|
6
|
+
LevelID: TypeAlias = int
|
|
7
|
+
MonkeyName: TypeAlias = str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Result(StrEnum):
|
|
11
|
+
CORRECT = auto()
|
|
12
|
+
INCORRECT = auto()
|
|
13
|
+
TIMEOUT = auto()
|
|
14
|
+
CANCEL = auto()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TouchEvent(BaseModel):
|
|
18
|
+
time: float
|
|
19
|
+
x: int
|
|
20
|
+
y: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseTrialConfig(BaseModel):
|
|
24
|
+
model_config = ConfigDict(frozen=True)
|
|
25
|
+
|
|
26
|
+
# basic config
|
|
27
|
+
level: int
|
|
28
|
+
|
|
29
|
+
# visual stimulation config
|
|
30
|
+
stimulation_size: int
|
|
31
|
+
|
|
32
|
+
# audio stimulation config
|
|
33
|
+
stimulus_duration: int
|
|
34
|
+
|
|
35
|
+
# trial lifecycle
|
|
36
|
+
time_out: int
|
|
37
|
+
inter_trial_interval: int
|
|
38
|
+
|
|
39
|
+
# reward config
|
|
40
|
+
reward_duration: int
|
|
41
|
+
reward_delay: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BaseTrialData(BaseModel):
|
|
45
|
+
animal: str
|
|
46
|
+
trial_id: int
|
|
47
|
+
current_level_trial_id: int
|
|
48
|
+
trial_start_time: float
|
|
49
|
+
trial_end_time: float
|
|
50
|
+
result: Result
|
|
51
|
+
correct_rate: float
|
|
52
|
+
touch_events: list[TouchEvent]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BaseDataToShow(BaseModel):
|
|
56
|
+
name: str
|
|
57
|
+
id: int
|
|
58
|
+
level_id: int
|
|
59
|
+
level: int
|
|
60
|
+
rewards: int
|
|
61
|
+
correct: int
|
|
62
|
+
incorrect: int
|
|
63
|
+
timeout: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PersistentData(BaseModel):
|
|
67
|
+
rewards: int
|
|
68
|
+
correct: int
|
|
69
|
+
incorrect: int
|
|
70
|
+
timeout: int
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
{
|
|
2
|
+
"default": {
|
|
3
|
+
"condition": {
|
|
4
|
+
"config": {
|
|
5
|
+
"evaluation_interval": 2,
|
|
6
|
+
"difficulty_increase_threshold": 0.8,
|
|
7
|
+
"difficulty_decrease_threshold": 0.45,
|
|
8
|
+
"next_task": "gngsid_discriminate_stage"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"params": {
|
|
12
|
+
"stimulation_size": 250,
|
|
13
|
+
"visual_stimulus_delay": 500,
|
|
14
|
+
"stimulus_freq": 2000,
|
|
15
|
+
"stimulus_freq_duration": 100,
|
|
16
|
+
"stimulus_interval": 130,
|
|
17
|
+
"time_out": 10000,
|
|
18
|
+
"inter_trial_interval": 2000,
|
|
19
|
+
"reward_duration": 1000,
|
|
20
|
+
"reward_delay": 500
|
|
21
|
+
},
|
|
22
|
+
"levels_table": {
|
|
23
|
+
"0": {
|
|
24
|
+
"level": 0,
|
|
25
|
+
"min_stimulus_duration": 2500,
|
|
26
|
+
"max_stimulus_duration": 6500,
|
|
27
|
+
"go_task_prob": 0.8,
|
|
28
|
+
"nogo_task_prob": 0.2
|
|
29
|
+
},
|
|
30
|
+
"1": {
|
|
31
|
+
"level": 1,
|
|
32
|
+
"min_stimulus_duration": 1000,
|
|
33
|
+
"max_stimulus_duration": 2500,
|
|
34
|
+
"go_task_prob": 0.8,
|
|
35
|
+
"nogo_task_prob": 0.2
|
|
36
|
+
},
|
|
37
|
+
"2": {
|
|
38
|
+
"level": 2,
|
|
39
|
+
"min_stimulus_duration": 3500,
|
|
40
|
+
"max_stimulus_duration": 6500,
|
|
41
|
+
"go_task_prob": 0.8,
|
|
42
|
+
"nogo_task_prob": 0.2
|
|
43
|
+
},
|
|
44
|
+
"3": {
|
|
45
|
+
"level": 3,
|
|
46
|
+
"min_stimulus_duration": 1000,
|
|
47
|
+
"max_stimulus_duration": 2500,
|
|
48
|
+
"go_task_prob": 0.8,
|
|
49
|
+
"nogo_task_prob": 0.2
|
|
50
|
+
},
|
|
51
|
+
"4": {
|
|
52
|
+
"level": 4,
|
|
53
|
+
"min_stimulus_duration": 3500,
|
|
54
|
+
"max_stimulus_duration": 6500,
|
|
55
|
+
"go_task_prob": 0.8,
|
|
56
|
+
"nogo_task_prob": 0.2
|
|
57
|
+
},
|
|
58
|
+
"5": {
|
|
59
|
+
"level": 5,
|
|
60
|
+
"min_stimulus_duration": 1000,
|
|
61
|
+
"max_stimulus_duration": 2500,
|
|
62
|
+
"go_task_prob": 0.8,
|
|
63
|
+
"nogo_task_prob": 0.2
|
|
64
|
+
},
|
|
65
|
+
"6": {
|
|
66
|
+
"level": 6,
|
|
67
|
+
"min_stimulus_duration": 3500,
|
|
68
|
+
"max_stimulus_duration": 6500,
|
|
69
|
+
"go_task_prob": 0.8,
|
|
70
|
+
"nogo_task_prob": 0.2
|
|
71
|
+
},
|
|
72
|
+
"7": {
|
|
73
|
+
"level": 7,
|
|
74
|
+
"min_stimulus_duration": 1000,
|
|
75
|
+
"max_stimulus_duration": 3500,
|
|
76
|
+
"go_task_prob": 0.8,
|
|
77
|
+
"nogo_task_prob": 0.2
|
|
78
|
+
},
|
|
79
|
+
"8": {
|
|
80
|
+
"level": 8,
|
|
81
|
+
"min_stimulus_duration": 3500,
|
|
82
|
+
"max_stimulus_duration": 6500,
|
|
83
|
+
"go_task_prob": 0.8,
|
|
84
|
+
"nogo_task_prob": 0.2
|
|
85
|
+
},
|
|
86
|
+
"9": {
|
|
87
|
+
"level": 9,
|
|
88
|
+
"min_stimulus_duration": 1000,
|
|
89
|
+
"max_stimulus_duration": 2500,
|
|
90
|
+
"go_task_prob": 0.8,
|
|
91
|
+
"nogo_task_prob": 0.2
|
|
92
|
+
},
|
|
93
|
+
"10": {
|
|
94
|
+
"level": 10,
|
|
95
|
+
"min_stimulus_duration": 3500,
|
|
96
|
+
"max_stimulus_duration": 6500,
|
|
97
|
+
"go_task_prob": 0.8,
|
|
98
|
+
"nogo_task_prob": 0.2
|
|
99
|
+
},
|
|
100
|
+
"11": {
|
|
101
|
+
"level": 11,
|
|
102
|
+
"min_stimulus_duration": 1000,
|
|
103
|
+
"max_stimulus_duration": 2500,
|
|
104
|
+
"go_task_prob": 0.8,
|
|
105
|
+
"nogo_task_prob": 0.2
|
|
106
|
+
},
|
|
107
|
+
"12": {
|
|
108
|
+
"level": 12,
|
|
109
|
+
"min_stimulus_duration": 3000,
|
|
110
|
+
"max_stimulus_duration": 5000,
|
|
111
|
+
"go_task_prob": 0.5,
|
|
112
|
+
"nogo_task_prob": 0.5
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|