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,45 @@
1
+ from pymxbi.detector import FusionContinuousDetectorModel
2
+ from PySide6.QtWidgets import QComboBox, QLabel, QLineEdit
3
+
4
+ from .....utils.serial import get_all_ports, get_baudrates
5
+ from ..device_card import DeviceCard
6
+
7
+
8
+ class FusionDetectorCard(DeviceCard[FusionContinuousDetectorModel]):
9
+ def __init__(self):
10
+ super().__init__()
11
+
12
+ self.set_title("Fusion Detector")
13
+
14
+ label_pin = QLabel("Beam Break Pin")
15
+ self._line_pin = QLineEdit()
16
+ self._line_pin.setPlaceholderText("Pin Number")
17
+ self.layout_config.addRow(label_pin, self._line_pin)
18
+
19
+ label_port = QLabel("Port")
20
+ self._combo_port = QComboBox()
21
+ self._combo_port.addItems(get_all_ports())
22
+ self.layout_config.addRow(label_port, self._combo_port)
23
+
24
+ label_baudrate = QLabel("Baudrate")
25
+ self._combo_baudrate = QComboBox()
26
+ for text, value in get_baudrates():
27
+ self._combo_baudrate.addItem(text, value)
28
+ self.layout_config.addRow(label_baudrate, self._combo_baudrate)
29
+
30
+ def load_config(self, model: FusionContinuousDetectorModel) -> None:
31
+ self.checkbox_enabled.setChecked(model.enabled)
32
+ self.line_device_id.setText(str(model.id))
33
+ self._line_pin.setText(str(model.pin))
34
+ self._combo_port.setCurrentText(model.port)
35
+ self._combo_baudrate.setCurrentText(str(model.baudrate))
36
+
37
+ @property
38
+ def result(self) -> FusionContinuousDetectorModel:
39
+ return FusionContinuousDetectorModel(
40
+ enabled=self.checkbox_enabled.isChecked(),
41
+ id=int(self.line_device_id.text()),
42
+ pin=int(self._line_pin.text()),
43
+ port=self._combo_port.currentText(),
44
+ baudrate=int(self._combo_baudrate.currentData()),
45
+ )
@@ -0,0 +1,20 @@
1
+ from pymxbi.detector import MockDetectorModel
2
+
3
+ from ..device_card import DeviceCard
4
+
5
+
6
+ class MockDetectorCard(DeviceCard[MockDetectorModel]):
7
+ def __init__(self):
8
+ super().__init__()
9
+ self.set_title("Mock Detector")
10
+
11
+ def load_config(self, model: MockDetectorModel) -> None:
12
+ self.checkbox_enabled.setChecked(model.enabled)
13
+ self.line_device_id.setText(str(model.id))
14
+
15
+ @property
16
+ def result(self) -> MockDetectorModel:
17
+ return MockDetectorModel(
18
+ enabled=self.checkbox_enabled.isChecked(),
19
+ id=int(self.line_device_id.text()),
20
+ )
@@ -0,0 +1,40 @@
1
+ from pymxbi.detector import RFIDContinuousDetectorModel
2
+ from PySide6.QtWidgets import (
3
+ QComboBox,
4
+ QLabel,
5
+ )
6
+
7
+ from .....utils.serial import get_all_ports, get_baudrates
8
+ from ..device_card import DeviceCard
9
+
10
+
11
+ class RFIDDetectorCard(DeviceCard[RFIDContinuousDetectorModel]):
12
+ def __init__(self):
13
+ super().__init__()
14
+ self.set_title("Single RFID Detector")
15
+
16
+ lable_port = QLabel("Port:")
17
+ self.combo_port = QComboBox()
18
+ self.combo_port.addItems(get_all_ports())
19
+ self.layout_config.addRow(lable_port, self.combo_port)
20
+
21
+ lable_baudrate = QLabel("Baudrate:")
22
+ self.combo_baudrate = QComboBox()
23
+ for text, value in get_baudrates():
24
+ self.combo_baudrate.addItem(text, value)
25
+ self.layout_config.addRow(lable_baudrate, self.combo_baudrate)
26
+
27
+ def load_config(self, model: RFIDContinuousDetectorModel) -> None:
28
+ self.checkbox_enabled.setChecked(model.enabled)
29
+ self.line_device_id.setText(str(model.id))
30
+ self.combo_port.setCurrentText(model.port)
31
+ self.combo_baudrate.setCurrentText(str(model.baudrate))
32
+
33
+ @property
34
+ def result(self) -> RFIDContinuousDetectorModel:
35
+ return RFIDContinuousDetectorModel(
36
+ enabled=self.checkbox_enabled.isChecked(),
37
+ id=int(self.line_device_id.text()),
38
+ port=self.combo_port.currentText(),
39
+ baudrate=int(self.combo_baudrate.currentText()),
40
+ )
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Generic, TypeVar
4
+
5
+ from PySide6.QtCore import Qt, Signal
6
+ from PySide6.QtGui import QIntValidator
7
+ from PySide6.QtWidgets import (
8
+ QCheckBox,
9
+ QFormLayout,
10
+ QLabel,
11
+ QLineEdit,
12
+ QMenu,
13
+ QVBoxLayout,
14
+ )
15
+
16
+ from ..card import CardFrame
17
+
18
+ TModel = TypeVar("TModel")
19
+
20
+
21
+ class DeviceCard(CardFrame, Generic[TModel]):
22
+ remove_requested = Signal()
23
+
24
+ def __init__(self, parent=None):
25
+ super().__init__(parent=parent, object_name="card")
26
+
27
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
28
+ self.customContextMenuRequested.connect(self._open_menu)
29
+
30
+ self.layout_main = QVBoxLayout()
31
+ self.setLayout(self.layout_main)
32
+ self.layout_main.setContentsMargins(8, 8, 8, 8)
33
+
34
+ self.label_title = QLabel("Device Configuration")
35
+ self.layout_main.addWidget(self.label_title)
36
+
37
+ self.layout_config = QFormLayout()
38
+ self.layout_main.addLayout(self.layout_config)
39
+
40
+ self.lable_enabled = QLabel("Enabled:")
41
+ self.checkbox_enabled = QCheckBox()
42
+ self.checkbox_enabled.setChecked(False)
43
+ self.layout_config.addRow(self.lable_enabled, self.checkbox_enabled)
44
+
45
+ self.label_id = QLabel("Device ID:")
46
+ self.line_device_id = QLineEdit("0")
47
+ self.int_validator = QIntValidator(0, 1000, self)
48
+ self.line_device_id.setValidator(self.int_validator)
49
+ self.layout_config.addRow(self.label_id, self.line_device_id)
50
+
51
+ def _open_menu(self, position) -> None:
52
+ menu = QMenu(self)
53
+ action_remove = menu.addAction("Remove")
54
+ action_remove.triggered.connect(
55
+ lambda _checked=False: self.remove_requested.emit()
56
+ )
57
+ menu.exec(self.mapToGlobal(position))
58
+
59
+ def set_title(self, title: str) -> None:
60
+ self.label_title.setText(title)
61
+
62
+ def load_config(self, model: TModel) -> None:
63
+ raise NotImplementedError
64
+
65
+ @property
66
+ def result(self) -> TModel:
67
+ raise NotImplementedError
@@ -0,0 +1,20 @@
1
+ from pymxbi.rewarder import MockRewarderModel
2
+
3
+ from ..device_card import DeviceCard
4
+
5
+
6
+ class MockRewarderCard(DeviceCard[MockRewarderModel]):
7
+ def __init__(self):
8
+ super().__init__()
9
+ self.set_title("Mock Rewarder")
10
+
11
+ def load_config(self, model: MockRewarderModel) -> None:
12
+ self.checkbox_enabled.setChecked(model.enabled)
13
+ self.line_device_id.setText(str(model.id))
14
+
15
+ @property
16
+ def result(self) -> MockRewarderModel:
17
+ return MockRewarderModel(
18
+ enabled=self.checkbox_enabled.isChecked(),
19
+ id=int(self.line_device_id.text()),
20
+ )
@@ -0,0 +1,33 @@
1
+ from pymxbi.rewarder import GPIORewarderModel
2
+ from PySide6.QtGui import QIntValidator
3
+ from PySide6.QtWidgets import (
4
+ QLabel,
5
+ QLineEdit,
6
+ )
7
+
8
+ from ..device_card import DeviceCard
9
+
10
+
11
+ class RPIGpioPumpCard(DeviceCard[GPIORewarderModel]):
12
+ def __init__(self):
13
+ super().__init__()
14
+ self.set_title("GPIO Pump")
15
+
16
+ lable_gpio_pin = QLabel("GPIO Pin:")
17
+ self.line_gpio_pin = QLineEdit("0")
18
+ int_validator = QIntValidator(0, 40, self)
19
+ self.line_gpio_pin.setValidator(int_validator)
20
+ self.layout_config.addRow(lable_gpio_pin, self.line_gpio_pin)
21
+
22
+ def load_config(self, model: GPIORewarderModel) -> None:
23
+ self.checkbox_enabled.setChecked(model.enabled)
24
+ self.line_device_id.setText(str(model.id))
25
+ self.line_gpio_pin.setText(str(model.pin))
26
+
27
+ @property
28
+ def result(self) -> GPIORewarderModel:
29
+ return GPIORewarderModel(
30
+ enabled=self.checkbox_enabled.isChecked(),
31
+ id=int(self.line_device_id.text()),
32
+ pin=int(self.line_gpio_pin.text()),
33
+ )
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Generic, TypeVar
5
+
6
+ from pymxbi.detector import DetectorModel
7
+ from pymxbi.rewarder import RewarderModel
8
+ from PySide6.QtCore import Qt
9
+ from PySide6.QtWidgets import (
10
+ QDialog,
11
+ QGridLayout,
12
+ QGroupBox,
13
+ QMenu,
14
+ QMessageBox,
15
+ QWidget,
16
+ )
17
+
18
+ from .device_card.device_card import DeviceCard
19
+ from .dialog.add_devices_dialog import AddDeviceDialog
20
+
21
+ T = TypeVar("T", RewarderModel, DetectorModel)
22
+
23
+
24
+ class Devices(QGroupBox, Generic[T]):
25
+ def __init__(
26
+ self,
27
+ parent: QWidget | None,
28
+ title: str,
29
+ *,
30
+ action_label: str,
31
+ device_types: Sequence[str],
32
+ dialog_title: str,
33
+ label: str,
34
+ card_factories: Mapping[str, type[QWidget]],
35
+ columns: int = 2,
36
+ ):
37
+ super().__init__(title, parent)
38
+
39
+ self._action_label = action_label
40
+ self._device_types = list(device_types)
41
+ self._dialog_title = dialog_title
42
+ self._label = label
43
+ self._card_factories = dict(card_factories)
44
+ self._columns = max(1, int(columns))
45
+
46
+ self._cards: list[DeviceCard[T]] = []
47
+
48
+ self._build_ui()
49
+ self._bind_events()
50
+
51
+ @property
52
+ def cards(self) -> list[DeviceCard[T]]:
53
+ return list(self._cards)
54
+
55
+ def results(self) -> list[T]:
56
+ return [card.result for card in self._cards]
57
+
58
+ def load_models(self, models: Sequence[T]) -> None:
59
+ for model in models:
60
+ self.add_model(model)
61
+
62
+ def add_model(self, model: T) -> None:
63
+ try:
64
+ device_type = model.device_type
65
+ except Exception as exc:
66
+ QMessageBox.warning(
67
+ self,
68
+ "Unsupported device",
69
+ f"Failed to determine device type ({exc!s})",
70
+ )
71
+ return
72
+ if device_type is None:
73
+ QMessageBox.warning(
74
+ self,
75
+ "Unsupported device",
76
+ "Missing device type",
77
+ )
78
+ return
79
+
80
+ self._add_device_card(str(device_type), model=model)
81
+
82
+ # -----------------------------
83
+ # UI / Events
84
+ # -----------------------------
85
+
86
+ def _build_ui(self) -> None:
87
+ layout = QGridLayout()
88
+ layout.setAlignment(Qt.AlignmentFlag.AlignTop)
89
+ for col in range(self._columns):
90
+ layout.setColumnStretch(col, 1)
91
+ self.setLayout(layout)
92
+
93
+ def _bind_events(self) -> None:
94
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
95
+ self.customContextMenuRequested.connect(self._open_menu)
96
+
97
+ def _open_menu(self, position) -> None:
98
+ menu = QMenu(self)
99
+ action = menu.addAction(self._action_label)
100
+ action.triggered.connect(lambda _checked=False: self._on_add_clicked())
101
+ menu.exec(self.mapToGlobal(position))
102
+
103
+ def _on_add_clicked(self) -> None:
104
+ dialog = AddDeviceDialog(
105
+ self,
106
+ device_types=self._device_types,
107
+ title=self._dialog_title,
108
+ label=self._label,
109
+ )
110
+ if dialog.exec() != QDialog.DialogCode.Accepted:
111
+ return
112
+
113
+ self._add_device_card(dialog.device_type, model=None)
114
+
115
+ # -----------------------------
116
+ # Cards / Layout
117
+ # -----------------------------
118
+
119
+ def _add_device_card(self, device_type: str, *, model: T | None) -> None:
120
+ card_factory = self._card_factories.get(device_type)
121
+ if card_factory is None:
122
+ QMessageBox.warning(
123
+ self,
124
+ "Unsupported device",
125
+ f"Unsupported device type: {device_type}",
126
+ )
127
+ return
128
+
129
+ card = card_factory()
130
+ if isinstance(card, DeviceCard):
131
+ if model is not None:
132
+ card.load_config(model)
133
+ card.remove_requested.connect(lambda c=card: self._on_remove_requested(c))
134
+ self._cards.append(card)
135
+
136
+ self._mount(card)
137
+
138
+ def _on_remove_requested(self, card: DeviceCard[T]) -> None:
139
+ self._remove_card(card)
140
+
141
+ def _remove_card(self, card: DeviceCard[T]) -> None:
142
+ layout = self.layout()
143
+ if isinstance(layout, QGridLayout):
144
+ layout.removeWidget(card)
145
+ card.setParent(None)
146
+ card.deleteLater()
147
+
148
+ self._reflow_cards()
149
+
150
+ def _reflow_cards(self) -> None:
151
+ layout = self.layout()
152
+ if not isinstance(layout, QGridLayout):
153
+ return
154
+
155
+ cards: list[DeviceCard[T]] = []
156
+ for i in range(layout.count()):
157
+ item = layout.itemAt(i)
158
+ if item is None:
159
+ continue
160
+ widget = item.widget()
161
+ if isinstance(widget, DeviceCard):
162
+ cards.append(widget)
163
+
164
+ while layout.count() > 0:
165
+ layout.takeAt(0)
166
+
167
+ self._cards = cards
168
+ for index, widget in enumerate(cards):
169
+ row, col = divmod(index, self._columns)
170
+ layout.addWidget(widget, row, col)
171
+
172
+ def _mount(self, widget: QWidget) -> None:
173
+ layout = self.layout()
174
+ if layout is None:
175
+ return
176
+
177
+ if isinstance(layout, QGridLayout):
178
+ index = layout.count()
179
+ row, col = divmod(index, self._columns)
180
+ layout.addWidget(widget, row, col)
181
+ return
182
+
183
+ layout.addWidget(widget)
@@ -0,0 +1,3 @@
1
+ from .add_devices_dialog import AddDeviceDialog
2
+
3
+ __all__ = ["AddDeviceDialog"]
@@ -0,0 +1,64 @@
1
+ from collections.abc import Sequence
2
+
3
+ from PySide6.QtWidgets import (
4
+ QComboBox,
5
+ QDialog,
6
+ QDialogButtonBox,
7
+ QFormLayout,
8
+ QLabel,
9
+ QVBoxLayout,
10
+ )
11
+
12
+
13
+ class AddDeviceDialog(QDialog):
14
+ """
15
+ Simple picker dialog.
16
+
17
+ `device_types` items can be either strings or StrEnum members (since they are `str`).
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ parent=None,
23
+ *,
24
+ device_types: Sequence[str],
25
+ title: str = "Add device",
26
+ label: str = "device type:",
27
+ ):
28
+ super().__init__(parent)
29
+ self._device_types = device_types
30
+ self._title = title
31
+ self._label = label
32
+ self._init_ui()
33
+ self._bind_events()
34
+
35
+ def _init_ui(self):
36
+ self.setWindowTitle(self._title)
37
+
38
+ self._main_layout = QVBoxLayout()
39
+ self.setLayout(self._main_layout)
40
+
41
+ self._layout_type = QFormLayout()
42
+ self._main_layout.addLayout(self._layout_type)
43
+
44
+ self._label_device_type = QLabel(self._label)
45
+ self._comba_device_type = QComboBox()
46
+ self._comba_device_type.addItems([str(item) for item in self._device_types])
47
+ self._layout_type.addRow(self._label_device_type, self._comba_device_type)
48
+
49
+ self.button_box = QDialogButtonBox(
50
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
51
+ )
52
+ self._main_layout.addWidget(self.button_box)
53
+
54
+ def _bind_events(self):
55
+ self.button_box.accepted.connect(self.accept)
56
+ self.button_box.rejected.connect(self.reject)
57
+
58
+ @property
59
+ def device_type(self) -> str:
60
+ return self._comba_device_type.currentText()
61
+
62
+ @property
63
+ def selected_device(self) -> str:
64
+ return self.device_type
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from PySide6.QtCore import Qt
4
+ from PySide6.QtWidgets import (
5
+ QComboBox,
6
+ QFormLayout,
7
+ QGridLayout,
8
+ QGroupBox,
9
+ QLabel,
10
+ QLineEdit,
11
+ QMenu,
12
+ )
13
+
14
+ from ...models.animal import AnimalConfig
15
+ from ...models.session import RewardEnum, SessionConfig
16
+ from .animal import AnimalCard
17
+
18
+
19
+ class ExperimentConfigGroup(QGroupBox):
20
+ def __init__(self, parent, experimenters: list[str]):
21
+ super().__init__("Config", parent)
22
+
23
+ layout = QFormLayout(self)
24
+ self.setLayout(layout)
25
+
26
+ lable_experimenter = QLabel("experimenter", self)
27
+ self.combo_experimenter = QComboBox(self)
28
+ self.combo_experimenter.addItems(experimenters)
29
+ layout.addRow(lable_experimenter, self.combo_experimenter)
30
+
31
+ lable_reward_type = QLabel("reward type", self)
32
+ self.combo_reward_type = QComboBox(self)
33
+ self.combo_reward_type.addItems(list(RewardEnum))
34
+ layout.addRow(lable_reward_type, self.combo_reward_type)
35
+
36
+ lable_notes = QLabel("notes", self)
37
+ self.line_notes = QLineEdit(self)
38
+ self.line_notes.setPlaceholderText("Notes")
39
+ layout.addRow(lable_notes, self.line_notes)
40
+
41
+ def load_config(self, config: SessionConfig):
42
+ self.combo_experimenter.setEditText(config.experimenter)
43
+ self.combo_reward_type.setEditText(config.reward_type)
44
+
45
+ def result(self) -> SessionConfig:
46
+ return SessionConfig(
47
+ experimenter=self.combo_experimenter.currentText(),
48
+ reward_type=RewardEnum(self.combo_reward_type.currentText()),
49
+ note=self.line_notes.text(),
50
+ )
51
+
52
+
53
+ class ExperimentAnimalsGroup(QGroupBox):
54
+ def __init__(self, parent=None, *, animals: dict[str, str]):
55
+ super().__init__("Animals", parent)
56
+
57
+ self._animals = animals
58
+ self.layout_animals = QGridLayout(self)
59
+ self.setLayout(self.layout_animals)
60
+
61
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
62
+ self.customContextMenuRequested.connect(self._on_context_menu)
63
+
64
+ def _on_context_menu(self, pos):
65
+ menu = QMenu(self)
66
+ action = menu.addAction("Add animal")
67
+ action.triggered.connect(lambda _checked=False: self._on_add_animal())
68
+ menu.exec(self.mapToGlobal(pos))
69
+
70
+ def _on_add_animal(self):
71
+ animal_card = AnimalCard(self, self._animals)
72
+ animal_card.remove_requested.connect(
73
+ lambda _card=animal_card: self._on_remove_animal(_card)
74
+ )
75
+ self._add_animal_card(animal_card)
76
+
77
+ def _add_animal_card(self, animal_card: AnimalCard) -> None:
78
+ index = self.layout_animals.count()
79
+ row = index // 4
80
+ col = index % 4
81
+ self.layout_animals.addWidget(animal_card, row, col)
82
+
83
+ def _on_remove_animal(self, animal_card: AnimalCard) -> None:
84
+ self.layout_animals.removeWidget(animal_card)
85
+ animal_card.setParent(None)
86
+ animal_card.deleteLater()
87
+ self._reflow_animal_cards()
88
+
89
+ def _reflow_animal_cards(self) -> None:
90
+ cards: list[AnimalCard] = []
91
+ for i in range(self.layout_animals.count()):
92
+ item = self.layout_animals.itemAt(i)
93
+ if item:
94
+ widget = item.widget()
95
+ if isinstance(widget, AnimalCard):
96
+ cards.append(widget)
97
+
98
+ while self.layout_animals.count() > 0:
99
+ self.layout_animals.takeAt(0)
100
+
101
+ for card in cards:
102
+ self._add_animal_card(card)
103
+
104
+ def load_config(self, config: SessionConfig):
105
+ for animal_configs in config.animals:
106
+ animal_card = AnimalCard(self, self._animals)
107
+ animal_card.load_config(animal_configs)
108
+ animal_card.remove_requested.connect(
109
+ lambda _card=animal_card: self._on_remove_animal(_card)
110
+ )
111
+ self._add_animal_card(animal_card)
112
+
113
+ def result(self) -> list[AnimalConfig]:
114
+ results: list[AnimalConfig] = []
115
+ for i in range(self.layout_animals.count()):
116
+ item = self.layout_animals.itemAt(i)
117
+ if not item:
118
+ continue
119
+ widget = item.widget()
120
+ if isinstance(widget, AnimalCard):
121
+ results.append(widget.result)
122
+ return results
@@ -0,0 +1,91 @@
1
+ from PySide6.QtCore import Signal
2
+ from PySide6.QtWidgets import (
3
+ QApplication,
4
+ QHBoxLayout,
5
+ QMainWindow,
6
+ QPushButton,
7
+ QVBoxLayout,
8
+ QWidget,
9
+ )
10
+
11
+ from ..config_store import ConfigStore
12
+ from ..models.session import Options, SessionConfig
13
+ from ..path import OPTIONS_PATH, SESSION_CONFIG_PATH
14
+ from .components.experiment_groups import ExperimentAnimalsGroup, ExperimentConfigGroup
15
+
16
+
17
+ class ExperimentPanel(QMainWindow):
18
+ accepted = Signal()
19
+
20
+ def __init__(self, parent=None):
21
+ super().__init__(parent)
22
+ self._config = ConfigStore(SESSION_CONFIG_PATH, SessionConfig)
23
+ self._options = ConfigStore(OPTIONS_PATH, Options)
24
+
25
+ self.setWindowTitle("Experiment Panel")
26
+
27
+ central_widget = QWidget(self)
28
+ self.setCentralWidget(central_widget)
29
+
30
+ layout_main = QVBoxLayout(self)
31
+ central_widget.setLayout(layout_main)
32
+
33
+ self.group_config = ExperimentConfigGroup(
34
+ self, self._options.value.experimenter
35
+ )
36
+ layout_main.addWidget(self.group_config)
37
+
38
+ self.group_animals = ExperimentAnimalsGroup(
39
+ self, animals=self._options.value.animals
40
+ )
41
+ layout_main.addWidget(self.group_animals)
42
+
43
+ self.combo_experimenter = self.group_config.combo_experimenter
44
+ self.combo_reward_type = self.group_config.combo_reward_type
45
+ self.line_notes = self.group_config.line_notes
46
+
47
+ layout_buttons = QHBoxLayout(self)
48
+ layout_main.addLayout(layout_buttons)
49
+ self._button_cancle = QPushButton("Cancel", self)
50
+ self._button_save = QPushButton("Save", self)
51
+ self._button_continue = QPushButton("Continue", self)
52
+ layout_buttons.addWidget(self._button_cancle)
53
+ layout_buttons.addWidget(self._button_save)
54
+ layout_buttons.addWidget(self._button_continue)
55
+
56
+ self._bind_signals()
57
+ self.load_config()
58
+
59
+ def _bind_signals(self):
60
+ self._button_cancle.clicked.connect(self._on_cancle)
61
+ self._button_save.clicked.connect(self._on_save)
62
+ self._button_continue.clicked.connect(self._on_continue)
63
+
64
+ def load_config(self):
65
+ self.group_config.load_config(self._config.value)
66
+ self.group_animals.load_config(self._config.value)
67
+
68
+ def result(self) -> SessionConfig:
69
+ session_config = self.group_config.result()
70
+ session_config.animals = self.group_animals.result()
71
+ return session_config
72
+
73
+ def _on_save(self):
74
+ self._config.save(self.result())
75
+
76
+ def _on_cancle(self):
77
+ self.close()
78
+
79
+ def _on_continue(self):
80
+ self._on_save()
81
+ self.close()
82
+ self.accepted.emit()
83
+
84
+
85
+ if __name__ == "__main__":
86
+ import sys
87
+
88
+ app = QApplication(sys.argv)
89
+ experiment_panel = ExperimentPanel()
90
+ experiment_panel.show()
91
+ sys.exit(app.exec())