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,336 @@
|
|
|
1
|
+
from concurrent.futures import Future
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from tkinter import CENTER, Canvas, Event
|
|
4
|
+
from typing import TYPE_CHECKING, Final
|
|
5
|
+
|
|
6
|
+
from mxbi.tasks.GNGSiD.models import Result, TouchEvent
|
|
7
|
+
from mxbi.tasks.GNGSiD.tasks.discriminate.discriminate_models import (
|
|
8
|
+
DataToShow,
|
|
9
|
+
TrialConfig,
|
|
10
|
+
TrialData,
|
|
11
|
+
)
|
|
12
|
+
from mxbi.tasks.GNGSiD.tasks.utils.targets import DiscriminateTarget
|
|
13
|
+
from mxbi.utils.aplayer import StimulusSequenceUnit
|
|
14
|
+
from mxbi.utils.tkinter.components.canvas_with_border import CanvasWithInnerBorder
|
|
15
|
+
from mxbi.utils.tkinter.components.showdata_widget import ShowDataWidget
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from mxbi.models.animal import AnimalState
|
|
19
|
+
from mxbi.models.session import ScreenConfig, SessionConfig
|
|
20
|
+
from mxbi.tasks.GNGSiD.models import PersistentData
|
|
21
|
+
from mxbi.theater import Theater
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GNGSiDDiscriminateScene:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
theater: "Theater",
|
|
28
|
+
session_config: "SessionConfig",
|
|
29
|
+
animal_state: "AnimalState",
|
|
30
|
+
screen_type: "ScreenConfig",
|
|
31
|
+
trial_config: "TrialConfig",
|
|
32
|
+
persistent_data: "PersistentData",
|
|
33
|
+
) -> None:
|
|
34
|
+
# Track shared dependencies and trial configuration
|
|
35
|
+
self._theater: Final[Theater] = theater
|
|
36
|
+
self._session_config = session_config
|
|
37
|
+
self._animal_state: Final[AnimalState] = animal_state
|
|
38
|
+
self._screen_type: Final[ScreenConfig] = screen_type
|
|
39
|
+
self._trial_config: Final[TrialConfig] = trial_config
|
|
40
|
+
self._persistent_data: Final["PersistentData"] = persistent_data
|
|
41
|
+
|
|
42
|
+
# Build stimulus units for attention, high, and low tones
|
|
43
|
+
attention_unit = self._build_stimulus_unit(
|
|
44
|
+
frequency=trial_config.stimulus_freq_low,
|
|
45
|
+
duration=trial_config.stimulus_freq_low_duration,
|
|
46
|
+
master_volume=trial_config.stimulus_freq_low_master_amp,
|
|
47
|
+
digital_volume=trial_config.stimulus_freq_low_digital_amp,
|
|
48
|
+
)
|
|
49
|
+
high_unit = self._build_stimulus_unit(
|
|
50
|
+
frequency=trial_config.stimulus_freq_high,
|
|
51
|
+
duration=trial_config.stimulus_freq_high_duration,
|
|
52
|
+
master_volume=trial_config.stimulus_freq_high_master_amp,
|
|
53
|
+
digital_volume=trial_config.stimulus_freq_high_digital_amp,
|
|
54
|
+
)
|
|
55
|
+
low_unit = self._build_stimulus_unit(
|
|
56
|
+
frequency=trial_config.stimulus_freq_low,
|
|
57
|
+
duration=trial_config.stimulus_freq_low_duration,
|
|
58
|
+
master_volume=trial_config.stimulus_freq_low_master_amp,
|
|
59
|
+
digital_volume=trial_config.stimulus_freq_low_digital_amp,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Pre-compute the stimuli sequences and timing values used in the trial
|
|
63
|
+
self._attention_stimulus = self._prepare_stimulus(
|
|
64
|
+
[attention_unit], trial_config.attention_duration
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Calculate total response duration including stimulus duration and extra response time
|
|
68
|
+
self._response_duration = (
|
|
69
|
+
self._trial_config.stimulus_duration
|
|
70
|
+
+ self._trial_config.extra_response_time
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self._reward_duration = self._trial_config.reward_duration
|
|
74
|
+
|
|
75
|
+
if trial_config.is_stimulus_trial:
|
|
76
|
+
stimulus_units = [high_unit, low_unit]
|
|
77
|
+
else:
|
|
78
|
+
stimulus_units = [attention_unit]
|
|
79
|
+
self._stimulus = self._prepare_stimulus(
|
|
80
|
+
stimulus_units, self._trial_config.stimulus_duration
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self._standard_reward_stimulus = self._theater.new_standard_reward_stimulus(
|
|
84
|
+
self._trial_config.stimulus_duration
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self._on_trial_start()
|
|
88
|
+
|
|
89
|
+
# region public api
|
|
90
|
+
def start(self) -> TrialData:
|
|
91
|
+
self._theater.mainloop()
|
|
92
|
+
return self._data
|
|
93
|
+
|
|
94
|
+
def cancle(self) -> None:
|
|
95
|
+
self._data.result = Result.CANCEL
|
|
96
|
+
self._theater.aplayer.stop()
|
|
97
|
+
self._on_trial_end()
|
|
98
|
+
|
|
99
|
+
# endregion
|
|
100
|
+
|
|
101
|
+
# region lifecycle
|
|
102
|
+
def _on_trial_start(self) -> None:
|
|
103
|
+
self._create_view()
|
|
104
|
+
self._init_data()
|
|
105
|
+
self._bind_first_stage()
|
|
106
|
+
|
|
107
|
+
def _on_inter_trial(self) -> None:
|
|
108
|
+
self._background.after(
|
|
109
|
+
self._trial_config.inter_trial_interval, self._on_trial_end
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _on_trial_end(self) -> None:
|
|
113
|
+
self._background.destroy()
|
|
114
|
+
self._theater.root.quit()
|
|
115
|
+
|
|
116
|
+
# endregion
|
|
117
|
+
|
|
118
|
+
# region views
|
|
119
|
+
def _create_view(self) -> None:
|
|
120
|
+
self._create_background()
|
|
121
|
+
self._create_show_data_view()
|
|
122
|
+
self._create_target()
|
|
123
|
+
|
|
124
|
+
def _create_background(self) -> None:
|
|
125
|
+
self._background = CanvasWithInnerBorder(
|
|
126
|
+
master=self._theater.root,
|
|
127
|
+
bg="black",
|
|
128
|
+
width=self._screen_type.width,
|
|
129
|
+
height=self._screen_type.height,
|
|
130
|
+
border_width=40,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self._background.place(relx=0.5, rely=0.5, anchor="center")
|
|
134
|
+
|
|
135
|
+
def _create_show_data_view(self) -> None:
|
|
136
|
+
self._show_data_widget = ShowDataWidget(self._background)
|
|
137
|
+
self._show_data_widget.place(relx=0, rely=1, anchor="sw")
|
|
138
|
+
data = DataToShow(
|
|
139
|
+
name=self._animal_state.name,
|
|
140
|
+
id=self._animal_state.trial_id,
|
|
141
|
+
level_id=self._animal_state.current_level_trial_id,
|
|
142
|
+
level=self._trial_config.level,
|
|
143
|
+
rewards=self._persistent_data.rewards,
|
|
144
|
+
correct=self._animal_state.correct_trial,
|
|
145
|
+
incorrect=self._persistent_data.incorrect,
|
|
146
|
+
timeout=self._persistent_data.timeout,
|
|
147
|
+
stimulus=self._trial_config.is_stimulus_trial,
|
|
148
|
+
)
|
|
149
|
+
self._show_data_widget.show_data(data.model_dump())
|
|
150
|
+
|
|
151
|
+
def _create_target(self) -> None:
|
|
152
|
+
x_shift = 240
|
|
153
|
+
x_center = self._screen_type.width * 0.5 + x_shift
|
|
154
|
+
y_center = self._screen_type.height * 0.5
|
|
155
|
+
|
|
156
|
+
self._trigger_canvas = DiscriminateTarget(
|
|
157
|
+
self._background, self._trial_config.stimulation_size
|
|
158
|
+
)
|
|
159
|
+
self._trigger_canvas.place(x=x_center, y=y_center, anchor="center")
|
|
160
|
+
|
|
161
|
+
def _create_wrong_view(self) -> None:
|
|
162
|
+
self._trigger_canvas = Canvas(
|
|
163
|
+
self._background,
|
|
164
|
+
bg="grey",
|
|
165
|
+
width=self._screen_type.width,
|
|
166
|
+
height=self._screen_type.height,
|
|
167
|
+
)
|
|
168
|
+
self._trigger_canvas.place(relx=0.5, rely=0.5, anchor=CENTER)
|
|
169
|
+
|
|
170
|
+
# endregion
|
|
171
|
+
|
|
172
|
+
# region event binding
|
|
173
|
+
def _bind_first_stage(self) -> None:
|
|
174
|
+
self._background.focus_set()
|
|
175
|
+
self._background.bind("<r>", lambda e: self._give_standard_stimulus())
|
|
176
|
+
self._trigger_canvas.bind("<ButtonPress>", self._on_first_touched)
|
|
177
|
+
self._trigger_canvas.after(self._trial_config.time_out, self._on_timeout)
|
|
178
|
+
|
|
179
|
+
def _bind_second_stage(self) -> None:
|
|
180
|
+
self._reward_duration = self._trial_config.reward_duration
|
|
181
|
+
self._trigger_canvas.bind("<ButtonPress>", self._on_second_touched)
|
|
182
|
+
if self._trial_config.is_stimulus_trial:
|
|
183
|
+
self._trigger_canvas.after(self._response_duration, self._on_incorrect)
|
|
184
|
+
self._schedule_reward_adjustments()
|
|
185
|
+
else:
|
|
186
|
+
self._trigger_canvas.after(
|
|
187
|
+
self._trial_config.stimulus_duration, self._on_correct
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# endregion
|
|
191
|
+
|
|
192
|
+
# region event handlers
|
|
193
|
+
def _on_first_touched(self, event: Event) -> None:
|
|
194
|
+
self._trigger_canvas.destroy()
|
|
195
|
+
self._record_touch(event)
|
|
196
|
+
future = self._give_stimulus(self._attention_stimulus)
|
|
197
|
+
future.add_done_callback(self._start_stimulus_stage)
|
|
198
|
+
|
|
199
|
+
def _start_stimulus_stage(self, future: Future) -> None:
|
|
200
|
+
if not future.result():
|
|
201
|
+
return
|
|
202
|
+
self._give_stimulus(self._stimulus)
|
|
203
|
+
self._background.after(0, self._prepare_second_stage)
|
|
204
|
+
|
|
205
|
+
def _prepare_second_stage(self) -> None:
|
|
206
|
+
self._create_target()
|
|
207
|
+
self._bind_second_stage()
|
|
208
|
+
|
|
209
|
+
def _on_second_touched(self, event: Event) -> None:
|
|
210
|
+
self._trigger_canvas.destroy()
|
|
211
|
+
self._record_touch(event)
|
|
212
|
+
|
|
213
|
+
if self._trial_config.is_stimulus_trial:
|
|
214
|
+
self._on_correct()
|
|
215
|
+
else:
|
|
216
|
+
self._on_incorrect()
|
|
217
|
+
|
|
218
|
+
def _record_touch(self, event: Event) -> None:
|
|
219
|
+
self._data.touch_events.append(
|
|
220
|
+
TouchEvent(time=datetime.now().timestamp(), x=event.x, y=event.y)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# endregion
|
|
224
|
+
|
|
225
|
+
# region result handlers
|
|
226
|
+
def _on_correct(self) -> None:
|
|
227
|
+
self._theater.aplayer.stop()
|
|
228
|
+
self._trigger_canvas.destroy()
|
|
229
|
+
|
|
230
|
+
self._background.after(self._trial_config.reward_delay, self._give_reward)
|
|
231
|
+
self._data.result = Result.CORRECT
|
|
232
|
+
self._data.correct_rate = (self._animal_state.correct_trial + 1) / (
|
|
233
|
+
self._animal_state.current_level_trial_id + 1
|
|
234
|
+
)
|
|
235
|
+
self._on_inter_trial()
|
|
236
|
+
|
|
237
|
+
def _on_incorrect(self) -> None:
|
|
238
|
+
self._theater.aplayer.stop()
|
|
239
|
+
self._trigger_canvas.destroy()
|
|
240
|
+
|
|
241
|
+
self._data.result = Result.INCORRECT
|
|
242
|
+
self._data.correct_rate = self._animal_state.correct_trial / (
|
|
243
|
+
self._animal_state.current_level_trial_id + 1
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self._create_wrong_view()
|
|
247
|
+
self._on_inter_trial()
|
|
248
|
+
|
|
249
|
+
def _on_timeout(self) -> None:
|
|
250
|
+
self._theater.aplayer.stop()
|
|
251
|
+
self._data.result = Result.TIMEOUT
|
|
252
|
+
self._data.correct_rate = self._animal_state.correct_trial / (
|
|
253
|
+
self._animal_state.current_level_trial_id + 1
|
|
254
|
+
)
|
|
255
|
+
self._create_wrong_view()
|
|
256
|
+
self._on_inter_trial()
|
|
257
|
+
|
|
258
|
+
# endregion
|
|
259
|
+
|
|
260
|
+
# region stimulus and reward
|
|
261
|
+
def _build_stimulus_unit(
|
|
262
|
+
self,
|
|
263
|
+
*,
|
|
264
|
+
frequency: int,
|
|
265
|
+
duration: int,
|
|
266
|
+
master_volume: int,
|
|
267
|
+
digital_volume: int,
|
|
268
|
+
) -> StimulusSequenceUnit:
|
|
269
|
+
unit = StimulusSequenceUnit(
|
|
270
|
+
frequency=frequency,
|
|
271
|
+
duration=duration,
|
|
272
|
+
interval=self._trial_config.stimulus_interval,
|
|
273
|
+
)
|
|
274
|
+
self._configure_unit_volume(unit, master_volume, digital_volume)
|
|
275
|
+
return unit
|
|
276
|
+
|
|
277
|
+
def _configure_unit_volume(
|
|
278
|
+
self,
|
|
279
|
+
unit: StimulusSequenceUnit,
|
|
280
|
+
master_volume: int,
|
|
281
|
+
digital_volume: int,
|
|
282
|
+
) -> None:
|
|
283
|
+
unit.master_volume = master_volume
|
|
284
|
+
unit.digital_volume = digital_volume
|
|
285
|
+
|
|
286
|
+
def _prepare_stimulus(
|
|
287
|
+
self, stimulus_units: list[StimulusSequenceUnit], total_duration: int
|
|
288
|
+
) -> list[StimulusSequenceUnit]:
|
|
289
|
+
return self._theater.aplayer.generate_stimulus_sequence(
|
|
290
|
+
stimulus_units, total_duration
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _give_stimulus(
|
|
294
|
+
self, stimulus_units: list[StimulusSequenceUnit]
|
|
295
|
+
) -> "Future[bool]":
|
|
296
|
+
return self._theater.aplayer.play_stimulus_sequence(stimulus_units)
|
|
297
|
+
|
|
298
|
+
def _give_reward(self) -> None:
|
|
299
|
+
self._persistent_data.rewards += 1
|
|
300
|
+
self._theater.reward.give_reward(self._reward_duration)
|
|
301
|
+
|
|
302
|
+
def _give_standard_stimulus(self) -> None:
|
|
303
|
+
self._standard_reward_stimulus.play(self._trial_config.reward_duration)
|
|
304
|
+
|
|
305
|
+
def _schedule_reward_adjustments(self) -> None:
|
|
306
|
+
self._background.after(
|
|
307
|
+
self._trial_config.medium_reward_threshold,
|
|
308
|
+
self._adjust_reward_duration,
|
|
309
|
+
self._trial_config.medium_reward_duration,
|
|
310
|
+
)
|
|
311
|
+
self._background.after(
|
|
312
|
+
self._trial_config.stimulus_duration,
|
|
313
|
+
self._adjust_reward_duration,
|
|
314
|
+
self._trial_config.low_reward_duration,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def _adjust_reward_duration(self, duration: int) -> None:
|
|
318
|
+
self._reward_duration = duration
|
|
319
|
+
|
|
320
|
+
# endregion
|
|
321
|
+
|
|
322
|
+
# region data
|
|
323
|
+
def _init_data(self) -> None:
|
|
324
|
+
self._data = TrialData(
|
|
325
|
+
animal=self._animal_state.name,
|
|
326
|
+
trial_id=self._animal_state.trial_id,
|
|
327
|
+
current_level_trial_id=self._animal_state.current_level_trial_id,
|
|
328
|
+
trial_config=self._trial_config,
|
|
329
|
+
trial_start_time=datetime.now().timestamp(),
|
|
330
|
+
trial_end_time=0,
|
|
331
|
+
result=Result.TIMEOUT,
|
|
332
|
+
correct_rate=0,
|
|
333
|
+
touch_events=[],
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# endregion
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from mxbi.tasks.GNGSiD.models import BaseDataToShow, BaseTrialConfig, BaseTrialData
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TrialConfig(BaseTrialConfig):
|
|
5
|
+
stimulus_freq: int
|
|
6
|
+
stimulus_freq_duration: int
|
|
7
|
+
stimulus_freq_master_amp: int
|
|
8
|
+
stimulus_freq_digital_amp: int
|
|
9
|
+
|
|
10
|
+
stimulus_interval: int
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TrialData(BaseTrialData):
|
|
14
|
+
trial_config: TrialConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataToShow(BaseDataToShow): ...
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from math import ceil
|
|
3
|
+
from tkinter import CENTER, Canvas, Event
|
|
4
|
+
from typing import TYPE_CHECKING, Final
|
|
5
|
+
|
|
6
|
+
from mxbi.tasks.GNGSiD.models import Result, TouchEvent
|
|
7
|
+
from mxbi.tasks.GNGSiD.tasks.touch.touch_models import DataToShow, TrialData
|
|
8
|
+
from mxbi.tasks.GNGSiD.tasks.utils.targets import DetectTarget
|
|
9
|
+
from mxbi.utils.aplayer import ToneConfig
|
|
10
|
+
from mxbi.utils.tkinter.components.canvas_with_border import CanvasWithInnerBorder
|
|
11
|
+
from mxbi.utils.tkinter.components.showdata_widget import ShowDataWidget
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from concurrent.futures import Future
|
|
15
|
+
|
|
16
|
+
from mxbi.models.animal import AnimalState
|
|
17
|
+
from mxbi.models.session import ScreenConfig, SessionConfig
|
|
18
|
+
from mxbi.tasks.GNGSiD.models import PersistentData
|
|
19
|
+
from mxbi.tasks.GNGSiD.tasks.touch.touch_models import TrialConfig
|
|
20
|
+
from mxbi.theater import Theater
|
|
21
|
+
from numpy import int16
|
|
22
|
+
from numpy.typing import NDArray
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GNGSiDTouchScene:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
theater: "Theater",
|
|
29
|
+
session: "SessionConfig",
|
|
30
|
+
animal_state: "AnimalState",
|
|
31
|
+
screen_type: "ScreenConfig",
|
|
32
|
+
trial_config: "TrialConfig",
|
|
33
|
+
persistent_data: "PersistentData",
|
|
34
|
+
) -> None:
|
|
35
|
+
self._theater: "Final[Theater]" = theater
|
|
36
|
+
self._animal_state: "Final[AnimalState]" = animal_state
|
|
37
|
+
self._screen_type: "Final[ScreenConfig]" = screen_type
|
|
38
|
+
self._trial_config: "Final[TrialConfig]" = trial_config
|
|
39
|
+
self._persistent_data: "Final[PersistentData]" = persistent_data
|
|
40
|
+
|
|
41
|
+
self._tone = self._prepare_stimulus()
|
|
42
|
+
self._standard_reward_stimulus = self._theater.new_standard_reward_stimulus(
|
|
43
|
+
self._trial_config.stimulus_duration
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self._set_stimulus_intensity()
|
|
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._bind_events()
|
|
67
|
+
self._init_data()
|
|
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.2, 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 = DetectTarget(
|
|
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
|
+
|
|
139
|
+
# Trigger event
|
|
140
|
+
self._background.bind("<ButtonPress>", self._on_background_touched)
|
|
141
|
+
self._trigger_canvas.bind("<ButtonPress>", self._on_touched)
|
|
142
|
+
|
|
143
|
+
# Timeout event
|
|
144
|
+
self._trigger_canvas.after(self._trial_config.time_out, self._on_timeout)
|
|
145
|
+
|
|
146
|
+
# endregion
|
|
147
|
+
|
|
148
|
+
# region event handlers
|
|
149
|
+
def _on_touched(self, event: Event) -> None:
|
|
150
|
+
self._data.touch_events.append(
|
|
151
|
+
TouchEvent(time=datetime.now().timestamp(), x=event.x, y=event.y)
|
|
152
|
+
)
|
|
153
|
+
self._background.unbind("<ButtonPress>")
|
|
154
|
+
self._trigger_canvas.destroy()
|
|
155
|
+
|
|
156
|
+
future = self._give_stimulus()
|
|
157
|
+
future.add_done_callback(self._on_stimulus_complete)
|
|
158
|
+
|
|
159
|
+
def _on_background_touched(self, event: Event) -> None:
|
|
160
|
+
self._data.touch_events.append(
|
|
161
|
+
TouchEvent(time=datetime.now().timestamp(), x=event.x, y=event.y)
|
|
162
|
+
)
|
|
163
|
+
self._background.unbind("<ButtonPress>")
|
|
164
|
+
self._trigger_canvas.destroy()
|
|
165
|
+
|
|
166
|
+
self._on_incorrect()
|
|
167
|
+
|
|
168
|
+
def _on_correct(self) -> None:
|
|
169
|
+
self._give_reward()
|
|
170
|
+
self._data.result = Result.CORRECT
|
|
171
|
+
self._data.correct_rate = (self._animal_state.correct_trial + 1) / (
|
|
172
|
+
self._animal_state.current_level_trial_id + 1
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self._on_inter_trial()
|
|
176
|
+
|
|
177
|
+
def _on_incorrect(self) -> None:
|
|
178
|
+
self._data.result = Result.INCORRECT
|
|
179
|
+
self._data.correct_rate = self._animal_state.correct_trial / (
|
|
180
|
+
self._animal_state.current_level_trial_id + 1
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
self._create_wrong_view()
|
|
184
|
+
self._on_inter_trial()
|
|
185
|
+
|
|
186
|
+
def _on_timeout(self) -> None:
|
|
187
|
+
self._data.result = Result.TIMEOUT
|
|
188
|
+
self._data.correct_rate = self._animal_state.correct_trial / (
|
|
189
|
+
self._animal_state.current_level_trial_id + 1
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self._create_wrong_view()
|
|
193
|
+
self._on_inter_trial()
|
|
194
|
+
|
|
195
|
+
# endregion
|
|
196
|
+
|
|
197
|
+
# region sitimulus and reward
|
|
198
|
+
def _prepare_stimulus(self) -> "NDArray[int16]":
|
|
199
|
+
unit_duration = (
|
|
200
|
+
self._trial_config.stimulus_freq_duration
|
|
201
|
+
+ self._trial_config.stimulus_freq_duration
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
times = ceil(self._trial_config.stimulus_duration / unit_duration)
|
|
205
|
+
times = max(times, 1)
|
|
206
|
+
|
|
207
|
+
freq_1 = ToneConfig(
|
|
208
|
+
frequency=self._trial_config.stimulus_freq,
|
|
209
|
+
duration=self._trial_config.stimulus_freq_duration,
|
|
210
|
+
)
|
|
211
|
+
freq_2 = ToneConfig(frequency=0, duration=self._trial_config.stimulus_interval)
|
|
212
|
+
|
|
213
|
+
return self._theater.aplayer.generate_stimulus([freq_1, freq_2], times)
|
|
214
|
+
|
|
215
|
+
def _give_stimulus(self) -> "Future[bool]":
|
|
216
|
+
return self._theater.aplayer.play_stimulus(self._tone)
|
|
217
|
+
|
|
218
|
+
def _on_stimulus_complete(self, future: "Future[bool]") -> None:
|
|
219
|
+
if future.result():
|
|
220
|
+
self._trigger_canvas.after(
|
|
221
|
+
self._trial_config.reward_delay,
|
|
222
|
+
lambda: self._on_correct(),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def _give_reward(self) -> None:
|
|
226
|
+
self._persistent_data.rewards += 1
|
|
227
|
+
self._theater.reward.give_reward(self._trial_config.reward_duration)
|
|
228
|
+
|
|
229
|
+
def _give_standard_stimulus(self) -> None:
|
|
230
|
+
self._standard_reward_stimulus.play(self._trial_config.reward_duration)
|
|
231
|
+
|
|
232
|
+
def _set_stimulus_intensity(self) -> None:
|
|
233
|
+
self._theater.acontroller.set_master_volume(
|
|
234
|
+
self._trial_config.stimulus_freq_master_amp
|
|
235
|
+
)
|
|
236
|
+
self._theater.acontroller.set_digital_volume(
|
|
237
|
+
self._trial_config.stimulus_freq_digital_amp
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# endregion
|
|
241
|
+
|
|
242
|
+
# region data
|
|
243
|
+
def _init_data(self) -> None:
|
|
244
|
+
self._data = TrialData(
|
|
245
|
+
animal=self._animal_state.name,
|
|
246
|
+
trial_id=self._animal_state.trial_id,
|
|
247
|
+
current_level_trial_id=self._animal_state.current_level_trial_id,
|
|
248
|
+
trial_config=self._trial_config,
|
|
249
|
+
trial_start_time=datetime.now().timestamp(),
|
|
250
|
+
trial_end_time=0,
|
|
251
|
+
result=Result.TIMEOUT,
|
|
252
|
+
correct_rate=0,
|
|
253
|
+
touch_events=[],
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# endregion
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from tkinter import Canvas
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def create_circle(
|
|
5
|
+
x_coord: float, y_coord: float, r: float, canvas: Canvas, color: str
|
|
6
|
+
) -> int:
|
|
7
|
+
x0 = x_coord - r
|
|
8
|
+
y0 = y_coord - r
|
|
9
|
+
x1 = x_coord + r
|
|
10
|
+
y1 = y_coord + r
|
|
11
|
+
return canvas.create_oval(x0, y0, x1, y1, outline="", fill=color)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DetectTarget(Canvas):
|
|
15
|
+
def __init__(self, master, size) -> None:
|
|
16
|
+
super().__init__(
|
|
17
|
+
master, width=size, height=size, bg="lightgray", highlightthickness=0
|
|
18
|
+
)
|
|
19
|
+
# TODO: figure out magic number
|
|
20
|
+
circle_config = [
|
|
21
|
+
(0.5, 2.1, "#616161"),
|
|
22
|
+
(0.5, 3.1, "#bababa"),
|
|
23
|
+
(0.5, 6.3, "white"),
|
|
24
|
+
]
|
|
25
|
+
for cx, divisor, color in circle_config:
|
|
26
|
+
create_circle(
|
|
27
|
+
size * cx,
|
|
28
|
+
size * cx,
|
|
29
|
+
size / divisor,
|
|
30
|
+
self,
|
|
31
|
+
color,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DiscriminateTarget(Canvas):
|
|
36
|
+
def __init__(self, master, size) -> None:
|
|
37
|
+
super().__init__(
|
|
38
|
+
master, width=size, height=size, bg="lightgray", highlightthickness=0
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
circle_config = [
|
|
42
|
+
(0.5, 0.5, 3.1, "#616161"),
|
|
43
|
+
(0.5, 0.5, 6.3, "white"),
|
|
44
|
+
(0.25, 0.25, 6.3, "#616161"),
|
|
45
|
+
(0.75, 0.25, 6.3, "#616161"),
|
|
46
|
+
(0.25, 0.75, 6.3, "#616161"),
|
|
47
|
+
(0.75, 0.75, 6.3, "#616161"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
for cx, cy, divisor, color in circle_config:
|
|
51
|
+
create_circle(
|
|
52
|
+
size * cx,
|
|
53
|
+
size * cy,
|
|
54
|
+
size / divisor,
|
|
55
|
+
self,
|
|
56
|
+
color,
|
|
57
|
+
)
|