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,122 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Final
|
|
2
|
+
|
|
3
|
+
from mxbi.data_logger import DataLogger, DataLoggerType
|
|
4
|
+
from mxbi.tasks.two_alternative_choice.models import PersistentData, Result
|
|
5
|
+
from mxbi.tasks.two_alternative_choice.stages.size_reduction_stage.size_reduction_models import (
|
|
6
|
+
config,
|
|
7
|
+
)
|
|
8
|
+
from mxbi.tasks.two_alternative_choice.tasks.touch.touch_scene import TwoACTouchScene
|
|
9
|
+
from mxbi.utils.logger import logger
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from mxbi.models.animal import AnimalState, ScheduleCondition
|
|
13
|
+
from mxbi.models.session import SessionState
|
|
14
|
+
from mxbi.models.task import Feedback
|
|
15
|
+
from mxbi.tasks.two_alternative_choice.stages.size_reduction_stage.size_reduction_models import (
|
|
16
|
+
SizeReductionStageConfig,
|
|
17
|
+
)
|
|
18
|
+
from mxbi.theater import Theater
|
|
19
|
+
|
|
20
|
+
_presistent_data: dict[str, PersistentData] = {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TWOACSizeReductionStage:
|
|
24
|
+
STAGE_NAME: Final[str] = "twoac_size_reduction_stage"
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
theater: "Theater",
|
|
29
|
+
session_state: "SessionState",
|
|
30
|
+
animal_state: "AnimalState",
|
|
31
|
+
) -> None:
|
|
32
|
+
self._theater = theater
|
|
33
|
+
self._session_state = session_state
|
|
34
|
+
self._animal_state = animal_state
|
|
35
|
+
|
|
36
|
+
self._stage_config = self._load_stage_config(animal_state.name)
|
|
37
|
+
|
|
38
|
+
_levels_config = self._stage_config.levels_table[animal_state.level]
|
|
39
|
+
_config = self._stage_config.trial_config
|
|
40
|
+
|
|
41
|
+
_config.level = _levels_config.level
|
|
42
|
+
_config.stimulation_size = _levels_config.stimulation_size
|
|
43
|
+
|
|
44
|
+
self._data_logger = DataLogger(
|
|
45
|
+
self._session_state,
|
|
46
|
+
self._animal_state.name,
|
|
47
|
+
self.STAGE_NAME,
|
|
48
|
+
DataLoggerType.JSONL,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self._presistent_data = _presistent_data.get(self._animal_state.name)
|
|
52
|
+
|
|
53
|
+
if self._presistent_data is None:
|
|
54
|
+
self._presistent_data = PersistentData(
|
|
55
|
+
rewards=0,
|
|
56
|
+
correct=0,
|
|
57
|
+
incorrect=0,
|
|
58
|
+
timeout=0,
|
|
59
|
+
)
|
|
60
|
+
_presistent_data[self._animal_state.name] = self._presistent_data
|
|
61
|
+
|
|
62
|
+
self._task = TwoACTouchScene(
|
|
63
|
+
theater,
|
|
64
|
+
session_state.session_config,
|
|
65
|
+
animal_state,
|
|
66
|
+
session_state.session_config.screen_type,
|
|
67
|
+
_config,
|
|
68
|
+
self._presistent_data,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def start(self) -> "Feedback":
|
|
72
|
+
trial_data = self._task.start()
|
|
73
|
+
self._data_logger.save(trial_data.model_dump())
|
|
74
|
+
|
|
75
|
+
feedback = self._handle_result(trial_data.result)
|
|
76
|
+
logger.debug(
|
|
77
|
+
f"{self.STAGE_NAME}: "
|
|
78
|
+
f"session_id={self._session_state.session_id}, "
|
|
79
|
+
f"animal_name={self._animal_state.name}, "
|
|
80
|
+
f"animal_level={self._animal_state.level}, "
|
|
81
|
+
f"state_name={self.STAGE_NAME}, "
|
|
82
|
+
f"result={trial_data}, "
|
|
83
|
+
f"feedback={feedback}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return feedback
|
|
87
|
+
|
|
88
|
+
def _load_stage_config(self, monkey: str) -> "SizeReductionStageConfig":
|
|
89
|
+
stage_config = config.root.get(monkey) or config.root.get("default")
|
|
90
|
+
if stage_config is None:
|
|
91
|
+
raise ValueError("No default stage config found")
|
|
92
|
+
return stage_config
|
|
93
|
+
|
|
94
|
+
def _handle_result(self, result: "Result") -> "Feedback":
|
|
95
|
+
feedback = False
|
|
96
|
+
match result:
|
|
97
|
+
case Result.CORRECT:
|
|
98
|
+
_presistent_data[self._animal_state.name].correct += 1
|
|
99
|
+
feedback = True
|
|
100
|
+
case Result.INCORRECT:
|
|
101
|
+
_presistent_data[self._animal_state.name].incorrect += 1
|
|
102
|
+
feedback = False
|
|
103
|
+
case Result.TIMEOUT:
|
|
104
|
+
_presistent_data[self._animal_state.name].timeout += 1
|
|
105
|
+
feedback = False
|
|
106
|
+
case Result.CANCEL:
|
|
107
|
+
feedback = False
|
|
108
|
+
|
|
109
|
+
return feedback
|
|
110
|
+
|
|
111
|
+
def quit(self) -> None:
|
|
112
|
+
self._task.cancle()
|
|
113
|
+
|
|
114
|
+
def on_idle(self) -> None:
|
|
115
|
+
self._task.cancle()
|
|
116
|
+
|
|
117
|
+
def on_return(self) -> None:
|
|
118
|
+
self._task.cancle()
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def condition(self) -> "ScheduleCondition | None":
|
|
122
|
+
return self._stage_config.condition
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from mxbi.tasks.two_alternative_choice.models import (
|
|
2
|
+
BaseDataToShow,
|
|
3
|
+
BaseTrialConfig,
|
|
4
|
+
BaseTrialData,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TrialConfig(BaseTrialConfig):
|
|
9
|
+
stimulus_freq: int
|
|
10
|
+
stimulus_freq_duration: int
|
|
11
|
+
|
|
12
|
+
stimulus_interval: int
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TrialData(BaseTrialData):
|
|
16
|
+
trial_config: TrialConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DataToShow(BaseDataToShow): ...
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
from concurrent.futures import Future
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from math import ceil
|
|
4
|
+
from tkinter import CENTER, Canvas, Event
|
|
5
|
+
from typing import TYPE_CHECKING, Final
|
|
6
|
+
|
|
7
|
+
from mxbi.tasks.two_alternative_choice.assets.starter import Starter
|
|
8
|
+
from mxbi.tasks.two_alternative_choice.models import Result, TouchEvent
|
|
9
|
+
from mxbi.tasks.two_alternative_choice.tasks.touch.touch_models import (
|
|
10
|
+
DataToShow,
|
|
11
|
+
TrialData,
|
|
12
|
+
)
|
|
13
|
+
from mxbi.utils.aplayer import ToneConfig
|
|
14
|
+
from mxbi.utils.tkinter.components.canvas_with_border import CanvasWithInnerBorder
|
|
15
|
+
from mxbi.utils.tkinter.components.showdata_widget import ShowDataWidget
|
|
16
|
+
from numpy import int16
|
|
17
|
+
from numpy.typing import NDArray
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from mxbi.models.animal import AnimalState
|
|
21
|
+
from mxbi.models.session import ScreenConfig, SessionConfig
|
|
22
|
+
from mxbi.tasks.two_alternative_choice.models import PersistentData
|
|
23
|
+
from mxbi.tasks.two_alternative_choice.tasks.touch.touch_models import TrialConfig
|
|
24
|
+
from mxbi.theater import Theater
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TwoACTouchScene:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
theater: "Theater",
|
|
31
|
+
session: "SessionConfig",
|
|
32
|
+
animal_state: "AnimalState",
|
|
33
|
+
screen_type: "ScreenConfig",
|
|
34
|
+
trial_config: "TrialConfig",
|
|
35
|
+
persistent_data: "PersistentData",
|
|
36
|
+
) -> None:
|
|
37
|
+
self._theater: "Final[Theater]" = theater
|
|
38
|
+
self._animal_state: "Final[AnimalState]" = animal_state
|
|
39
|
+
self._screen_type: "Final[ScreenConfig]" = screen_type
|
|
40
|
+
self._trial_config: "Final[TrialConfig]" = trial_config
|
|
41
|
+
self._persistent_data: "Final[PersistentData]" = persistent_data
|
|
42
|
+
|
|
43
|
+
self._tone = self._prepare_stimulus()
|
|
44
|
+
self._standard_reward_stimulus = self._theater.new_standard_reward_stimulus(
|
|
45
|
+
self._trial_config.stimulus_duration
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self._on_trial_start()
|
|
49
|
+
|
|
50
|
+
# region public api
|
|
51
|
+
def start(self) -> "TrialData":
|
|
52
|
+
self._theater.mainloop()
|
|
53
|
+
|
|
54
|
+
return self._data
|
|
55
|
+
|
|
56
|
+
def cancle(self) -> None:
|
|
57
|
+
self._data.result = Result.CANCEL
|
|
58
|
+
self._theater.aplayer.stop()
|
|
59
|
+
self._on_trial_end()
|
|
60
|
+
|
|
61
|
+
# endregion
|
|
62
|
+
|
|
63
|
+
# region lifecycle
|
|
64
|
+
def _on_trial_start(self) -> None:
|
|
65
|
+
self._create_view()
|
|
66
|
+
self._init_data()
|
|
67
|
+
self._bind_events()
|
|
68
|
+
|
|
69
|
+
def _on_inter_trial(self) -> None:
|
|
70
|
+
self._background.after(
|
|
71
|
+
self._trial_config.inter_trial_interval, self._on_trial_end
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _on_trial_end(self) -> None:
|
|
75
|
+
self._background.destroy()
|
|
76
|
+
self._theater.root.quit()
|
|
77
|
+
|
|
78
|
+
# endregion
|
|
79
|
+
|
|
80
|
+
# region views
|
|
81
|
+
def _create_view(self) -> None:
|
|
82
|
+
self._create_background()
|
|
83
|
+
self._create_show_data_widget()
|
|
84
|
+
self._create_target()
|
|
85
|
+
|
|
86
|
+
def _create_background(self) -> None:
|
|
87
|
+
self._background = CanvasWithInnerBorder(
|
|
88
|
+
master=self._theater.root,
|
|
89
|
+
bg="black",
|
|
90
|
+
width=self._screen_type.width,
|
|
91
|
+
height=self._screen_type.height,
|
|
92
|
+
border_width=40,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self._background.place(relx=0.5, rely=0.5, anchor="center")
|
|
96
|
+
|
|
97
|
+
def _create_show_data_widget(self) -> None:
|
|
98
|
+
self._show_data_widget = ShowDataWidget(self._background)
|
|
99
|
+
self._show_data_widget.place(relx=0, rely=1, anchor="sw")
|
|
100
|
+
data = DataToShow(
|
|
101
|
+
name=self._animal_state.name,
|
|
102
|
+
id=self._animal_state.trial_id,
|
|
103
|
+
level_id=self._animal_state.current_level_trial_id,
|
|
104
|
+
level=self._trial_config.level,
|
|
105
|
+
rewards=self._persistent_data.rewards,
|
|
106
|
+
correct=self._animal_state.correct_trial,
|
|
107
|
+
incorrect=self._persistent_data.incorrect,
|
|
108
|
+
timeout=self._persistent_data.timeout,
|
|
109
|
+
)
|
|
110
|
+
self._show_data_widget.show_data(data.model_dump())
|
|
111
|
+
|
|
112
|
+
def _create_target(self) -> None:
|
|
113
|
+
xshift = 240
|
|
114
|
+
xcenter = self._screen_type.width * 0.5 + xshift
|
|
115
|
+
ycenter = self._screen_type.height * 0.5
|
|
116
|
+
|
|
117
|
+
self._trigger_canvas = Starter(
|
|
118
|
+
self._background, self._trial_config.stimulation_size
|
|
119
|
+
)
|
|
120
|
+
self._trigger_canvas.place(x=xcenter, y=ycenter, anchor="center")
|
|
121
|
+
|
|
122
|
+
def _create_wrong_view(self) -> None:
|
|
123
|
+
self._trigger_canvas = Canvas(
|
|
124
|
+
self._background,
|
|
125
|
+
bg="grey",
|
|
126
|
+
width=self._screen_type.width,
|
|
127
|
+
height=self._screen_type.height,
|
|
128
|
+
)
|
|
129
|
+
self._trigger_canvas.place(relx=0.5, rely=0.5, anchor=CENTER)
|
|
130
|
+
|
|
131
|
+
# endregion
|
|
132
|
+
|
|
133
|
+
# region event binding
|
|
134
|
+
def _bind_events(self) -> None:
|
|
135
|
+
# Manual reward
|
|
136
|
+
self._background.focus_set()
|
|
137
|
+
self._background.bind("<r>", lambda e: self._give_standard_stimulus())
|
|
138
|
+
self._background.bind("<s>", lambda e: self._theater.caputre(self._background))
|
|
139
|
+
|
|
140
|
+
# Trigger event
|
|
141
|
+
self._background.bind("<ButtonPress>", self._on_background_touched)
|
|
142
|
+
self._trigger_canvas.bind("<ButtonPress>", self._on_touched)
|
|
143
|
+
|
|
144
|
+
# Timeout event
|
|
145
|
+
self._trigger_canvas.after(self._trial_config.time_out, self._on_timeout)
|
|
146
|
+
|
|
147
|
+
# endregion
|
|
148
|
+
|
|
149
|
+
# region event handlers
|
|
150
|
+
def _on_touched(self, event: Event) -> None:
|
|
151
|
+
self._data.touch_events.append(
|
|
152
|
+
TouchEvent(time=datetime.now().timestamp(), x=event.x, y=event.y)
|
|
153
|
+
)
|
|
154
|
+
self._background.unbind("<ButtonPress>")
|
|
155
|
+
self._trigger_canvas.destroy()
|
|
156
|
+
|
|
157
|
+
future = self._give_stimulus()
|
|
158
|
+
future.add_done_callback(self._on_stimulus_complete)
|
|
159
|
+
|
|
160
|
+
def _on_background_touched(self, event: Event) -> None:
|
|
161
|
+
self._data.touch_events.append(
|
|
162
|
+
TouchEvent(time=datetime.now().timestamp(), x=event.x, y=event.y)
|
|
163
|
+
)
|
|
164
|
+
self._background.unbind("<ButtonPress>")
|
|
165
|
+
self._trigger_canvas.destroy()
|
|
166
|
+
|
|
167
|
+
self._on_incorrect()
|
|
168
|
+
|
|
169
|
+
def _on_correct(self) -> None:
|
|
170
|
+
self._give_reward()
|
|
171
|
+
self._data.result = Result.CORRECT
|
|
172
|
+
self._data.correct_rate = (self._animal_state.correct_trial + 1) / (
|
|
173
|
+
self._animal_state.current_level_trial_id + 1
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
self._on_inter_trial()
|
|
177
|
+
|
|
178
|
+
def _on_incorrect(self) -> None:
|
|
179
|
+
self._data.result = Result.INCORRECT
|
|
180
|
+
self._data.correct_rate = self._animal_state.correct_trial / (
|
|
181
|
+
self._animal_state.current_level_trial_id + 1
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
self._create_wrong_view()
|
|
185
|
+
self._on_inter_trial()
|
|
186
|
+
|
|
187
|
+
def _on_timeout(self) -> None:
|
|
188
|
+
self._data.result = Result.TIMEOUT
|
|
189
|
+
self._data.correct_rate = self._animal_state.correct_trial / (
|
|
190
|
+
self._animal_state.current_level_trial_id + 1
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self._create_wrong_view()
|
|
194
|
+
self._on_inter_trial()
|
|
195
|
+
|
|
196
|
+
# endregion
|
|
197
|
+
|
|
198
|
+
# region sitimulus and reward
|
|
199
|
+
def _prepare_stimulus(self) -> "NDArray[int16]":
|
|
200
|
+
unit_duration = (
|
|
201
|
+
self._trial_config.stimulus_freq_duration
|
|
202
|
+
+ self._trial_config.stimulus_freq_duration
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
times = ceil(self._trial_config.stimulus_duration / unit_duration)
|
|
206
|
+
times = max(times, 1)
|
|
207
|
+
|
|
208
|
+
freq_1 = ToneConfig(
|
|
209
|
+
frequency=self._trial_config.stimulus_freq,
|
|
210
|
+
duration=self._trial_config.stimulus_freq_duration,
|
|
211
|
+
)
|
|
212
|
+
freq_2 = ToneConfig(frequency=0, duration=self._trial_config.stimulus_interval)
|
|
213
|
+
|
|
214
|
+
return self._theater.aplayer.generate_stimulus([freq_1, freq_2], times)
|
|
215
|
+
|
|
216
|
+
def _give_stimulus(self) -> "Future[bool]":
|
|
217
|
+
return self._theater.aplayer.play_stimulus(self._tone)
|
|
218
|
+
|
|
219
|
+
def _on_stimulus_complete(self, future: "Future[bool]") -> None:
|
|
220
|
+
if future.result():
|
|
221
|
+
self._trigger_canvas.after(
|
|
222
|
+
self._trial_config.reward_delay,
|
|
223
|
+
lambda: self._on_correct(),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _give_reward(self) -> None:
|
|
227
|
+
self._persistent_data.rewards += 1
|
|
228
|
+
self._theater.reward.give_reward(self._trial_config.reward_duration)
|
|
229
|
+
|
|
230
|
+
def _give_standard_stimulus(self) -> None:
|
|
231
|
+
self._standard_reward_stimulus.play(self._trial_config.reward_duration)
|
|
232
|
+
|
|
233
|
+
# endregion
|
|
234
|
+
|
|
235
|
+
# region data
|
|
236
|
+
def _init_data(self) -> None:
|
|
237
|
+
self._data = TrialData(
|
|
238
|
+
animal=self._animal_state.name,
|
|
239
|
+
trial_id=self._animal_state.trial_id,
|
|
240
|
+
current_level_trial_id=self._animal_state.current_level_trial_id,
|
|
241
|
+
trial_config=self._trial_config,
|
|
242
|
+
trial_start_time=datetime.now().timestamp(),
|
|
243
|
+
trial_end_time=0,
|
|
244
|
+
result=Result.TIMEOUT,
|
|
245
|
+
correct_rate=0,
|
|
246
|
+
touch_events=[],
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# endregion
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from pygame import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class _Task:
|
|
9
|
+
interval_ms: int
|
|
10
|
+
callback: Callable[[], None]
|
|
11
|
+
repeat: bool
|
|
12
|
+
next_fire_ms: int
|
|
13
|
+
cancelled: bool = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FrameTimer:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._tasks: list[_Task] = []
|
|
19
|
+
|
|
20
|
+
def after(self, delay_ms: int, callback: Callable[[], None]) -> _Task:
|
|
21
|
+
now = time.get_ticks()
|
|
22
|
+
task = _Task(delay_ms, callback, False, now + delay_ms)
|
|
23
|
+
self._tasks.append(task)
|
|
24
|
+
return task
|
|
25
|
+
|
|
26
|
+
def every(self, interval_ms: int, callback: Callable[[], None]) -> _Task:
|
|
27
|
+
now = time.get_ticks()
|
|
28
|
+
task = _Task(interval_ms, callback, True, now + interval_ms)
|
|
29
|
+
self._tasks.append(task)
|
|
30
|
+
return task
|
|
31
|
+
|
|
32
|
+
def cancel(self, task: _Task) -> None:
|
|
33
|
+
task.cancelled = True
|
|
34
|
+
|
|
35
|
+
def update(self) -> None:
|
|
36
|
+
now = time.get_ticks()
|
|
37
|
+
for task in self._tasks:
|
|
38
|
+
if task.cancelled:
|
|
39
|
+
continue
|
|
40
|
+
if now >= task.next_fire_ms:
|
|
41
|
+
task.callback()
|
|
42
|
+
if task.repeat and not task.cancelled:
|
|
43
|
+
task.next_fire_ms += task.interval_ms
|
|
44
|
+
else:
|
|
45
|
+
task.cancelled = True
|
|
46
|
+
|
|
47
|
+
self._tasks = [task for task in self._tasks if not task.cancelled]
|
|
File without changes
|
mxbiflow/tmp_email.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from mxbi.utils.logger import logger
|
|
2
|
+
from pymotego.send_email import EmailAttachment, EmailClient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def send_email(subject: str, body: str, attachments: list[EmailAttachment]) -> None:
|
|
6
|
+
with EmailClient() as client:
|
|
7
|
+
response = client.send(
|
|
8
|
+
subject=subject,
|
|
9
|
+
html_body=body,
|
|
10
|
+
attachments=attachments,
|
|
11
|
+
)
|
|
12
|
+
logger.info(f"Status: {response.status_code}")
|
|
13
|
+
logger.info(f"Body: {response.text}")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from PySide6.QtCore import Qt, Signal
|
|
2
|
+
from PySide6.QtWidgets import QComboBox, QFormLayout, QLabel, QLineEdit, QMenu
|
|
3
|
+
|
|
4
|
+
from ...config_store import ConfigStore
|
|
5
|
+
from ...models.animal import AnimalConfig
|
|
6
|
+
from ...path import STAGE_PATH
|
|
7
|
+
from ...scene import Scenes
|
|
8
|
+
from .card import CardFrame
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AnimalCard(CardFrame):
|
|
12
|
+
remove_requested = Signal()
|
|
13
|
+
|
|
14
|
+
def __init__(self, parent, animals: dict[str, str]):
|
|
15
|
+
super().__init__(parent=parent, object_name="card")
|
|
16
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
17
|
+
self.customContextMenuRequested.connect(self._on_context_menu)
|
|
18
|
+
|
|
19
|
+
items = list(animals.items())
|
|
20
|
+
self._animal_ids = [animal_id for animal_id, _name in items]
|
|
21
|
+
self._animal_names = [name for _animal_id, name in items]
|
|
22
|
+
|
|
23
|
+
layout = QFormLayout(self)
|
|
24
|
+
layout.setContentsMargins(8, 8, 8, 8)
|
|
25
|
+
|
|
26
|
+
label_animal_name = QLabel("animal", self)
|
|
27
|
+
self.combo_animal_name = QComboBox(self)
|
|
28
|
+
self.combo_animal_name.addItems(self._animal_names)
|
|
29
|
+
self.combo_animal_name.currentIndexChanged.connect(self._on_animal_changed)
|
|
30
|
+
layout.addRow(label_animal_name, self.combo_animal_name)
|
|
31
|
+
|
|
32
|
+
lable_animal_id = QLabel("id", self)
|
|
33
|
+
self.line_animal_id = QLineEdit(self)
|
|
34
|
+
self.line_animal_id.setReadOnly(True)
|
|
35
|
+
self.line_animal_id.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
36
|
+
layout.addRow(lable_animal_id, self.line_animal_id)
|
|
37
|
+
|
|
38
|
+
label_stage = QLabel("stage", self)
|
|
39
|
+
items = ConfigStore(STAGE_PATH, Scenes).value
|
|
40
|
+
self.combo_stage = QComboBox(self)
|
|
41
|
+
self.combo_stage.addItems([i for i in items.root])
|
|
42
|
+
self.combo_stage.setCurrentText("idle")
|
|
43
|
+
layout.addRow(label_stage, self.combo_stage)
|
|
44
|
+
|
|
45
|
+
label_level = QLabel("level", self)
|
|
46
|
+
self.combo_level = QComboBox(self)
|
|
47
|
+
self.combo_level.addItems(["0", "1"])
|
|
48
|
+
self.combo_level.setCurrentText("0")
|
|
49
|
+
layout.addRow(label_level, self.combo_level)
|
|
50
|
+
|
|
51
|
+
self._sync_animal_id()
|
|
52
|
+
|
|
53
|
+
def _on_animal_changed(self, _index: int) -> None:
|
|
54
|
+
self._sync_animal_id()
|
|
55
|
+
|
|
56
|
+
def _sync_animal_id(self) -> None:
|
|
57
|
+
index = self.combo_animal_name.currentIndex()
|
|
58
|
+
if index < 0 or index >= len(self._animal_ids):
|
|
59
|
+
self.line_animal_id.setText("")
|
|
60
|
+
return
|
|
61
|
+
self.line_animal_id.setText(self._animal_ids[index])
|
|
62
|
+
|
|
63
|
+
def animal_id(self) -> str:
|
|
64
|
+
return self.line_animal_id.text()
|
|
65
|
+
|
|
66
|
+
def animal_name(self) -> str:
|
|
67
|
+
return self.combo_animal_name.currentText()
|
|
68
|
+
|
|
69
|
+
def _on_context_menu(self, pos):
|
|
70
|
+
menu = QMenu(self)
|
|
71
|
+
action = menu.addAction("Remove")
|
|
72
|
+
action.triggered.connect(lambda _checked=False: self.remove_requested.emit())
|
|
73
|
+
menu.exec(self.mapToGlobal(pos))
|
|
74
|
+
|
|
75
|
+
def load_config(self, config: AnimalConfig) -> None:
|
|
76
|
+
self.combo_animal_name.setCurrentText(config.name)
|
|
77
|
+
self.combo_stage.setCurrentText(config.stage)
|
|
78
|
+
self.combo_level.setCurrentText(str(config.level))
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def result(self) -> AnimalConfig:
|
|
82
|
+
return AnimalConfig(
|
|
83
|
+
rfid_id=self.animal_id(),
|
|
84
|
+
name=self.animal_name(),
|
|
85
|
+
stage=self.combo_stage.currentText(),
|
|
86
|
+
level=int(self.combo_level.currentText()),
|
|
87
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pymxbi import MXBIModel
|
|
4
|
+
from pymxbi.platform import PlatformEnum
|
|
5
|
+
from pymxbi.screen import get_screen_size
|
|
6
|
+
from PySide6.QtCore import Signal
|
|
7
|
+
from PySide6.QtWidgets import QComboBox, QFormLayout, QGroupBox, QLabel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseConfig(QGroupBox):
|
|
11
|
+
changed = Signal(str)
|
|
12
|
+
|
|
13
|
+
def __init__(self, parent, mxbi_options: list[str]):
|
|
14
|
+
super().__init__(parent)
|
|
15
|
+
|
|
16
|
+
self.setTitle("Base config")
|
|
17
|
+
|
|
18
|
+
self._layout = QFormLayout()
|
|
19
|
+
self.setLayout(self._layout)
|
|
20
|
+
|
|
21
|
+
self._label_mxbi = QLabel("mxbi id:")
|
|
22
|
+
self._combo_mxbi = QComboBox()
|
|
23
|
+
self._combo_mxbi.addItems(mxbi_options)
|
|
24
|
+
self._layout.addRow(self._label_mxbi, self._combo_mxbi)
|
|
25
|
+
|
|
26
|
+
self._label_platform = QLabel("platform:")
|
|
27
|
+
self._combo_platform = QComboBox()
|
|
28
|
+
self._combo_platform.addItems([platform.value for platform in PlatformEnum])
|
|
29
|
+
self._layout.addRow(self._label_platform, self._combo_platform)
|
|
30
|
+
|
|
31
|
+
self._label_screen = QLabel("screen:")
|
|
32
|
+
self._combo_screen = QComboBox()
|
|
33
|
+
|
|
34
|
+
for screen in get_screen_size():
|
|
35
|
+
self._combo_screen.addItem(
|
|
36
|
+
f"{screen.width} * {screen.height}", (screen.width, screen.height)
|
|
37
|
+
)
|
|
38
|
+
self._layout.addRow(self._label_screen, self._combo_screen)
|
|
39
|
+
|
|
40
|
+
self._bind_events()
|
|
41
|
+
|
|
42
|
+
def _emit_changed(self, msg: str) -> None:
|
|
43
|
+
self.changed.emit(msg)
|
|
44
|
+
|
|
45
|
+
def _bind_events(self) -> None:
|
|
46
|
+
self._combo_mxbi.currentTextChanged.connect(self._emit_changed)
|
|
47
|
+
self._combo_platform.currentTextChanged.connect(self._emit_changed)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def mxbi_id(self) -> str:
|
|
51
|
+
return self._combo_mxbi.currentText()
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def platform(self) -> str:
|
|
55
|
+
return self._combo_platform.currentText()
|
|
56
|
+
|
|
57
|
+
def load_from_model(self, model: MXBIModel) -> None:
|
|
58
|
+
self._combo_mxbi.setCurrentText(str(model.mxbi_id))
|
|
59
|
+
self._combo_platform.setCurrentText(str(model.platform))
|
|
60
|
+
|
|
61
|
+
def apply_to_model(self, model: MXBIModel) -> None:
|
|
62
|
+
mxbi_id_text = self._combo_mxbi.currentText().strip()
|
|
63
|
+
try:
|
|
64
|
+
model.mxbi_id = int(mxbi_id_text) if mxbi_id_text else 0
|
|
65
|
+
except ValueError:
|
|
66
|
+
model.mxbi_id = 0
|
|
67
|
+
|
|
68
|
+
model.platform = PlatformEnum(self._combo_platform.currentText())
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from PySide6.QtWidgets import QFrame
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CardFrame(QFrame):
|
|
5
|
+
def __init__(self, parent=None, object_name: str = "card"):
|
|
6
|
+
super().__init__(parent=parent)
|
|
7
|
+
|
|
8
|
+
self.setObjectName(object_name)
|
|
9
|
+
self.setFrameShape(QFrame.Shape.Box)
|
|
10
|
+
self.setLineWidth(1)
|
|
11
|
+
self.setStyleSheet(
|
|
12
|
+
f"""
|
|
13
|
+
QFrame#{object_name} {{
|
|
14
|
+
border: 1px solid #C8C8C8;
|
|
15
|
+
border-radius: 6px;
|
|
16
|
+
}}
|
|
17
|
+
"""
|
|
18
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .detector.beambreak_detector_card import BeambreakDetectorCard
|
|
2
|
+
from .detector.fusion_detector import FusionDetectorCard
|
|
3
|
+
from .detector.mock_detector_card import MockDetectorCard
|
|
4
|
+
from .detector.rfid_detector import RFIDDetectorCard
|
|
5
|
+
from .device_card import DeviceCard
|
|
6
|
+
from .rewarder.mock_rewarder_card import MockRewarderCard
|
|
7
|
+
from .rewarder.rpi_gpio_rewarder import RPIGpioPumpCard
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DeviceCard",
|
|
11
|
+
"MockRewarderCard",
|
|
12
|
+
"RPIGpioPumpCard",
|
|
13
|
+
"MockDetectorCard",
|
|
14
|
+
"RFIDDetectorCard",
|
|
15
|
+
"BeambreakDetectorCard",
|
|
16
|
+
"FusionDetectorCard",
|
|
17
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pymxbi.detector import FusionContinuousDetectorModel
|
|
2
|
+
from PySide6.QtWidgets import QLabel, QLineEdit
|
|
3
|
+
|
|
4
|
+
from ..device_card import DeviceCard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BeambreakDetectorCard(DeviceCard[FusionContinuousDetectorModel]):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__()
|
|
10
|
+
|
|
11
|
+
self.set_title("Beambreak Detector")
|
|
12
|
+
|
|
13
|
+
label_pin = QLabel("Pin")
|
|
14
|
+
self._line_pin = QLineEdit()
|
|
15
|
+
self._line_pin.setPlaceholderText("Pin")
|
|
16
|
+
self.layout_config.addRow(label_pin, self._line_pin)
|
|
17
|
+
|
|
18
|
+
def load_config(self, model: FusionContinuousDetectorModel) -> None:
|
|
19
|
+
self.checkbox_enabled.setChecked(model.enabled)
|
|
20
|
+
self.line_device_id.setText(str(model.id))
|
|
21
|
+
self._line_pin.setText(str(model.pin))
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def result(self) -> FusionContinuousDetectorModel:
|
|
25
|
+
return FusionContinuousDetectorModel(
|
|
26
|
+
enabled=self.checkbox_enabled.isChecked(),
|
|
27
|
+
id=int(self.line_device_id.text()),
|
|
28
|
+
pin=int(self._line_pin.text()),
|
|
29
|
+
)
|