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.
Files changed (93) hide show
  1. mxbiflow/__init__.py +3 -0
  2. mxbiflow/assets/__init__.py +5 -0
  3. mxbiflow/assets/clicker.wav +0 -0
  4. mxbiflow/config_store.py +68 -0
  5. mxbiflow/data_logger.py +114 -0
  6. mxbiflow/default/__init__.py +4 -0
  7. mxbiflow/default/idle/assets/apple_v1.png +0 -0
  8. mxbiflow/default/idle/idle.py +57 -0
  9. mxbiflow/detector_bridge.py +87 -0
  10. mxbiflow/game.py +84 -0
  11. mxbiflow/infra/eventbus.py +31 -0
  12. mxbiflow/main.py +106 -0
  13. mxbiflow/models/animal.py +130 -0
  14. mxbiflow/models/reward.py +7 -0
  15. mxbiflow/models/session.py +145 -0
  16. mxbiflow/mxbiflow.py +43 -0
  17. mxbiflow/path.py +41 -0
  18. mxbiflow/scene/__init__.py +8 -0
  19. mxbiflow/scene/scene_manager.py +64 -0
  20. mxbiflow/scene/scene_protocol.py +22 -0
  21. mxbiflow/scheduler.py +90 -0
  22. mxbiflow/tasks/GNGSiD/models.py +70 -0
  23. mxbiflow/tasks/GNGSiD/stages/detect_stage/config.json +116 -0
  24. mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage.py +161 -0
  25. mxbiflow/tasks/GNGSiD/stages/detect_stage/detect_stage_models.py +65 -0
  26. mxbiflow/tasks/GNGSiD/stages/discriminate_stage/config.json +70 -0
  27. mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage.py +173 -0
  28. mxbiflow/tasks/GNGSiD/stages/discriminate_stage/discriminate_stage_models.py +80 -0
  29. mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/config.json +83 -0
  30. mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_models.py +58 -0
  31. mxbiflow/tasks/GNGSiD/stages/size_reduction_stage/size_reduction_stage.py +149 -0
  32. mxbiflow/tasks/GNGSiD/tasks/artifacts.py +13 -0
  33. mxbiflow/tasks/GNGSiD/tasks/detect/models.py +21 -0
  34. mxbiflow/tasks/GNGSiD/tasks/detect/scene.py +271 -0
  35. mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_models.py +31 -0
  36. mxbiflow/tasks/GNGSiD/tasks/discriminate/discriminate_scene.py +336 -0
  37. mxbiflow/tasks/GNGSiD/tasks/touch/touch_models.py +17 -0
  38. mxbiflow/tasks/GNGSiD/tasks/touch/touch_scene.py +256 -0
  39. mxbiflow/tasks/GNGSiD/tasks/utils/targets.py +57 -0
  40. mxbiflow/tasks/cross_modal/bundle_dir.py +553 -0
  41. mxbiflow/tasks/cross_modal/config.py +41 -0
  42. mxbiflow/tasks/cross_modal/media.py +61 -0
  43. mxbiflow/tasks/cross_modal/models.py +57 -0
  44. mxbiflow/tasks/cross_modal/scene.py +252 -0
  45. mxbiflow/tasks/cross_modal/stage.py +218 -0
  46. mxbiflow/tasks/cross_modal/trial_io.py +23 -0
  47. mxbiflow/tasks/cross_modal/trial_schema.py +113 -0
  48. mxbiflow/tasks/default/error_task/error_scene.py +53 -0
  49. mxbiflow/tasks/default/idle_task/assets/apple_v1.png +0 -0
  50. mxbiflow/tasks/default/idle_task/idle_scene.py +85 -0
  51. mxbiflow/tasks/default/initial_habituation_training/README.md +188 -0
  52. mxbiflow/tasks/default/initial_habituation_training/stages/config.csv +7 -0
  53. mxbiflow/tasks/default/initial_habituation_training/stages/config.json +67 -0
  54. mxbiflow/tasks/default/initial_habituation_training/stages/initial_habituation_training_stage.py +172 -0
  55. mxbiflow/tasks/default/initial_habituation_training/stages/models.py +56 -0
  56. mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward.py +244 -0
  57. mxbiflow/tasks/default/initial_habituation_training/tasks/stay_to_reward/stay_to_reward_models.py +50 -0
  58. mxbiflow/tasks/task_protocol.py +26 -0
  59. mxbiflow/tasks/task_table.py +29 -0
  60. mxbiflow/tasks/two_alternative_choice/assets/starter.py +27 -0
  61. mxbiflow/tasks/two_alternative_choice/models.py +68 -0
  62. mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/config.json +118 -0
  63. mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_models.py +41 -0
  64. mxbiflow/tasks/two_alternative_choice/stages/size_reduction_stage/size_reduction_stage.py +122 -0
  65. mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_models.py +19 -0
  66. mxbiflow/tasks/two_alternative_choice/tasks/touch/touch_scene.py +249 -0
  67. mxbiflow/timer/__init__.py +3 -0
  68. mxbiflow/timer/frame_timer.py +47 -0
  69. mxbiflow/timer/realtime_timer.py +0 -0
  70. mxbiflow/tmp_email.py +13 -0
  71. mxbiflow/ui/components/animal.py +87 -0
  72. mxbiflow/ui/components/baseconfig.py +68 -0
  73. mxbiflow/ui/components/card.py +18 -0
  74. mxbiflow/ui/components/device_card/__init__.py +17 -0
  75. mxbiflow/ui/components/device_card/detector/beambreak_detector_card.py +29 -0
  76. mxbiflow/ui/components/device_card/detector/fusion_detector.py +45 -0
  77. mxbiflow/ui/components/device_card/detector/mock_detector_card.py +20 -0
  78. mxbiflow/ui/components/device_card/detector/rfid_detector.py +40 -0
  79. mxbiflow/ui/components/device_card/device_card.py +67 -0
  80. mxbiflow/ui/components/device_card/rewarder/mock_rewarder_card.py +20 -0
  81. mxbiflow/ui/components/device_card/rewarder/rpi_gpio_rewarder.py +33 -0
  82. mxbiflow/ui/components/devices.py +183 -0
  83. mxbiflow/ui/components/dialog/__init__.py +3 -0
  84. mxbiflow/ui/components/dialog/add_devices_dialog.py +64 -0
  85. mxbiflow/ui/components/experiment_groups.py +122 -0
  86. mxbiflow/ui/experiment_panel.py +91 -0
  87. mxbiflow/ui/mxbi_panel.py +152 -0
  88. mxbiflow/utils/logger.py +19 -0
  89. mxbiflow/utils/serial.py +10 -0
  90. mxbiflow-0.1.1.dist-info/METADATA +168 -0
  91. mxbiflow-0.1.1.dist-info/RECORD +93 -0
  92. mxbiflow-0.1.1.dist-info/WHEEL +4 -0
  93. 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
+ )