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,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,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())
|