1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-10 02:30:54 +02:00

Compare commits

..

9 Commits

72 changed files with 3647 additions and 845 deletions

View File

@@ -111,7 +111,7 @@ tests:
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt6]
- pip install -e .[dev,pyside6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
@@ -135,7 +135,9 @@ tests-3.10-pyside6:
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyside6]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.12-pyside6:
@@ -150,7 +152,9 @@ tests-3.12-pyside6:
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyside6]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.10-pyqt5:
@@ -165,7 +169,9 @@ tests-3.10-pyqt5:
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt5]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.11-pyqt5:
@@ -180,7 +186,9 @@ tests-3.11-pyqt5:
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt5]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.12-pyqt5:
@@ -195,7 +203,9 @@ tests-3.12-pyqt5:
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt5]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.10-pyqt6:
@@ -210,7 +220,9 @@ tests-3.10-pyqt6:
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt6]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.11-pyqt6:
@@ -225,7 +237,9 @@ tests-3.11-pyqt6:
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt6]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
tests-3.12-pyqt6:
@@ -240,7 +254,9 @@ tests-3.12-pyqt6:
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- pip install -e ./bec/bec_lib[dev]
- pip install -e .[dev,pyqt6]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
- coverage xml
allow_failure: true
end-2-end-conda:
@@ -256,7 +272,7 @@ end-2-end-conda:
- conda config --prepend channels conda-forge
- conda config --set channel_priority strict
- conda config --set always_yes yes --set changeps1 no
- conda create -q -n test-environment python=3.11
- conda create -q -n test-environment python=3.10
- conda init bash
- source ~/.bashrc
- conda activate test-environment
@@ -271,7 +287,7 @@ end-2-end-conda:
- pip install -e ./bec_lib[dev]
- pip install -e ./bec_ipython_client[dev]
- cd ../
- pip install -e .[dev,pyqt6]
- pip install -e .[dev,pyside6]
- cd ./tests/end-2-end
- pytest --start-servers --flush-redis --random-order

View File

@@ -2,57 +2,6 @@
## v0.56.2 (2024-06-05)
### Documentation
* docs: restructured docs layout ([`3c9181d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3c9181d93d68faa4efb3b91c486ca9ca935975a0))
### Fix
* fix(bar): ring saves current value in config ([`9648e3e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9648e3ea96a4109be6be694d855151ed6d3ad661))
* fix(dock): dock saves configs of all children widgets ([`4be756a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4be756a8676421c3a3451458995232407295df84))
* fix(dock_area): save/restore state is saved in config ([`46face0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/46face0ee59122f04cb383da685a6658beeeb389))
* fix(figure): added correct types of configs to subplot widgets ([`6f3b1ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6f3b1ea985c18929b9bab54239eeb600f03b274a))
## v0.56.1 (2024-06-04)
### Fix
* fix(spiral_progress_bar/rings): config min/max values added check for floats ([`9d615c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9d615c915c8f7cc2ea8f1dc17993b98fe462c682))
* fix(spiral_progress_bar): Endpoint is always stored as a string in the RingConnection Config ([`d253991`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2539918b296559e1d684344e179775a2423daa9))
## v0.56.0 (2024-05-29)
### Build
* build: added pyside6 as dependency ([`db301b1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/db301b1be27bba76c8bb21fbff93cb4902b592a5))
### Ci
* ci: added tests for pyside6, pyqt6 and pyqt5, default test and e2e is python 3.11 and pyqt6 ([`855be35`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/855be3551a1372bcbebba6f8930903f99202bbca))
### Documentation
* docs(examples): example apps section deleted ([`ad208a5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ad208a5ef8495c45a8b83a4850ba9a1041b42717))
### Feature
* feat(utils/ui_loader): universal ui loader for pyside/pyqt ([`0fea8d6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0fea8d606574fa99dda3b117da5d5209c251f694))
### Fix
* fix(examples): outdated examples removed (mca_plot.py, stream_plot.py, motor_example.py) ([`ddc9510`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ddc9510c2ba8dadf291809eeb5b135a105259492))
* fix: compatibility adjustment to .ui loading and tests for PySide6 ([`07b99d9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/07b99d91a57a645cddd76294f48d78773e4c9ea5))
## v0.55.0 (2024-05-24)
### Feature
@@ -167,6 +116,10 @@
* build(cli): changed repo name to bec_widgets ([`799ea55`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/799ea554de9a7f3720d100be4886a63f02c6a390))
* build(setup): fakeredis added to dev env ([`df32350`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df323504fea024a97304d96c2e39e61450714069))
* build(setup): PyQt6 version is set to 6.7 ([`0ab8aa3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0ab8aa3a2fe51b5c38b25fca44c1c422bb42478d))
### Ci
* ci: added rule for parent-child pipelines ([`e085125`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e0851250eecb85503db929d37f75d2ba366308a6))
@@ -174,3 +127,45 @@
### Feature
* feat(utils): added plugin helper to find and load ([`5ece269`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5ece269adb0e9b0c2a468f1dfbaa6212e86d3561))
## v0.50.2 (2024-04-30)
### Fix
* fix: 'disconnect_slot' has to be symmetric with 'connect_slot' regarding QtThreadSafeCallback ([`0dfcaa4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0dfcaa4b708948af0a40ec7cf34d03ff1e96ffac))
## v0.50.1 (2024-04-29)
### Fix
* fix(cli): BECFigure takes the port to connect to redis from the current BECClient, supporting plugins ([`57cb136`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/57cb136a098e87a452414bf44e627edb562f6799))
## v0.50.0 (2024-04-29)
### Feature
* feat(plots): universal cleanup and remove also for children items ([`381d713`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/381d713837bb9217c58ba1d8b89691aa35c9f5ec))
* feat(rpc/rpc_register): singleton rpc register for all rpc connections for session ([`a898e7e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a898e7e4f14e9ae854703dddbd1eb8c50cb640ff))
### Fix
* fix(widgets/figure): access pattern changed for getting widgets by coordinates for rpc ([`13c018a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/13c018a79704a7497c140df57179d294e43ecffa))
* fix(plots): cleanup policy reviewed for children items ([`8f20a0b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8f20a0b3b1b5dd117b36b45645717190b9ee9cbf))
* fix(rpc/client_utils): getoutput more transparent + error handling ([`6b6a6b2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b6a6b2249f24d3d02bd5fcd7ef1c63ed794c304))
* fix(rpc_register): thread lock for listign all connections ([`2ca3267`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2ca32675ec3f00137e2140259db51f6e5aa7bb71))
### Test
* test(cli/rpc_register): e2e RPCRegister ([`4f261be`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4f261be4c7cfe54501443d031f9266f4f838f6e2))
* test(cli/rpc_register): rpc_register tests added ([`40eb75f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/40eb75f85a4d99d498b086a37e799276a9d2ac3f))
## v0.49.1 (2024-04-26)

View File

@@ -1410,16 +1410,6 @@ class BECMotorMap(RPCBase):
class BECDock(RPCBase):
@property
@rpc_call
def config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@property
@rpc_call
def rpc_id(self) -> "str":
@@ -1538,16 +1528,6 @@ class BECDock(RPCBase):
class BECDockArea(RPCBase, BECGuiClientMixin):
@property
@rpc_call
def config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@property
@rpc_call
def panels(self) -> "dict":

View File

@@ -0,0 +1,313 @@
import json
import os
import threading
import h5py
import numpy as np
import pyqtgraph as pg
import zmq
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QKeySequence
from qtpy.QtWidgets import QDialog, QFileDialog, QFrame, QLabel, QShortcut, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
# from scipy.stats import multivariate_normal
class EigerPlot(QWidget):
update_signal = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
# pg.setConfigOptions(background="w", foreground="k", antialias=True)
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "eiger_plot.ui"), self)
# Set widow name
self.setWindowTitle("Eiger Plot")
self.hist_lims = None
self.mask = None
self.image = None
# UI
self.init_ui()
self.hook_signals()
self.key_bindings()
# ZMQ Consumer
self._zmq_consumer_exit_event = threading.Event()
self._zmq_consumer_thread = self.start_zmq_consumer()
def close(self):
super().close()
self._zmq_consumer_exit_event.set()
self._zmq_consumer_thread.join()
def init_ui(self):
# Create Plot and add ImageItem
self.plot_item = pg.PlotItem()
self.plot_item.setAspectLocked(True)
self.imageItem = pg.ImageItem()
self.plot_item.addItem(self.imageItem)
# Setting up histogram
self.hist = pg.HistogramLUTItem()
self.hist.setImageItem(self.imageItem)
self.hist.gradient.loadPreset("magma")
self.update_hist()
# Adding Items to Graphical Layout
self.glw_layout = QVBoxLayout(self.ui.glw_placeholder)
self.glw = pg.GraphicsLayoutWidget()
self.glw_layout.addWidget(self.glw)
self.glw.addItem(self.plot_item)
self.glw.addItem(self.hist)
def hook_signals(self):
# Buttons
# self.pushButton_test.clicked.connect(self.start_sim_stream)
self.ui.pushButton_mask.clicked.connect(self.load_mask_dialog)
self.ui.pushButton_delete_mask.clicked.connect(self.delete_mask)
self.ui.pushButton_help.clicked.connect(self.show_help_dialog)
# SpinBoxes
self.ui.doubleSpinBox_hist_min.valueChanged.connect(self.update_hist)
self.ui.doubleSpinBox_hist_max.valueChanged.connect(self.update_hist)
# Signal/Slots
self.update_signal.connect(self.on_image_update)
def key_bindings(self):
# Key bindings for rotation
rotate_plus = QShortcut(QKeySequence("Ctrl+A"), self)
rotate_minus = QShortcut(QKeySequence("Ctrl+Z"), self)
self.ui.comboBox_rotation.setToolTip("Increase rotation: Ctrl+A\nDecrease rotation: Ctrl+Z")
self.ui.checkBox_transpose.setToolTip("Toggle transpose: Ctrl+T")
max_index = self.ui.comboBox_rotation.count() - 1 # Maximum valid index
rotate_plus.activated.connect(
lambda: self.ui.comboBox_rotation.setCurrentIndex(
min(self.ui.comboBox_rotation.currentIndex() + 1, max_index)
)
)
rotate_minus.activated.connect(
lambda: self.ui.comboBox_rotation.setCurrentIndex(
max(self.ui.comboBox_rotation.currentIndex() - 1, 0)
)
)
# Key bindings for transpose
transpose = QShortcut(QKeySequence("Ctrl+T"), self)
transpose.activated.connect(self.ui.checkBox_transpose.toggle)
FFT = QShortcut(QKeySequence("Ctrl+F"), self)
FFT.activated.connect(self.ui.checkBox_FFT.toggle)
self.ui.checkBox_FFT.setToolTip("Toggle FFT: Ctrl+F")
log = QShortcut(QKeySequence("Ctrl+L"), self)
log.activated.connect(self.ui.checkBox_log.toggle)
self.ui.checkBox_log.setToolTip("Toggle log: Ctrl+L")
mask = QShortcut(QKeySequence("Ctrl+M"), self)
mask.activated.connect(self.ui.pushButton_mask.click)
self.ui.pushButton_mask.setToolTip("Load mask: Ctrl+M")
delete_mask = QShortcut(QKeySequence("Ctrl+D"), self)
delete_mask.activated.connect(self.ui.pushButton_delete_mask.click)
self.ui.pushButton_delete_mask.setToolTip("Delete mask: Ctrl+D")
def update_hist(self):
self.hist_levels = [
self.ui.doubleSpinBox_hist_min.value(),
self.ui.doubleSpinBox_hist_max.value(),
]
self.hist.setLevels(min=self.hist_levels[0], max=self.hist_levels[1])
self.hist.setHistogramRange(
self.hist_levels[0] - 0.1 * self.hist_levels[0],
self.hist_levels[1] + 0.1 * self.hist_levels[1],
)
def load_mask_dialog(self):
options = QFileDialog.Options()
options |= QFileDialog.ReadOnly
file_name, _ = QFileDialog.getOpenFileName(
self, "Select Mask File", "", "H5 Files (*.h5);;All Files (*)", options=options
)
if file_name:
self.load_mask(file_name)
def load_mask(self, path):
try:
with h5py.File(path, "r") as f:
self.mask = f["data"][...]
if self.mask is not None:
# Set label to mask name without path
self.label_mask.setText(os.path.basename(path))
except KeyError as e:
# Update GUI with the error message
print(f"Error: {str(e)}")
def delete_mask(self):
self.mask = None
self.label_mask.setText("No Mask")
@pyqtSlot()
def on_image_update(self):
# TODO first rotate then transpose
if self.mask is not None:
# self.image = np.ma.masked_array(self.image, mask=self.mask) #TODO test if np works
self.image = self.image * (1 - self.mask) + 1
if self.ui.checkBox_FFT.isChecked():
self.image = np.abs(np.fft.fftshift(np.fft.fft2(self.image)))
if self.ui.comboBox_rotation.currentIndex() > 0: # rotate
self.image = np.rot90(
self.image, k=self.ui.comboBox_rotation.currentIndex(), axes=(0, 1)
)
if self.ui.checkBox_transpose.isChecked(): # transpose
self.image = np.transpose(self.image)
if self.ui.checkBox_log.isChecked():
self.image = np.log10(self.image)
self.imageItem.setImage(self.image, autoLevels=False)
###############################
# ZMQ Consumer
###############################
def start_zmq_consumer(self):
consumer_thread = threading.Thread(
target=self.zmq_consumer, args=(self._zmq_consumer_exit_event,), daemon=True
)
consumer_thread.start()
return consumer_thread
def zmq_consumer(self, exit_event):
print("starting consumer")
live_stream_url = "tcp://129.129.95.38:20000"
receiver = zmq.Context().socket(zmq.SUB)
receiver.connect(live_stream_url)
receiver.setsockopt_string(zmq.SUBSCRIBE, "")
poller = zmq.Poller()
poller.register(receiver, zmq.POLLIN)
# code could be a bit simpler here, testing exit_event in
# 'while' condition, but like this it is easier for the
# 'test_zmq_consumer' test
while True:
if poller.poll(1000): # 1s timeout
raw_meta, raw_data = receiver.recv_multipart(zmq.NOBLOCK)
meta = json.loads(raw_meta.decode("utf-8"))
self.image = np.frombuffer(raw_data, dtype=meta["type"]).reshape(meta["shape"])
self.update_signal.emit()
if exit_event.is_set():
break
receiver.disconnect(live_stream_url)
###############################
# just simulations from here
###############################
def show_help_dialog(self):
dialog = QDialog(self)
dialog.setWindowTitle("Help")
layout = QVBoxLayout()
# Key bindings section
layout.addWidget(QLabel("Keyboard Shortcuts:"))
key_bindings = [
("Ctrl+A", "Increase rotation"),
("Ctrl+Z", "Decrease rotation"),
("Ctrl+T", "Toggle transpose"),
("Ctrl+F", "Toggle FFT"),
("Ctrl+L", "Toggle log scale"),
("Ctrl+M", "Load mask"),
("Ctrl+D", "Delete mask"),
]
for keys, action in key_bindings:
layout.addWidget(QLabel(f"{keys} - {action}"))
# Separator
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
layout.addWidget(separator)
# Histogram section
layout.addWidget(QLabel("Histogram:"))
layout.addWidget(
QLabel(
"Use the Double Spin Boxes to adjust the minimum and maximum values of the histogram."
)
)
# Another Separator
another_separator = QFrame()
another_separator.setFrameShape(QFrame.HLine)
another_separator.setFrameShadow(QFrame.Sunken)
layout.addWidget(another_separator)
# Mask section
layout.addWidget(QLabel("Mask:"))
layout.addWidget(
QLabel(
"Use 'Load Mask' to load a mask from an H5 file. 'Delete Mask' removes the current mask."
)
)
dialog.setLayout(layout)
dialog.exec()
###############################
# just simulations from here
###############################
# def start_sim_stream(self):
# sim_stream_thread = threading.Thread(target=self.sim_stream, daemon=True)
# sim_stream_thread.start()
#
# def sim_stream(self):
# for i in range(100):
# # Generate 100x100 image of random noise
# self.image = np.random.rand(100, 100) * 0.2
#
# # Define Gaussian parameters
# x, y = np.mgrid[0:50, 0:50]
# pos = np.dstack((x, y))
#
# # Center at (25, 25) longer along y-axis
# rv = multivariate_normal(mean=[25, 25], cov=[[25, 0], [0, 80]])
#
# # Generate Gaussian in the first quadrant
# gaussian_quadrant = rv.pdf(pos) * 40
#
# # Place Gaussian in the first quadrant
# self.image[0:50, 0:50] += gaussian_quadrant * 10
#
# self.update_signal.emit()
# time.sleep(0.1)
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
plot = EigerPlot()
plot.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,200 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>874</width>
<height>762</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="1,4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Plot Control</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Histogram MIN</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_hist_min">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Histogram MAX</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_hist_max">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
<property name="value">
<double>2.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Data Control</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="checkBox_FFT">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>FFT</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_log">
<property name="text">
<string>log</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_mask">
<property name="text">
<string>Load Mask</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_delete_mask">
<property name="text">
<string>Delete Mask</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Orientation</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_rotation">
<item>
<property name="text">
<string>0</string>
</property>
</item>
<item>
<property name="text">
<string>90</string>
</property>
</item>
<item>
<property name="text">
<string>180</string>
</property>
</item>
<item>
<property name="text">
<string>270</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Rotation</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_transpose">
<property name="text">
<string>Transpose</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Help</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_mask">
<property name="text">
<string>No Mask</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_help">
<property name="text">
<string>Help</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QWidget" name="glw_placeholder" native="true"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -2,16 +2,18 @@ import os
import numpy as np
import pyqtgraph as pg
import qdarktheme
from pyqtgraph.Qt import QtWidgets
from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtpy.QtCore import QSize
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher, UILoader
from bec_widgets.widgets import BECFigure
from bec_widgets.widgets.dock.dock_area import BECDockArea
from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBar
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
@@ -24,6 +26,7 @@ class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
self.kernel_client.start_channels()
self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
# self.set_console_font_size(70)
def shutdown_kernel(self):
self.kernel_client.stop_channels()
@@ -43,22 +46,27 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.ui.splitter.setSizes([200, 100])
self.safe_close = False
# self.figure.clean_signal.connect(self.confirm_close)
self.register = RPCRegister()
self.register.add_rpc(self.figure)
# console push
self.console.kernel_manager.kernel.shell.push(
{
"fig": self.figure,
"register": self.register,
"dock": self.dock,
"w1": self.w1,
"w2": self.w2,
"w3": self.w3,
"d0": self.d0,
"d1": self.d1,
"d2": self.d2,
"fig0": self.fig0,
"fig1": self.fig1,
"fig2": self.fig2,
"d3": self.d3,
"bar": self.bar,
"b2a": self.button_2_a,
"b2b": self.button_2_b,
"b2c": self.button_2_c,
"bec": self.figure.client,
"scans": self.figure.client.scans,
"dev": self.figure.client.device_manager.devices,
@@ -103,22 +111,25 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.c1 = self.w1.get_config()
def _init_dock(self):
self.button_1 = QtWidgets.QPushButton("Button 1 ")
self.button_2_a = QtWidgets.QPushButton("Button to be added at place 0,0 in d3")
self.button_2_b = QtWidgets.QPushButton("button after without postions specified")
self.button_2_c = QtWidgets.QPushButton("button super late")
self.button_3 = QtWidgets.QPushButton("Button above Figure ")
self.bar = SpiralProgressBar()
self.d0 = self.dock.add_dock(name="dock_0")
self.fig0 = self.d0.add_widget_bec("BECFigure")
self.fig0.image("eiger", vrange=(0, 100))
self.label_2 = QtWidgets.QLabel("label which is added separately")
self.label_3 = QtWidgets.QLabel("Label above figure")
self.d1 = self.dock.add_dock(name="dock_1", position="right")
self.fig1 = self.d1.add_widget_bec("BECFigure")
self.fig1.plot(x_name="samx", y_name="bpm4i")
self.fig1.plot(x_name="samx", y_name="bpm3a")
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
self.fig2 = self.d2.add_widget_bec("BECFigure", row=0, col=0)
self.fig2.motor_map(x_name="samx", y_name="samy")
self.fig2.plot(x_name="samx", y_name="bpm4i")
self.bar = self.d2.add_widget_bec("SpiralProgressBar", row=0, col=1)
self.bar.set_diameter(200)
self.d1 = self.dock.add_dock(widget=self.button_1, position="left")
self.d1.addWidget(self.label_2)
self.d2 = self.dock.add_dock(widget=self.bar, position="right")
self.d3 = self.dock.add_dock(name="figure")
self.fig_dock3 = BECFigure()
self.fig_dock3.plot(x_name="samx", y_name="bpm4d")
self.d3.add_widget(self.label_3)
self.d3.add_widget(self.button_3)
self.d3.add_widget(self.fig_dock3)
self.dock.save_state()
@@ -144,7 +155,6 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
app.setApplicationName("Jupyter Console")
app.setApplicationDisplayName("Jupyter Console")
qdarktheme.setup_theme("auto")
icon = QIcon()
icon.addFile(os.path.join(module_path, "assets", "terminal_icon.png"), size=QSize(48, 48))
app.setWindowIcon(icon)

View File

@@ -0,0 +1,159 @@
# import simulation_progress as SP
import numpy as np
import pyqtgraph as pg
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
class StreamApp(QWidget):
update_signal = pyqtSignal()
new_scan_id = pyqtSignal(str)
def __init__(self, device, sub_device):
super().__init__()
pg.setConfigOptions(background="w", foreground="k")
self.init_ui()
self.setWindowTitle("MCA readout")
self.data = None
self.scan_id = None
self.stream_consumer = None
self.device = device
self.sub_device = sub_device
self.start_device_consumer()
# self.start_device_consumer(self.device) # for simulation
self.new_scan_id.connect(self.create_new_stream_consumer)
self.update_signal.connect(self.plot_new)
def init_ui(self):
# Create layout and add widgets
self.layout = QVBoxLayout()
self.setLayout(self.layout)
# Create plot
self.glw = pg.GraphicsLayoutWidget()
self.layout.addWidget(self.glw)
# Create Plot and add ImageItem
self.plot_item = pg.PlotItem()
self.plot_item.setAspectLocked(False)
self.imageItem = pg.ImageItem()
# self.plot_item1D = pg.PlotItem()
# self.plot_item.addItem(self.imageItem)
# self.plot_item.addItem(self.plot_item1D)
# Setting up histogram
# self.hist = pg.HistogramLUTItem()
# self.hist.setImageItem(self.imageItem)
# self.hist.gradient.loadPreset("magma")
# self.update_hist()
# Adding Items to Graphical Layout
self.glw.addItem(self.plot_item)
# self.glw.addItem(self.hist)
@pyqtSlot(str)
def create_new_stream_consumer(self, scan_id: str):
print(f"Creating new stream consumer for scan_id: {scan_id}")
self.connect_stream_consumer(scan_id, self.device)
def connect_stream_consumer(self, scan_id, device):
if self.stream_consumer is not None:
self.stream_consumer.shutdown()
self.stream_consumer = connector.stream_consumer(
topics=MessageEndpoints.device_async_readback(scan_id=scan_id, device=device),
cb=self._streamer_cb,
parent=self,
)
self.stream_consumer.start()
def start_device_consumer(self):
self.device_consumer = connector.consumer(
topics=MessageEndpoints.scan_status(), cb=self._device_cv, parent=self
)
self.device_consumer.start()
# def start_device_consumer(self, device): #for simulation
# self.device_consumer = connector.consumer(
# topics=MessageEndpoints.device_status(device), cb=self._device_cv, parent=self
# )
#
# self.device_consumer.start()
def plot_new(self):
print(f"Printing data from plot update: {self.data}")
self.plot_item.plot(self.data[0])
# self.imageItem.setImage(self.data, autoLevels=False)
@staticmethod
def _streamer_cb(msg, *, parent, **_kwargs) -> None:
msgMCS = msg.value
print(msgMCS)
row = msgMCS.content["signals"][parent.sub_device]
metadata = msgMCS.metadata
# Check if the current number of rows is odd
# if parent.data is not None and parent.data.shape[0] % 2 == 1:
# row = np.flip(row) # Flip the row
print(f"Printing data from callback update: {row}")
parent.data = np.array([row])
# if parent.data is None:
# parent.data = np.array([row])
# else:
# parent.data = np.vstack((parent.data, row))
parent.update_signal.emit()
@staticmethod
def _device_cv(msg, *, parent, **_kwargs) -> None:
print("Getting ScanID")
msgDEV = msg.value
current_scan_id = msgDEV.content["scan_id"]
if parent.scan_id is None:
parent.scan_id = current_scan_id
parent.new_scan_id.emit(current_scan_id)
print(f"New scan_id: {current_scan_id}")
if current_scan_id != parent.scan_id:
parent.scan_id = current_scan_id
# parent.data = None
# parent.imageItem.clear()
parent.new_scan_id.emit(current_scan_id)
print(f"New scan_id: {current_scan_id}")
if __name__ == "__main__":
import argparse
from bec_lib.redis_connector import RedisConnector
parser = argparse.ArgumentParser(description="Stream App.")
parser.add_argument("--port", type=str, default="pc15543:6379", help="Port for RedisConnector")
parser.add_argument("--device", type=str, default="mcs", help="Device name")
parser.add_argument("--sub_device", type=str, default="mca4", help="Sub-device name")
args = parser.parse_args()
connector = RedisConnector(args.port)
app = QApplication([])
streamApp = StreamApp(device=args.device, sub_device=args.sub_device)
streamApp.show()
app.exec()

View File

@@ -0,0 +1,28 @@
import time
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from bec_lib.redis_connector import RedisConnector
connector = RedisConnector("localhost:6379")
metadata = {}
scan_id = "ScanID1"
metadata.update(
{"scan_id": scan_id, "async_update": "append"} # this will be different for each scan
)
for ii in range(20):
data = {"mca1": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "mca2": [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]}
msg = messages.DeviceMessage(signals=data, metadata=metadata).dumps()
connector.xadd(
topic=MessageEndpoints.device_async_readback(
scan_id=scan_id, device="mca"
), # scan_id will be different for each scan
msg={"data": msg}, # TODO should be msg_dict
expire=1800,
)
print(f"Sent {ii}")
time.sleep(0.5)

View File

@@ -0,0 +1,17 @@
selected_motors:
motor_x: "samx"
motor_y: "samy"
plot_motors:
max_points: 1000
num_dim_points: 100
scatter_size: 5
precision: 3
mode_lock: False # "Individual" or "Start/Stop". False to unlock
extra_columns:
- sample name: "sample 1"
- step_x [mu]: 25
- step_y [mu]: 25
- exp_time [s]: 1
- start: 1
- tilt [deg]: 0

View File

@@ -0,0 +1,10 @@
redis:
host: pc15543
port: 6379
mongodb:
host: localhost
port: 27017
scibec:
host: http://localhost
port: 3030
beamline: MyBeamline

View File

@@ -0,0 +1,17 @@
selected_motors:
motor_x: "samx"
motor_y: "samy"
plot_motors:
max_points: 1000
num_dim_points: 100
scatter_size: 5
precision: 3
mode_lock: Start/Stop # "Individual" or "Start/Stop"
extra_columns:
- sample name: "sample 1"
- step_x [mu]: 25
- step_y [mu]: 25
- exp_time [s]: 1
- start: 1
- tilt [deg]: 0

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>845</width>
<height>635</height>
</rect>
</property>
<property name="windowTitle">
<string>Line Plot</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QSplitter" name="splitter_plot">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QWidget" name="glw_plot_placeholder" native="true"/>
<widget class="QWidget" name="glw_image_placeholder" native="true"/>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,1,1,15">
<item>
<widget class="QPushButton" name="pushButton_generate">
<property name="text">
<string>Generate 1D and 2D data without stream</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>1st angle of azimutal segment (deg)</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum">
<double>360.000000000000000</double>
</property>
<property name="singleStep">
<double>0.250000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>f1amp</string>
</property>
</item>
<item>
<property name="text">
<string>f2amp</string>
</property>
</item>
<item>
<property name="text">
<string>f2 phase</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Precision</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_precision">
<property name="value">
<number>4</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="cursor_table">
<property name="textElideMode">
<enum>Qt::ElideMiddle</enum>
</property>
<column>
<property name="text">
<string>Display</string>
</property>
</column>
<column>
<property name="text">
<string>X</string>
</property>
</column>
<column>
<property name="text">
<string>Y</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,342 @@
import os
import threading
import time
import numpy as np
import pyqtgraph
import pyqtgraph as pg
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from bec_lib.redis_connector import RedisConnector
from pyqtgraph import mkBrush, mkPen
from pyqtgraph.Qt import QtCore, QtWidgets
from qtpy.QtCore import Signal, Slot
from qtpy.QtWidgets import QTableWidgetItem, QVBoxLayout
from bec_widgets.utils import Colors, Crosshair, UILoader
from bec_widgets.utils.bec_dispatcher import BECDispatcher
class StreamPlot(QtWidgets.QWidget):
update_signal = Signal()
roi_signal = Signal(tuple)
def __init__(self, name="", y_value_list=["gauss_bpm"], client=None, parent=None) -> None:
"""
Basic plot widget for displaying scan data.
Args:
name (str, optional): Name of the plot. Defaults to "".
y_value_list (list, optional): List of signals to be plotted. Defaults to ["gauss_bpm"].
"""
# Client and device manager from BEC
self.client = BECDispatcher().client if client is None else client
super(StreamPlot, self).__init__()
# Set style for pyqtgraph plots
pg.setConfigOption("background", "w")
pg.setConfigOption("foreground", "k")
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "line_plot.ui"), self)
self._idle_time = 100
self.connector = RedisConnector(["localhost:6379"])
self.y_value_list = y_value_list
self.previous_y_value_list = None
self.plotter_data_x = []
self.plotter_data_y = []
self.plotter_scan_id = None
self._current_proj = None
self._current_metadata_ep = "px_stream/projection_{}/metadata"
self.proxy_update = pg.SignalProxy(self.update_signal, rateLimit=25, slot=self.update)
self._data_retriever_thread_exit_event = threading.Event()
self.data_retriever = threading.Thread(
target=self.on_projection, args=(self._data_retriever_thread_exit_event,), daemon=True
)
self.data_retriever.start()
##########################
# UI
##########################
self.init_ui()
self.init_curves()
self.hook_crosshair()
def close(self):
super().close()
self._data_retriever_thread_exit_event.set()
self.data_retriever.join()
def init_ui(self):
"""Setup all ui elements"""
##########################
# 1D Plot
##########################
# LabelItem for ROI
self.label_plot = pg.LabelItem(justify="center")
self.glw_plot_layout = QVBoxLayout(self.ui.glw_plot_placeholder)
self.glw_plot = pg.GraphicsLayoutWidget()
self.glw_plot_layout.addWidget(self.glw_plot)
self.glw_plot.addItem(self.label_plot)
self.label_plot.setText("ROI region")
# ROI selector - so far from [-1,1] #TODO update to scale with xrange
self.roi_selector = pg.LinearRegionItem([-1, 1])
self.glw_plot.nextRow() # TODO update of cursor
self.label_plot_moved = pg.LabelItem(justify="center")
self.glw_plot.addItem(self.label_plot_moved)
self.label_plot_moved.setText("Actual coordinates (X, Y)")
# Label for coordinates clicked
self.glw_plot.nextRow()
self.label_plot_clicked = pg.LabelItem(justify="center")
self.glw_plot.addItem(self.label_plot_clicked)
self.label_plot_clicked.setText("Clicked coordinates (X, Y)")
# 1D PlotItem
self.glw_plot.nextRow()
self.plot = pg.PlotItem()
self.plot.setLogMode(True, True)
self.glw_plot.addItem(self.plot)
self.plot.addLegend()
##########################
# 2D Plot
##########################
# Label for coordinates moved
self.label_image_moved = pg.LabelItem(justify="center")
self.glw_image_layout = QVBoxLayout(self.ui.glw_image_placeholder)
self.glw_image = pg.GraphicsLayoutWidget()
self.glw_plot_layout.addWidget(self.glw_image)
self.glw_image.addItem(self.label_image_moved)
self.label_image_moved.setText("Actual coordinates (X, Y)")
# Label for coordinates clicked
self.glw_image.nextRow()
self.label_image_clicked = pg.LabelItem(justify="center")
self.glw_image.addItem(self.label_image_clicked)
self.label_image_clicked.setText("Clicked coordinates (X, Y)")
# TODO try to lock aspect ratio with view
# # Create a window
# win = pg.GraphicsLayoutWidget()
# win.show()
#
# # Create a ViewBox
# view = win.addViewBox()
#
# # Lock the aspect ratio
# view.setAspectLocked(True)
# # Create an ImageItem
# image_item = pg.ImageItem(np.random.random((100, 100)))
#
# # Add the ImageItem to the ViewBox
# view.addItem(image_item)
# 2D ImageItem
self.glw_image.nextRow()
self.plot_image = pg.PlotItem()
self.glw_image.addItem(self.plot_image)
def init_curves(self):
# init of 1D plot
self.plot.clear()
self.curves = []
self.pens = []
self.brushs = []
self.color_list = Colors.golden_angle_color(colormap="CET-R2", num=len(self.y_value_list))
for ii, y_value in enumerate(self.y_value_list):
pen = mkPen(color=self.color_list[ii], width=2, style=QtCore.Qt.DashLine)
brush = mkBrush(color=self.color_list[ii])
curve = pg.PlotDataItem(symbolBrush=brush, pen=pen, skipFiniteCheck=True, name=y_value)
self.plot.addItem(curve)
self.curves.append(curve)
self.pens.append(pen)
self.brushs.append(brush)
# check if roi selector is in the plot
if self.roi_selector not in self.plot.items:
self.plot.addItem(self.roi_selector)
# init of 2D plot
self.plot_image.clear()
self.img = pg.ImageItem()
self.plot_image.addItem(self.img)
# hooking signals
self.hook_crosshair()
self.init_table()
def splitter_sizes(self): ...
def hook_crosshair(self):
self.crosshair_1d = Crosshair(self.plot, precision=4)
self.crosshair_1d.coordinatesChanged1D.connect(
lambda x, y: self.label_plot_moved.setText(f"Moved : ({x}, {y})")
)
self.crosshair_1d.coordinatesClicked1D.connect(
lambda x, y: self.label_plot_clicked.setText(f"Moved : ({x}, {y})")
)
self.crosshair_1d.coordinatesChanged1D.connect(
lambda x, y: self.update_table(table_widget=self.cursor_table, x=x, y_values=y)
)
self.crosshair_2D = Crosshair(self.plot_image)
self.crosshair_2D.coordinatesChanged2D.connect(
lambda x, y: self.label_image_moved.setText(f"Moved : ({x}, {y})")
)
self.crosshair_2D.coordinatesClicked2D.connect(
lambda x, y: self.label_image_clicked.setText(f"Moved : ({x}, {y})")
)
# ROI
self.roi_selector.sigRegionChangeFinished.connect(self.get_roi_region)
def get_roi_region(self):
"""For testing purpose now, get roi region and print it to self.label as tuple"""
region = self.roi_selector.getRegion()
self.label_plot.setText(f"x = {(10 ** region[0]):.4f}, y ={(10 ** region[1]):.4f}")
return_dict = {
"horiz_roi": [
np.where(self.plotter_data_x[0] > 10 ** region[0])[0][0],
np.where(self.plotter_data_x[0] < 10 ** region[1])[0][-1],
]
}
msg = messages.DeviceMessage(signals=return_dict).dumps()
self.connector.set_and_publish("px_stream/gui_event", msg=msg)
self.roi_signal.emit(region)
def init_table(self):
# Init number of rows in table according to n of devices
self.ui.cursor_table.setRowCount(len(self.y_value_list))
# self.table.setHorizontalHeaderLabels(["(X, Y) - Moved", "(X, Y) - Clicked"]) #TODO can be dynamic
self.ui.cursor_table.setVerticalHeaderLabels(self.y_value_list)
self.ui.cursor_table.resizeColumnsToContents()
def update_table(self, table_widget, x, y_values):
for i, y in enumerate(y_values):
table_widget.setItem(i, 1, QTableWidgetItem(str(x)))
table_widget.setItem(i, 2, QTableWidgetItem(str(y)))
table_widget.resizeColumnsToContents()
def update(self):
"""Update the plot with the new data."""
# check if QTable was initialised and if list of devices was changed
# if self.y_value_list != self.previous_y_value_list:
# self.setup_cursor_table()
# self.previous_y_value_list = self.y_value_list.copy() if self.y_value_list else None
self.curves[0].setData(self.plotter_data_x[0], self.plotter_data_y[0])
@staticmethod
def flip_even_rows(arr):
arr_copy = np.copy(arr) # Create a writable copy
arr_copy[1::2, :] = arr_copy[1::2, ::-1]
return arr_copy
@staticmethod
def remove_curve_by_name(plot: pyqtgraph.PlotItem, name: str) -> None:
# def remove_curve_by_name(plot: pyqtgraph.PlotItem, checkbox: QtWidgets.QCheckBox, name: str) -> None:
"""Removes a curve from the given plot by the specified name.
Args:
plot (pyqtgraph.PlotItem): The plot from which to remove the curve.
name (str): The name of the curve to remove.
"""
# if checkbox.isChecked():
for item in plot.items:
if isinstance(item, pg.PlotDataItem) and getattr(item, "opts", {}).get("name") == name:
plot.removeItem(item)
return
# else:
# return
def on_projection(self, exit_event):
while not exit_event.is_set():
if self._current_proj is None:
time.sleep(0.1)
continue
endpoint = f"px_stream/projection_{self._current_proj}/data"
msgs = self.client.connector.lrange(topic=endpoint, start=-1, end=-1)
data = msgs
if not data:
continue
with np.errstate(divide="ignore", invalid="ignore"):
self.plotter_data_y = [
np.sum(
np.sum(data[-1].content["signals"]["data"] * self._current_norm, axis=1)
/ np.sum(self._current_norm, axis=0),
axis=0,
).squeeze()
]
self.update_signal.emit()
@Slot(dict, dict)
def on_dap_update(self, data: dict, metadata: dict):
flipped_data = self.flip_even_rows(data["data"]["z"])
self.img.setImage(flipped_data)
@Slot(dict, dict)
def new_proj(self, content: dict, _metadata: dict):
proj_nr = content["signals"]["proj_nr"]
endpoint = f"px_stream/projection_{proj_nr}/metadata"
msg_raw = self.client.connector.get(topic=endpoint)
msg = messages.DeviceMessage.loads(msg_raw)
self._current_q = msg.content["signals"]["q"]
self._current_norm = msg.content["signals"]["norm_sum"]
self._current_metadata = msg.content["signals"]["metadata"]
self.plotter_data_x = [self._current_q]
self._current_proj = proj_nr
if __name__ == "__main__":
import argparse
# from bec_widgets import ctrl_c # TODO uncomment when ctrl_c is ready to be compatible with qtpy
parser = argparse.ArgumentParser()
parser.add_argument(
"--signals", help="specify recorded signals", nargs="+", default=["gauss_bpm"]
)
# default = ["gauss_bpm", "bpm4i", "bpm5i", "bpm6i", "xert"],
value = parser.parse_args()
print(f"Plotting signals for: {', '.join(value.signals)}")
# Client from dispatcher
bec_dispatcher = BECDispatcher()
client = bec_dispatcher.client
app = QtWidgets.QApplication([])
# ctrl_c.setup(app) # TODO uncomment when ctrl_c is ready to be compatible with qtpy
plot = StreamPlot(y_value_list=value.signals, client=client)
bec_dispatcher.connect_slot(plot.new_proj, "px_stream/proj_nr")
bec_dispatcher.connect_slot(
plot.on_dap_update, MessageEndpoints.processed_data("px_dap_worker")
)
plot.show()
# client.callbacks.register("scan_segment", plot, sync=False)
app.exec()

View File

View File

@@ -0,0 +1,17 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
"""PySide6 port of the Qt Designer taskmenuextension example from Qt v6.x"""
import sys
from bec_ipython_client.main import BECIPythonClient
from PySide6.QtWidgets import QApplication
from tictactoe import TicTacToe
if __name__ == "__main__":
app = QApplication(sys.argv)
window = TicTacToe()
window.state = "-X-XO----"
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,24 @@
import os
import subprocess
import sys
from PySide6.scripts.pyside_tool import designer
import bec_widgets
def main():
# os.environ["PYSIDE_DESIGNER_PLUGINS"] = os.path.join(
# "/Users/janwyzula/PSI/bec_widgets/bec_widgets/plugin"
# )
os.environ["PYSIDE_DESIGNER_PLUGINS"] = os.path.join(
os.path.dirname(bec_widgets.__file__), "widgets/motor_control/selection"
)
# os.environ["PYTHONFRAMEWORKPREFIX"] = os.path.join(
# os.path.dirname(bec_widgets.__file__), "widgets/motor_control/selection"
# )
designer()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,12 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from tictactoe import TicTacToe
from tictactoeplugin import TicTacToePlugin
# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin
if __name__ == "__main__":
QPyDesignerCustomWidgetCollection.addCustomWidget(TicTacToePlugin())

View File

@@ -0,0 +1,4 @@
{
"files": ["tictactoe.py", "main.py", "registertictactoe.py", "tictactoeplugin.py",
"tictactoetaskmenu.py"]
}

View File

@@ -0,0 +1,135 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import Property, QPoint, QRect, QSize, Qt, Slot
from PySide6.QtGui import QPainter, QPen
from PySide6.QtWidgets import QWidget
EMPTY = "-"
CROSS = "X"
NOUGHT = "O"
DEFAULT_STATE = "---------"
class TicTacToe(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._state = DEFAULT_STATE
self._turn_number = 0
def minimumSizeHint(self):
return QSize(200, 200)
def sizeHint(self):
return QSize(200, 200)
def setState(self, new_state):
self._turn_number = 0
self._state = DEFAULT_STATE
for position in range(min(9, len(new_state))):
mark = new_state[position]
if mark == CROSS or mark == NOUGHT:
self._turn_number += 1
self._change_state_at(position, mark)
position += 1
self.update()
def state(self):
return self._state
@Slot()
def clear_board(self):
self._state = DEFAULT_STATE
self._turn_number = 0
self.update()
def _change_state_at(self, pos, new_state):
self._state = self._state[:pos] + new_state + self._state[pos + 1 :]
def mousePressEvent(self, event):
if self._turn_number == 9:
self.clear_board()
return
for position in range(9):
cell = self._cell_rect(position)
if cell.contains(event.position().toPoint()):
if self._state[position] == EMPTY:
new_state = CROSS if self._turn_number % 2 == 0 else NOUGHT
self._change_state_at(position, new_state)
self._turn_number += 1
self.update()
def paintEvent(self, event):
with QPainter(self) as painter:
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(QPen(Qt.darkGreen, 1))
painter.drawLine(self._cell_width(), 0, self._cell_width(), self.height())
painter.drawLine(2 * self._cell_width(), 0, 2 * self._cell_width(), self.height())
painter.drawLine(0, self._cell_height(), self.width(), self._cell_height())
painter.drawLine(0, 2 * self._cell_height(), self.width(), 2 * self._cell_height())
painter.setPen(QPen(Qt.darkBlue, 2))
for position in range(9):
cell = self._cell_rect(position)
if self._state[position] == CROSS:
painter.drawLine(cell.topLeft(), cell.bottomRight())
painter.drawLine(cell.topRight(), cell.bottomLeft())
elif self._state[position] == NOUGHT:
painter.drawEllipse(cell)
painter.setPen(QPen(Qt.yellow, 3))
for position in range(0, 8, 3):
if (
self._state[position] != EMPTY
and self._state[position + 1] == self._state[position]
and self._state[position + 2] == self._state[position]
):
y = self._cell_rect(position).center().y()
painter.drawLine(0, y, self.width(), y)
self._turn_number = 9
for position in range(3):
if (
self._state[position] != EMPTY
and self._state[position + 3] == self._state[position]
and self._state[position + 6] == self._state[position]
):
x = self._cell_rect(position).center().x()
painter.drawLine(x, 0, x, self.height())
self._turn_number = 9
if (
self._state[0] != EMPTY
and self._state[4] == self._state[0]
and self._state[8] == self._state[0]
):
painter.drawLine(0, 0, self.width(), self.height())
self._turn_number = 9
if (
self._state[2] != EMPTY
and self._state[4] == self._state[2]
and self._state[6] == self._state[2]
):
painter.drawLine(0, self.height(), self.width(), 0)
self._turn_number = 9
def _cell_rect(self, position):
h_margin = self.width() / 30
v_margin = self.height() / 30
row = int(position / 3)
column = position - 3 * row
pos = QPoint(column * self._cell_width() + h_margin, row * self._cell_height() + v_margin)
size = QSize(self._cell_width() - 2 * h_margin, self._cell_height() - 2 * v_margin)
return QRect(pos, size)
def _cell_width(self):
return self.width() / 3
def _cell_height(self):
return self.height() / 3
state = Property(str, state, setState)

View File

@@ -0,0 +1,68 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtDesigner import QDesignerCustomWidgetInterface
from PySide6.QtGui import QIcon
from tictactoe import TicTacToe
from tictactoetaskmenu import TicTacToeTaskMenuFactory
DOM_XML = """
<ui language='c++'>
<widget class='TicTacToe' name='ticTacToe'>
<property name='geometry'>
<rect>
<x>0</x>
<y>0</y>
<width>200</width>
<height>200</height>
</rect>
</property>
<property name='state'>
<string>-X-XO----</string>
</property>
</widget>
</ui>
"""
class TicTacToePlugin(QDesignerCustomWidgetInterface):
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = TicTacToe(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "tictactoe"
def initialize(self, form_editor):
self._form_editor = form_editor
manager = form_editor.extensionManager()
iid = TicTacToeTaskMenuFactory.task_menu_iid()
manager.registerExtensions(TicTacToeTaskMenuFactory(manager), iid)
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "TicTacToe"
def toolTip(self):
return "Tic Tac Toe Example, demonstrating class QDesignerTaskMenuExtension (Python)"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,67 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import Slot
from PySide6.QtDesigner import QExtensionFactory, QPyDesignerTaskMenuExtension
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
from tictactoe import TicTacToe
class TicTacToeDialog(QDialog):
def __init__(self, parent):
super().__init__(parent)
layout = QVBoxLayout(self)
self._ticTacToe = TicTacToe(self)
layout.addWidget(self._ticTacToe)
button_box = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Reset
)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
reset_button = button_box.button(QDialogButtonBox.Reset)
reset_button.clicked.connect(self._ticTacToe.clear_board)
layout.addWidget(button_box)
def set_state(self, new_state):
self._ticTacToe.setState(new_state)
def state(self):
return self._ticTacToe.state
class TicTacToeTaskMenu(QPyDesignerTaskMenuExtension):
def __init__(self, ticTacToe, parent):
super().__init__(parent)
self._ticTacToe = ticTacToe
self._edit_state_action = QAction("Edit State...", None)
self._edit_state_action.triggered.connect(self._edit_state)
def taskActions(self):
return [self._edit_state_action]
def preferredEditAction(self):
return self._edit_state_action
@Slot()
def _edit_state(self):
dialog = TicTacToeDialog(self._ticTacToe)
dialog.set_state(self._ticTacToe.state)
if dialog.exec() == QDialog.Accepted:
self._ticTacToe.state = dialog.state()
class TicTacToeTaskMenuFactory(QExtensionFactory):
def __init__(self, extension_manager):
super().__init__(extension_manager)
@staticmethod
def task_menu_iid():
return "org.qt-project.Qt.Designer.TaskMenu"
def createExtension(self, object, iid, parent):
if iid != TicTacToeTaskMenuFactory.task_menu_iid():
return None
if object.__class__.__name__ != "TicTacToe":
return None
return TicTacToeTaskMenu(object, parent)

View File

@@ -174,11 +174,11 @@ class Crosshair(QObject):
if isinstance(item, pg.PlotDataItem):
if x is None or all(v is None for v in y_values):
return
coordinate_to_emit = (
coordinance_to_emit = (
round(x, self.precision),
[round(y_val, self.precision) for y_val in y_values],
)
self.coordinatesChanged1D.emit(coordinate_to_emit)
self.coordinatesChanged1D.emit(coordinance_to_emit)
for i, y_val in enumerate(y_values):
self.marker_moved_1d[i].setData(
[x if not self.is_log_x else np.log10(x)],
@@ -187,8 +187,8 @@ class Crosshair(QObject):
elif isinstance(item, pg.ImageItem):
if x is None or y_values is None:
return
coordinate_to_emit = (x, y_values)
self.coordinatesChanged2D.emit(coordinate_to_emit)
coordinance_to_emit = (x, y_values)
self.coordinatesChanged2D.emit(coordinance_to_emit)
def mouse_clicked(self, event):
"""Handles the mouse clicked event, updating the crosshair position and emitting signals.

View File

@@ -0,0 +1,14 @@
from qtpy.QtWidgets import QComboBox
from bec_widgets.utils import BECConnector, ConnectionConfig
class DeviceCombobox(BECConnector, QComboBox):
def __init__(self, parent=None, client=None, config=None, gui_id=None):
super().__init__(client=client, config=config, gui_id=gui_id)
QComboBox.__init__(self, parent=parent)
self.get_bec_shortcuts()
def get_device(self):
return getattr(self.dev, self.text().lower())

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal, Optional
from typing import TYPE_CHECKING, Literal, Optional
from pydantic import Field
from pyqtgraph.dockarea import Dock
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
class DockConfig(ConnectionConfig):
widgets: dict[str, Any] = Field({}, description="The widgets in the dock.")
widgets: dict[str, ConnectionConfig] = Field({}, description="The widgets in the dock.")
position: Literal["bottom", "top", "left", "right", "above", "below"] = Field(
"bottom", description="The position of the dock."
)
@@ -26,7 +26,6 @@ class DockConfig(ConnectionConfig):
class BECDock(BECConnector, Dock):
USER_ACCESS = [
"config_dict",
"rpc_id",
"widget_list",
"show_title_bar",
@@ -180,7 +179,6 @@ class BECDock(BECConnector, Dock):
widget = RPCWidgetHandler.create_widget(widget_type)
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
self.config.widgets[widget.gui_id] = widget.config
return widget
@@ -211,7 +209,6 @@ class BECDock(BECConnector, Dock):
self.layout_manager.shift_widgets(shift, start_row=row)
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
self.config.widgets[widget.gui_id] = widget.config
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
"""
@@ -245,7 +242,6 @@ class BECDock(BECConnector, Dock):
"""
widget = self.rpc_register.get_rpc_by_id(widget_rpc_id)
self.layout.removeWidget(widget)
self.config.widgets.pop(widget_rpc_id, None)
widget.close()
def remove(self):

View File

@@ -16,14 +16,10 @@ from .dock import BECDock, DockConfig
class DockAreaConfig(ConnectionConfig):
docks: dict[str, DockConfig] = Field({}, description="The docks in the dock area.")
docks_state: Optional[dict] = Field(
None, description="The state of the docks in the dock area."
)
class BECDockArea(BECConnector, DockArea):
USER_ACCESS = [
"config_dict",
"panels",
"save_state",
"remove_dock",
@@ -85,7 +81,7 @@ class BECDockArea(BECConnector, DockArea):
extra(str): Extra docks that are in the dockarea but that are not mentioned in state will be added to the bottom of the dockarea, unless otherwise specified by the extra argument.
"""
if state is None:
state = self.config.docks_state
state = self._last_state
self.restoreState(state, missing=missing, extra=extra)
def save_state(self) -> dict:
@@ -95,9 +91,8 @@ class BECDockArea(BECConnector, DockArea):
Returns:
dict: The state of the dock area.
"""
last_state = self.saveState()
self.config.docks_state = last_state
return last_state
self._last_state = self.saveState()
return self._last_state
def remove_dock(self, name: str):
"""

View File

@@ -26,7 +26,7 @@ class FigureConfig(ConnectionConfig):
theme: Literal["dark", "light"] = Field("dark", description="The theme of the figure widget.")
num_cols: int = Field(1, description="The number of columns in the figure widget.")
num_rows: int = Field(1, description="The number of rows in the figure widget.")
widgets: dict[str, Waveform1DConfig | ImageConfig | MotorMapConfig | SubplotConfig] = Field(
widgets: dict[str, SubplotConfig] = Field(
{}, description="The list of widgets to be added to the figure widget."
)

View File

@@ -1,12 +1,12 @@
import os
from qtpy.QtWidgets import QWidget
from qtpy import uic
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlErrors, MotorControlWidget
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget, MotorControlErrors
class MotorControlAbsolute(MotorControlWidget):

View File

@@ -7,7 +7,6 @@ from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QKeySequence
from qtpy.QtWidgets import QDoubleSpinBox, QShortcut, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
@@ -28,7 +27,7 @@ class MotorControlRelative(MotorControlWidget):
"""Load the UI from the .ui file."""
# Loading UI
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "movement_relative.ui"), self)
uic.loadUi(os.path.join(current_path, "movement_relative.ui"), self)
def _init_ui(self):
"""Initialize the UI."""
@@ -52,15 +51,15 @@ class MotorControlRelative(MotorControlWidget):
# Update step precision
self.precision = self.config["motor_control"]["precision"]
self.ui.spinBox_precision.setValue(self.precision)
self.spinBox_precision.setValue(self.precision)
# Update step sizes
self.ui.spinBox_step_x.setValue(self.config["motor_control"]["step_size_x"])
self.ui.spinBox_step_y.setValue(self.config["motor_control"]["step_size_y"])
self.spinBox_step_x.setValue(self.config["motor_control"]["step_size_x"])
self.spinBox_step_y.setValue(self.config["motor_control"]["step_size_y"])
# Checkboxes for keyboard shortcuts and x/y step size link
self.ui.checkBox_same_xy.setChecked(self.config["motor_control"]["step_x_y_same"])
self.ui.checkBox_enableArrows.setChecked(self.config["motor_control"]["move_with_arrows"])
self.checkBox_same_xy.setChecked(self.config["motor_control"]["step_x_y_same"])
self.checkBox_enableArrows.setChecked(self.config["motor_control"]["move_with_arrows"])
self._init_ui()
@@ -68,32 +67,30 @@ class MotorControlRelative(MotorControlWidget):
"""Initialize the motor control elements"""
# Connect checkbox and spinBoxes
self.ui.checkBox_same_xy.stateChanged.connect(self._sync_step_sizes)
self.ui.spinBox_step_x.valueChanged.connect(self._update_step_size_x)
self.ui.spinBox_step_y.valueChanged.connect(self._update_step_size_y)
self.checkBox_same_xy.stateChanged.connect(self._sync_step_sizes)
self.spinBox_step_x.valueChanged.connect(self._update_step_size_x)
self.spinBox_step_y.valueChanged.connect(self._update_step_size_y)
self.ui.toolButton_right.clicked.connect(
self.toolButton_right.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, "x", 1)
)
self.ui.toolButton_left.clicked.connect(
self.toolButton_left.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, "x", -1)
)
self.ui.toolButton_up.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, "y", 1)
)
self.ui.toolButton_down.clicked.connect(
self.toolButton_up.clicked.connect(lambda: self.move_motor_relative(self.motor_y, "y", 1))
self.toolButton_down.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, "y", -1)
)
# Switch between key shortcuts active
self.ui.checkBox_enableArrows.stateChanged.connect(self._update_arrow_key_shortcuts)
self.checkBox_enableArrows.stateChanged.connect(self._update_arrow_key_shortcuts)
self._update_arrow_key_shortcuts()
# Enable/Disable GUI
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
# Precision update
self.ui.spinBox_precision.valueChanged.connect(lambda x: self._update_precision(x))
self.spinBox_precision.valueChanged.connect(lambda x: self._update_precision(x))
# Error messages
self.motor_thread.motor_error.connect(
@@ -101,7 +98,7 @@ class MotorControlRelative(MotorControlWidget):
)
# Stop Button
self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
def _init_keyboard_shortcuts(self) -> None:
"""Initialize the keyboard shortcuts"""
@@ -110,42 +107,42 @@ class MotorControlRelative(MotorControlWidget):
increase_x_shortcut = QShortcut(QKeySequence("Ctrl+A"), self)
decrease_x_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
increase_x_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_x, 2)
lambda: self._change_step_size(self.spinBox_step_x, 2)
)
decrease_x_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_x, 0.5)
lambda: self._change_step_size(self.spinBox_step_x, 0.5)
)
self.ui.spinBox_step_x.setToolTip("Increase step size: Ctrl+A\nDecrease step size: Ctrl+Z")
self.spinBox_step_x.setToolTip("Increase step size: Ctrl+A\nDecrease step size: Ctrl+Z")
# Increase/decrease step size for Y motor
increase_y_shortcut = QShortcut(QKeySequence("Alt+A"), self)
decrease_y_shortcut = QShortcut(QKeySequence("Alt+Z"), self)
increase_y_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_y, 2)
lambda: self._change_step_size(self.spinBox_step_y, 2)
)
decrease_y_shortcut.activated.connect(
lambda: self._change_step_size(self.ui.spinBox_step_y, 0.5)
lambda: self._change_step_size(self.spinBox_step_y, 0.5)
)
self.ui.spinBox_step_y.setToolTip("Increase step size: Alt+A\nDecrease step size: Alt+Z")
self.spinBox_step_y.setToolTip("Increase step size: Alt+A\nDecrease step size: Alt+Z")
# Stop Button
self.ui.pushButton_stop.setShortcut("Ctrl+X")
self.ui.pushButton_stop.setToolTip("Ctrl+X")
self.pushButton_stop.setShortcut("Ctrl+X")
self.pushButton_stop.setToolTip("Ctrl+X")
def _update_arrow_key_shortcuts(self) -> None:
"""Update the arrow key shortcuts based on the checkbox state."""
if self.ui.checkBox_enableArrows.isChecked():
if self.checkBox_enableArrows.isChecked():
# Set the arrow key shortcuts for motor movement
self.ui.toolButton_right.setShortcut(Qt.Key_Right)
self.ui.toolButton_left.setShortcut(Qt.Key_Left)
self.ui.toolButton_up.setShortcut(Qt.Key_Up)
self.ui.toolButton_down.setShortcut(Qt.Key_Down)
self.toolButton_right.setShortcut(Qt.Key_Right)
self.toolButton_left.setShortcut(Qt.Key_Left)
self.toolButton_up.setShortcut(Qt.Key_Up)
self.toolButton_down.setShortcut(Qt.Key_Down)
else:
# Clear the shortcuts
self.ui.toolButton_right.setShortcut("")
self.ui.toolButton_left.setShortcut("")
self.ui.toolButton_up.setShortcut("")
self.ui.toolButton_down.setShortcut("")
self.toolButton_right.setShortcut("")
self.toolButton_left.setShortcut("")
self.toolButton_up.setShortcut("")
self.toolButton_down.setShortcut("")
def _update_precision(self, precision: int) -> None:
"""
@@ -153,8 +150,8 @@ class MotorControlRelative(MotorControlWidget):
Args:
precision(int): Precision of the coordinates.
"""
self.ui.spinBox_step_x.setDecimals(precision)
self.ui.spinBox_step_y.setDecimals(precision)
self.spinBox_step_x.setDecimals(precision)
self.spinBox_step_y.setDecimals(precision)
self.precision_signal.emit(precision)
def _change_step_size(self, spinBox: QDoubleSpinBox, factor: float) -> None:
@@ -170,21 +167,21 @@ class MotorControlRelative(MotorControlWidget):
def _sync_step_sizes(self):
"""Sync step sizes based on checkbox state."""
if self.ui.checkBox_same_xy.isChecked():
value = self.ui.spinBox_step_x.value()
self.ui.spinBox_step_y.setValue(value)
if self.checkBox_same_xy.isChecked():
value = self.spinBox_step_x.value()
self.spinBox_step_y.setValue(value)
def _update_step_size_x(self):
"""Update step size for x if checkbox is checked."""
if self.ui.checkBox_same_xy.isChecked():
value = self.ui.spinBox_step_x.value()
self.ui.spinBox_step_y.setValue(value)
if self.checkBox_same_xy.isChecked():
value = self.spinBox_step_x.value()
self.spinBox_step_y.setValue(value)
def _update_step_size_y(self):
"""Update step size for y if checkbox is checked."""
if self.ui.checkBox_same_xy.isChecked():
value = self.ui.spinBox_step_y.value()
self.ui.spinBox_step_x.setValue(value)
if self.checkBox_same_xy.isChecked():
value = self.spinBox_step_y.value()
self.spinBox_step_x.setValue(value)
@pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str):
@@ -209,11 +206,11 @@ class MotorControlRelative(MotorControlWidget):
"""
# Disable or enable all controls within the motorControl_absolute group box
for widget in self.ui.motorControl.findChildren(QWidget):
for widget in self.motorControl.findChildren(QWidget):
widget.setEnabled(disable)
# Enable the pushButton_stop if the motor is moving
self.ui.pushButton_stop.setEnabled(True)
self.pushButton_stop.setEnabled(True)
def move_motor_relative(self, motor, axis: str, direction: int) -> None:
"""
@@ -224,7 +221,7 @@ class MotorControlRelative(MotorControlWidget):
direction(int): Direction to move. 1 for positive, -1 for negative.
"""
if axis == "x":
step = direction * self.ui.spinBox_step_x.value()
step = direction * self.spinBox_step_x.value()
elif axis == "y":
step = direction * self.ui.spinBox_step_y.value()
step = direction * self.spinBox_step_y.value()
self.motor_thread.move_relative(motor, step)

View File

@@ -0,0 +1,30 @@
import qdarktheme
from qtpy.QtWidgets import QApplication
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.widgets.motor_control.selection.selection import MotorControlSelection
CONFIG_DEFAULT = {
"motor_control": {
"motor_x": "samx",
"motor_y": "samy",
"step_size_x": 3,
"step_size_y": 5,
"precision": 4,
"step_x_y_same": False,
"move_with_arrows": False,
}
}
if __name__ == "__main__":
bec_dispatcher = BECDispatcher()
# BECclient global variables
client = bec_dispatcher.client
client.start()
app = QApplication([])
qdarktheme.setup_theme("auto")
motor_control = MotorControlSelection(client=client, config=CONFIG_DEFAULT)
window = motor_control
window.show()
app.exec()

View File

@@ -0,0 +1,13 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from selection import MotorControlSelection
from selectionplugin import MotorControlSelectionPlugin
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin
if __name__ == "__main__":
QPyDesignerCustomWidgetCollection.addCustomWidget(MotorControlSelectionPlugin())

View File

@@ -0,0 +1,4 @@
{
"files": ["selection.py", "motor_selection_launch.py", "registertictactoe.py", "tictactoeplugin.py",
"tictactoetaskmenu.py"]
}

View File

@@ -0,0 +1,58 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from selection import MotorControlSelection
from PySide6.QtGui import QIcon
from PySide6.QtDesigner import QDesignerCustomWidgetInterface
DOM_XML = """
<ui language='c++'>
<widget class='MotorControlSelection' name='selection'>
</widget>
</ui>
"""
class MotorControlSelectionPlugin(QDesignerCustomWidgetInterface):
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = MotorControlSelection(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return QIcon()
def includeFile(self):
return "selection"
def initialize(self, form_editor):
self._form_editor = form_editor
# manager = form_editor.extensionManager()
# iid = TicTacToeTaskMenuFactory.task_menu_iid()
# manager.registerExtensions(TicTacToeTaskMenuFactory(manager), iid)
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "MotorControlSelection"
def toolTip(self):
return "MotorControl Selection Example for BEC Widgets"
def whatsThis(self):
return self.toolTip()

View File

@@ -17,16 +17,16 @@ class RingConnections(BaseModel):
@field_validator("endpoint")
def validate_endpoint(cls, v, values):
slot = values.data["slot"]
v = v.endpoint if isinstance(v, EndpointInfo) else v
endpoint = v.endpoint if isinstance(v, EndpointInfo) else v
if slot == "on_scan_progress":
if v != "scans/scan_progress":
if endpoint != "scans/scan_progress":
raise PydanticCustomError(
"unsupported endpoint",
"For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'.",
{"wrong_value": v},
)
elif slot == "on_device_readback":
if not v.startswith("internal/devices/readback/"):
if not endpoint.startswith("internal/devices/readback/"):
raise PydanticCustomError(
"unsupported endpoint",
"For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'.",
@@ -36,7 +36,6 @@ class RingConnections(BaseModel):
class RingConfig(ConnectionConfig):
value: int | float | None = Field(0, description="Value for the progress bars.")
direction: int | None = Field(
-1, description="Direction of the progress bars. -1 for clockwise, 1 for counter-clockwise."
)
@@ -55,8 +54,8 @@ class RingConfig(ConnectionConfig):
description="Start position for the progress bars in degrees. Default is 90 degrees - corespons to "
"the top of the ring.",
)
min_value: int | float | None = Field(0, description="Minimum value for the progress bars.")
max_value: int | float | None = Field(100, description="Maximum value for the progress bars.")
min_value: int | None = Field(0, description="Minimum value for the progress bars.")
max_value: int | None = Field(100, description="Maximum value for the progress bars.")
precision: int | None = Field(3, description="Precision for the progress bars.")
update_behaviour: Literal["manual", "auto"] | None = Field(
"auto", description="Update behaviour for the progress bars."
@@ -103,6 +102,7 @@ class Ring(BECConnector):
self.background_color = None
self.start_position = None
self.config = config
self.value = 0
self.RID = None
self._init_config_params()
@@ -114,7 +114,7 @@ class Ring(BECConnector):
self.set_connections(self.config.connections.slot, self.config.connections.endpoint)
def set_value(self, value: int | float):
self.config.value = round(
self.value = round(
max(self.config.min_value, min(self.config.max_value, value)), self.config.precision
)

View File

@@ -115,6 +115,7 @@ class SpiralProgressBar(BECConnector, QWidget):
self.entry_validator = EntryValidator(self.dev)
self.RID = None
self.values = None
# For updating bar behaviour
self._auto_updates = True
@@ -543,7 +544,7 @@ class SpiralProgressBar(BECConnector, QWidget):
pen = QtGui.QPen(ring.color, ring.config.line_width, QtCore.Qt.SolidLine)
pen.setCapStyle(QtCore.Qt.RoundCap)
painter.setPen(pen)
proportion = (ring.config.value - ring.config.min_value) / (
proportion = (ring.value - ring.config.min_value) / (
(ring.config.max_value - ring.config.min_value) + 1e-3
)
angle = int(proportion * 360 * 16 * ring.config.direction)

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z"/></svg>

Before

Width:  |  Height:  |  Size: 292 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="48px" viewBox="0 0 20 20" width="48px" fill="#000000"><g><rect fill="none" height="20" width="20" x="0"/></g><g><g><path d="M16.5,3h-13C2.67,3,2,3.67,2,4.5v9C2,14.33,2.67,15,3.5,15H7v2h6v-2h3.5c0.83,0,1.5-0.67,1.5-1.5v-9 C18,3.67,17.33,3,16.5,3z M16.5,13.5h-13v-9h13V13.5z"/><rect height="1" width="7" x="5" y="7"/><polygon points="14,8 15,8 15,7 14,7 14,6 13,6 13,9 14,9"/><rect height="1" width="7" x="8" y="10"/><polygon points="6,12 7,12 7,9 6,9 6,10 5,10 5,11 6,11"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 568 B

View File

@@ -1,97 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="99.058548mm"
height="89.967583mm"
viewBox="0 0 99.058554 89.967582"
version="1.1"
id="svg1040"
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
sodipodi:docname="index_api.svg">
<defs
id="defs1034" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="533.74914"
inkscape:cy="10.90433"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="930"
inkscape:window-height="472"
inkscape:window-x="2349"
inkscape:window-y="267"
inkscape:window-maximized="0" />
<metadata
id="metadata1037">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(195.19933,-1.0492759)">
<g
id="g1008"
transform="matrix(1.094977,0,0,1.094977,-521.5523,-198.34055)">
<path
inkscape:connector-curvature="0"
id="path899"
d="M 324.96812,187.09499 H 303.0455 v 72.1639 h 22.67969"
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path899-3"
d="m 361.58921,187.09499 h 21.92262 v 72.1639 h -22.67969"
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<g
transform="translate(415.87139,46.162126)"
id="g944">
<circle
style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path918"
cx="-84.40152"
cy="189.84375"
r="2.2293637" />
<circle
style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path918-5"
cx="-72.949402"
cy="189.84375"
r="2.2293637" />
<circle
style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path918-6"
cx="-61.497284"
cy="189.84375"
r="2.2293637" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="89.624855mm"
height="89.96759mm"
viewBox="0 0 89.62486 89.96759"
version="1.1"
id="svg1040"
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
sodipodi:docname="index_contribute.svg">
<defs
id="defs1034" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="683.11893"
inkscape:cy="-59.078181"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="930"
inkscape:window-height="472"
inkscape:window-x="2349"
inkscape:window-y="267"
inkscape:window-maximized="0" />
<metadata
id="metadata1037">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(234.72009,17.466935)">
<g
id="g875"
transform="matrix(0.99300176,0,0,0.99300176,-133.24106,-172.58804)">
<path
sodipodi:nodetypes="ccc"
inkscape:connector-curvature="0"
id="path869"
d="m -97.139881,161.26069 47.247024,40.25446 -47.247024,40.25446"
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path871"
d="m -49.514879,241.81547 h 32.505951"
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="101.09389mm"
height="89.96759mm"
viewBox="0 0 101.09389 89.96759"
version="1.1"
id="svg1040"
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
sodipodi:docname="index_getting_started.svg">
<defs
id="defs1034" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="-93.242129"
inkscape:cy="-189.9825"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1875"
inkscape:window-height="1056"
inkscape:window-x="1965"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata1037">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(2.9219487,-8.5995374)">
<path
style="fill:#5a5a5a;fill-opacity:1;stroke-width:0.20233451"
d="M 37.270955,98.335591 C 33.358064,97.07991 31.237736,92.52319 32.964256,89.08022 c 0.18139,-0.361738 4.757999,-5.096629 10.17021,-10.521968 l 9.84041,-9.864254 -4.03738,-4.041175 -4.037391,-4.041172 -4.96415,4.916665 c -3.61569,3.581096 -5.238959,5.04997 -5.975818,5.407377 l -1.011682,0.490718 H 17.267525 1.5866055 L 0.65034544,70.96512 C -2.2506745,69.535833 -3.5952145,66.18561 -2.5925745,62.884631 c 0.53525,-1.762217 1.61699004,-3.050074 3.22528014,-3.839847 l 1.15623996,-0.56778 13.2591094,-0.05613 13.259111,-0.05613 11.5262,-11.527539 11.526199,-11.527528 H 40.622647 c -12.145542,0 -12.189222,-0.0046 -13.752801,-1.445851 -2.229871,-2.055423 -2.162799,-5.970551 0.135998,-7.938238 1.475193,-1.262712 1.111351,-1.238469 18.588522,-1.238469 12.899229,0 16.035311,0.05193 16.692589,0.276494 0.641832,0.219264 2.590731,2.051402 9.416301,8.852134 l 8.606941,8.575638 h 6.848168 c 4.837422,0 7.092281,0.07311 7.679571,0.249094 0.48064,0.144008 1.22985,0.634863 1.77578,1.163429 2.383085,2.307333 1.968685,6.539886 -0.804989,8.221882 -0.571871,0.346781 -1.38284,0.687226 -1.80217,0.756523 -0.41933,0.06928 -4.2741,0.127016 -8.56615,0.128238 -6.56998,0.0016 -7.977492,-0.04901 -8.902732,-0.321921 -0.975569,-0.287742 -1.400468,-0.622236 -3.783999,-2.978832 l -2.685021,-2.654679 -5.05411,5.051071 -5.0541,5.051081 3.926292,3.947202 c 2.365399,2.378001 4.114289,4.309171 4.399158,4.857713 0.39266,0.75606 0.47311,1.219412 0.474321,2.731516 0.003,3.083647 0.620779,2.331942 -13.598011,16.531349 -10.273768,10.259761 -12.679778,12.563171 -13.500979,12.92519 -1.267042,0.55857 -3.156169,0.681342 -4.390271,0.285321 z m 40.130741,-65.45839 c -2.212909,-0.579748 -3.782711,-1.498393 -5.51275,-3.226063 -2.522111,-2.518633 -3.633121,-5.181304 -3.633121,-8.707194 0,-3.530699 1.11238,-6.197124 3.631161,-8.704043 4.866751,-4.8438383 12.324781,-4.8550953 17.211791,-0.026 3.908758,3.862461 4.818578,9.377999 2.372188,14.380771 -0.846209,1.730481 -3.39493,4.326384 -5.143839,5.239072 -2.69708,1.407492 -6.042829,1.798628 -8.92543,1.043434 z"
id="path1000"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="123.72241mm"
height="89.96759mm"
viewBox="0 0 123.72242 89.96759"
version="1.1"
id="svg1040"
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
sodipodi:docname="index_userguide.svg">
<defs
id="defs1034" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="332.26618"
inkscape:cy="83.744004"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="930"
inkscape:window-height="472"
inkscape:window-x="2349"
inkscape:window-y="267"
inkscape:window-maximized="0" />
<metadata
id="metadata1037">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(141.8903,-20.32143)">
<path
style="fill:#5a5a5a;fill-opacity:1;stroke-width:0.20483544"
d="m -139.53374,110.1657 c -0.80428,-0.24884 -1.71513,-1.11296 -2.07107,-1.96486 -0.23905,-0.57214 -0.28453,-6.28104 -0.28453,-35.720988 0,-38.274546 -0.079,-35.840728 1.19849,-36.91568 0.58869,-0.495345 4.63766,-2.187548 8.47998,-3.544073 l 1.58749,-0.560453 v -3.309822 c 0,-3.025538 0.0396,-3.388179 0.46086,-4.222122 0.68808,-1.362003 1.38671,-1.714455 4.60319,-2.322195 4.12797,-0.779966 5.13304,-0.912766 8.81544,-1.16476 11.80964,-0.808168 22.80911,2.509277 30.965439,9.3392 1.750401,1.465747 3.840861,3.5635 5.0903,5.108065 l 0.659122,0.814805 0.659109,-0.814805 c 1.249431,-1.544565 3.33988,-3.642318 5.09029,-5.108065 8.156331,-6.829923 19.155791,-10.147368 30.965441,-9.3392 3.682389,0.251994 4.68748,0.384794 8.81544,1.16476 3.21647,0.60774 3.91511,0.960192 4.60318,2.322195 0.4213,0.833943 0.46087,1.196584 0.46087,4.222122 v 3.309822 l 1.58748,0.560453 c 4.10165,1.448077 7.98852,3.072753 8.5259,3.563743 1.22643,1.120567 1.15258,-1.245868 1.15258,36.927177 0,34.567591 -0.005,35.083151 -0.40663,35.903991 -0.22365,0.45804 -0.73729,1.05665 -1.14143,1.33024 -1.22281,0.82783 -2.17721,0.70485 -5.86813,-0.7561 -9.19595,-3.63998 -18.956011,-6.38443 -26.791332,-7.53353 -3.02827,-0.44412 -9.26189,-0.61543 -11.77821,-0.3237 -5.19357,0.60212 -8.736108,2.05527 -11.700039,4.79936 -0.684501,0.63371 -1.466141,1.23646 -1.736979,1.33942 -0.63859,0.2428 -4.236521,0.2428 -4.875112,0 -0.27083,-0.10296 -1.05247,-0.70571 -1.73696,-1.33942 -2.96395,-2.74409 -6.50648,-4.19724 -11.700058,-4.79936 -2.516312,-0.29173 -8.749941,-0.12042 -11.778201,0.3237 -7.78194,1.14127 -17.39965,3.83907 -26.73341,7.49883 -3.38325,1.32658 -4.15525,1.50926 -5.11851,1.21125 z m 4.2107,-5.34052 c 5.86759,-2.29858 14.40398,-4.922695 20.2018,-6.210065 6.31584,-1.402418 8.5236,-1.646248 14.91592,-1.647338 4.68699,-7.94e-4 6.013661,0.0632 7.257809,0.3497 0.837332,0.19286 1.561052,0.312028 1.60828,0.264819 0.147111,-0.147119 -1.803289,-1.307431 -4.154879,-2.471801 -8.12511,-4.023029 -18.27311,-4.986568 -29.0861,-2.761718 -1.09536,0.22538 -2.32708,0.40827 -2.73715,0.406418 -1.12787,-0.005 -2.3054,-0.76382 -2.84516,-1.8332 l -0.46086,-0.913098 V 62.99179 35.97471 l -0.56331,0.138329 c -0.30981,0.07608 -1.89985,0.665075 -3.5334,1.308881 -2.27551,0.896801 -2.96414,1.252878 -2.94452,1.522563 0.014,0.193604 0.0372,15.284513 0.0512,33.535345 0.014,18.250839 0.0538,33.183322 0.0884,33.183322 0.0346,0 1.02543,-0.3771 2.20198,-0.83801 z m 113.006991,-32.697216 -0.0518,-33.535203 -3.17495,-1.272156 c -1.74623,-0.699685 -3.33627,-1.278755 -3.53341,-1.286819 -0.33966,-0.01389 -0.35847,1.401778 -0.35847,26.980216 v 26.994863 l -0.46087,0.913112 c -0.53976,1.06939 -1.71729,1.828088 -2.84515,1.833189 -0.41008,0.0021 -1.6418,-0.181031 -2.73716,-0.406421 -11.888201,-2.446089 -22.84337,-1.046438 -31.491022,4.02332 -1.68175,0.985941 -2.216748,1.467501 -1.36534,1.228942 1.575181,-0.441362 4.990592,-0.73864 8.524862,-0.742011 5.954408,-0.005 11.43046,0.791951 19.10874,2.78333 3.9516,1.024874 12.1555,3.687454 15.6699,5.085704 1.23926,0.49306 2.36869,0.90517 2.50985,0.9158 0.20489,0.0155 0.2462,-6.745894 0.20483,-33.515866 z m -59.76135,-2.233777 V 40.065438 l -0.95972,-1.357442 c -1.380522,-1.952627 -5.376262,-5.847994 -7.64336,-7.45136 -3.778692,-2.672401 -9.063392,-4.943324 -13.672511,-5.875304 -3.19731,-0.646503 -5.23069,-0.833103 -9.05886,-0.831312 -4.37716,0.0021 -7.70223,0.349169 -11.83461,1.235469 l -1.07538,0.230645 v 31.242342 c 0,26.565778 0.0426,31.226011 0.28429,31.133261 0.15637,-0.06 1.42379,-0.297169 2.81648,-0.527026 12.37657,-2.042634 23.21658,-0.346861 32.521639,5.087596 2.10018,1.226558 5.20202,3.618878 6.880942,5.30692 0.788609,0.792909 1.502978,1.446609 1.587468,1.452679 0.0845,0.006 0.153622,-13.411893 0.153622,-29.817719 z m 5.80221,28.3766 c 6.21476,-6.141601 15.08488,-10.061509 25.025529,-11.05933 4.262419,-0.427849 11.579921,-0.0054 16.017661,0.924912 0.75932,0.15916 1.45259,0.244888 1.54058,0.190498 0.088,-0.05434 0.16003,-14.060382 0.16003,-31.124436 V 26.176883 l -0.52136,-0.198219 c -0.66893,-0.254325 -4.77649,-0.95482 -7.159981,-1.221048 -2.41372,-0.269605 -8.559851,-0.266589 -10.759229,0.0052 -6.458111,0.798299 -12.584091,3.083792 -17.405651,6.49374 -2.267091,1.603366 -6.262831,5.498733 -7.64336,7.45136 l -0.959721,1.357438 v 29.828747 c 0,16.405812 0.0532,29.828746 0.11802,29.828746 0.065,0 0.77928,-0.65347 1.587482,-1.452149 z"
id="path845"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csscccscsssscsssssscscsccsccsccscsscccccccscccccccccsccscscscccscccsccssccsscccscccccsccccsccscsccsscc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="48px" viewBox="0 0 20 20" width="48px" fill="#000000"><g><rect fill="none" height="20" width="20" x="0"/></g><g><g><path d="M14.45,10.86l0.26,2.1c0.06,0.46-0.1,0.92-0.43,1.25l-3.55,3.55l-1.41-4.24l-2.83-2.83L2.25,9.27l3.55-3.55 c0.33-0.33,0.79-0.49,1.25-0.43l2.1,0.26C13.92,0.78,17.8,2.2,17.8,2.2C17.8,2.2,19.22,6.08,14.45,10.86z M10.2,6.62 C9.18,7.64,8.37,9.06,7.9,9.97l2.12,2.12c0.91-0.46,2.34-1.27,3.36-2.3C16.18,7,16.54,4.67,16.5,3.5C15.33,3.46,13,3.82,10.2,6.62 z M14,7.5C14,6.68,13.32,6,12.5,6C11.67,6,11,6.68,11,7.5S11.67,9,12.5,9C13.32,9,14,8.33,14,7.5z M7.99,6.92L6.86,6.78L5.02,8.61 l1.62,0.54C6.97,8.52,7.43,7.71,7.99,6.92z M10.85,13.36l0.54,1.62l1.83-1.83l-0.14-1.14C12.29,12.57,11.48,13.03,10.85,13.36z M5.25,12.5c-0.62,0-1.18,0.25-1.59,0.66C2.44,14.38,2,18,2,18s3.62-0.44,4.84-1.66c0.41-0.41,0.66-0.97,0.66-1.59 C7.5,13.51,6.49,12.5,5.25,12.5z M5.78,15.28c-0.44,0.44-1.87,0.81-1.87,0.81s0.37-1.43,0.81-1.87c0.29-0.29,0.77-0.29,1.06,0 S6.07,14.99,5.78,15.28z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -64,7 +64,6 @@ add_module_names = False # Remove namespaces from class/method signatures
autodoc_inherit_docstrings = True # If no docstring, inherit from base class
set_type_checking_flag = True # Enable 'expensive' imports for sphinx_autodoc_typehints
autoclass_content = "both" # Include both class docstring and __init__
autodoc_mock_imports = ["pyqtgraph"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@@ -77,5 +76,6 @@ language = "Python"
html_theme = "pydata_sphinx_theme"
html_static_path = ["_static"]
html_css_files = ["custom.css"]
html_logo = "../bec_widgets/assets/bec_widgets_icon.png"
html_css_files = ["css/custom.css"]
html_logo = "_static/bec.png"
html_theme_options = {"show_nav_level": 1, "navbar_align": "content"}

View File

@@ -1,6 +1,14 @@
(developer)=
# Development
```{toctree}
---
maxdepth: 1
hidden: true
---
reference/
```
To contribute to the development of BEC Widgets, start by setting up the development environment:
1. **Clone the Repository**:

View File

@@ -1,5 +1,4 @@
(api_reference)=
# API Reference
## API Reference
```{eval-rst}
.. autosummary::
@@ -8,5 +7,4 @@
:recursive:
bec_widgets
```

View File

@@ -1,70 +1,39 @@
# BEC Widgets documentation
A flexible and extensible framework for building graphical user interfaces in Python, optimized for use in the BEC environment.
<br><br>
<br><br>
````{grid} 2
````{grid} 3
:gutter: 5
```{grid-item-card}
```{grid-item-card} Introduction
:link: introduction
:link-type: ref
:img-top: /assets/index_getting_started.svg
:text-align: center
## Introduction
General information about BEC Widgets.
General information.
```
```{grid-item-card}
```{grid-item-card} User
:link: user
:link-type: ref
:img-top: /assets/index_user_guide.svg
:text-align: center
## User guide
Information for users of BEC Widgets.
Information for users.
```
```{grid-item-card}
```{grid-item-card} Developer
:link: developer
:link-type: ref
:img-top: /assets/index_contribute.svg
:text-align: center
## Developer guide
Information for developers of BEC Widgets.
Information for developers.
```
```{grid-item-card}
:link: api_reference
:link-type: ref
:img-top: /assets/index_api.svg
:text-align: center
## API reference
Comprehensive reference of all BEC Widget classes, functions, and methods.
```
````
```{toctree}
---
numbered: true
maxdepth: 2
hidden: true
maxdepth: 1
---
introduction/introduction
user/user
developer/developer
api_reference/api_reference
```

View File

@@ -1,11 +0,0 @@
(user.api_reference)=
# User API Reference
```{eval-rst}
.. autosummary::
:toctree: _autosummary
:template: custom-module-template.rst
bec_widgets.cli.client
```

View File

@@ -1,14 +0,0 @@
(user.applications)=
# Applications
**Coming soon**
```{toctree}
---
maxdepth: 1
hidden: true
---
apps/motor_app
apps/plot_app
apps/modular_app

39
docs/user/apps.md Normal file
View File

@@ -0,0 +1,39 @@
(user.apps)=
# Applications
In the `bec_widgets/examples` directory, you will find practical applications that demonstrate the capabilities of BEC Widgets in real-world scenarios. These applications showcase the adaptability and functionality of the framework for various beamline experiment needs.
**Motor Alignment Tool**
This tool assists in aligning motors with samples during experiments. It enables users to move motors, visually track their movement, and record positions for precise alignment.
- **Location:** `bec_widgets/examples/motor_movement`
- **Further Details:** [Motor Alignment Tool Documentation](#user.apps.motor_app)
**General Plotting Live Acquisition Tool**
This application is designed for live data visualization. It allows users to view real-time signals from detectors in a multi-grid layout, facilitating immediate analysis during experiments.
- **Location:** `bec_widgets/examples/plot_app`
- **Further Details:** [General Plotting Live Acquisition Tool Documentation](#user.apps.plot_app)
**Modular Application**
A bespoke application built entirely using BEC Widgets' modular components. This example illustrates the framework's flexibility in creating customized GUIs tailored to specific experimental setups.
- **Location:** `bec_widgets/examples/modular_app`
- **Further Details:** [Modular Application](#user.apps.modular_app)
---
Note: The documentation for these applications is currently under development. The provided links will direct you to their respective pages once the documentation is complete.
```{toctree}
---
maxdepth: 1
hidden: true
---
apps/motor_app
apps/plot_app
apps/modular_app

View File

@@ -0,0 +1,6 @@
(user.apps.modular_app)=
# Modular Application
_to be added..._

View File

@@ -0,0 +1,34 @@
(user.apps.motor_app)=
# Motor Alignment
The Motor Alignment Application is a key component of the BEC Widgets suite, designed to facilitate precise alignment of motors.
Users can easily launch this app using the script located at `/bec_widgets/example/motor_movement/motor_example.py` script.
The application's primary function is to enable users to align motors to specific positions and to visually track the motor's trajectory.
## Controlling Motors
In the top middle panel of the application, users will find combobox dropdown menus for selecting the motors they wish to track on the x and y axes of the motor map.
These motors are automatically loaded from the current active BEC instance, ensuring seamless integration and ease of use.
There are two primary methods to control motor movements:
1. **Manual Control with Arrow Keys:** Users can manually drive the motors using arrow keys. Before doing so, they need to select the step size for each motor, allowing for precise and incremental movements.
2. **Direct Position Entry:** Alternatively, users can input a desired position in the text input box and then click the Go button. This action will move the motor directly to the specified coordinates.
As the motors are moved, their trajectory is plotted in real-time, providing users with a visual representation of the motor's path. This feature is particularly useful for understanding the movement patterns and making necessary adjustments.
## Saving and Exporting Data
Users have the ability to save the current motor position in a table widget. This functionality is beneficial for recalling and returning to specific positions. By clicking the Go button in the table widget, the motors will automatically move back to the saved position.
Additionally, users can annotate each saved position with notes and comments directly in the table widget. This feature is invaluable for keeping track of specific alignment settings or observations. The contents of the table, including the notes, can be exported to a .csv file. This exported data can be used for initiating scans or for record-keeping purposes.
The table widget also supports saving and loading functionalities, allowing users to preserve their motor positions and notes across sessions. The saved files are in a user-friendly format for ease of access and use.
## Example of Use
![Motor app example](motor_app_10fps.gif)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

@@ -0,0 +1,6 @@
(user.apps.plot_app)=
# General Plotting Tool
_to be added..._

View File

@@ -1,12 +0,0 @@
(user.getting_started)=
# Getting Started
This section provides a comprehensive guide to getting started with BEC Widgets. Whether you are new to BEC Widgets or looking to refresh your knowledge, this guide will help you set up and navigate the framework with ease.
```{toctree}
---
maxdepth: 2
hidden: true
---
getting_started/installation
```

View File

@@ -14,7 +14,7 @@ Before installing BEC Widgets, please ensure the following requirements are met:
Install BEC Widgets using the pip package manager. Open your terminal and execute:
```bash
pip install bec_widgets PyQt6
pip install bec_widgets
```
This command installs BEC Widgets along with its dependencies, including the default PyQt6.

View File

@@ -1,67 +1,38 @@
(user)=
# User
**Overview**
Welcome to the User section of the BEC Widgets documentation! BEC Widgets is a versatile GUI framework tailored for beamline scientists, enabling efficient and intuitive interaction with beamline experiments. This section is designed to guide both new and experienced users through the essential aspects of utilizing BEC Widgets.
**Key Topics**
- [Installing BEC Widgets](#user.installation): Instructions for installing BEC Widgets on your system.
- [Example Applications](#user.apps): Overview of bespoke applications and demonstrations of BEC Widgets in action, showcasing its use in real-world beamline scenarios.
- [Widgets Overview](#user.widgets): Detailed information on the variety of widgets available, their functions, and how to use them effectively.
- [Customization and Configuration](#user.customisation): Tips on customizing and configuring BEC Widgets to suit your specific experimental needs using Qt Designer.
**Bug Reports and Feature Requests**
We value your feedback and contributions to improving BEC Widgets. If you encounter any issues or have ideas for new features, we encourage you to report them.
- **Bug Reports:** If you find a bug or an issue, please report it on our repository's [Issues page](https://gitlab.psi.ch/bec/bec-widgets/-/issues?sort=created_date&state=opened). We have a template for bug reporting to help you provide all necessary information.
- **Feature Requests:** Have an idea for a new feature or an enhancement? Share it with us on the [Issues page](https://gitlab.psi.ch/bec/bec-widgets/-/issues?sort=created_date&state=opened) of our repository. We have a feature request template that you can use to describe your proposal.
**Development**
For advanced details about BEC Widgets internal architecture, development contributions, or customization techniques, please explore the [Developer](#developer) section.
```{toctree}
---
maxdepth: 2
maxdepth: 3
hidden: true
---
getting_started/getting_started.md
applications/applications.md
widgets/widgets.md
api_reference/api_reference.md
```
***
````{grid} 2
:gutter: 5
```{grid-item-card}
:link: user.getting_started
:link-type: ref
:img-top: /assets/rocket_launch_48dp.svg
:text-align: center
## Getting Started
Learn how to install BEC Widgets and get started with the framework.
```
```{grid-item-card}
:link: user.applications
:link-type: ref
:img-top: /assets/display_settings_48dp.svg
:text-align: center
## Applications
Learn how to use BEC Widgets Applications, to modify and create new applications.
```
```{grid-item-card}
:link: user.widgets
:link-type: ref
:img-top: /assets/apps_48dp.svg
:text-align: center
## Widgets
Learn about the building blocks of larger applications: widgets.
```
```{grid-item-card}
:link: user.api_reference
:link-type: ref
:img-top: /assets/index_api.svg
:text-align: center
## API reference
Comprehensive reference of all user-facing classes, functions, and methods.
```
````
installation
apps
widgets
customisation

View File

@@ -15,7 +15,7 @@ BEC Widgets includes a variety of visualization widgets designed to cater to div
- Customizable visual elements such as line color and style.
**Example of Use:**
![Waveform 1D](./w1D.gif)
![Waveform 1D](./widgets/w1D.gif)
### 2D Scatter Plot
**Purpose:** The 2D scatter plot widget is designed for more complex data visualization. It employs a false color map to represent a third dimension (z-axis), making it an ideal tool for visualizing multidimensional data sets.
@@ -27,7 +27,7 @@ BEC Widgets includes a variety of visualization widgets designed to cater to div
- Tools for selecting and inspecting specific data points.
**Example of Use:**
![Waveform 1D](./scatter_2D.gif)
![Waveform 1D](./widgets/scatter_2D.gif)
### Motor Position Map
**Purpose:** A specialized component derived from the Motor Alignment Tool. It's focused on tracking and visualizing the position of motors, crucial for precise alignment and movement tracking during scans.
@@ -38,4 +38,4 @@ BEC Widgets includes a variety of visualization widgets designed to cater to div
- Ability to record and recall specific motor positions for repetitive tasks.
**Example of Use:**
![Waveform 1D](./motor.gif)
![Waveform 1D](./widgets/motor.gif)

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "0.56.2"
version = "0.55.0"
description = "BEC Widgets"
requires-python = ">=3.10"
classifiers = [

View File

@@ -150,7 +150,7 @@ def test_spiral_bar(rpc_server_dock):
dock = BECDockArea(rpc_server_dock.gui_id)
dock_server = rpc_server_dock.gui
d0 = dock.add_dock(name="dock_0")
d0 = dock.add_dock("dock_0")
bar = d0.add_widget_bec("SpiralProgressBar")
assert bar.__class__.__name__ == "SpiralProgressBar"
@@ -163,7 +163,7 @@ def test_spiral_bar(rpc_server_dock):
expected_colors = Colors.golden_angle_color("viridis", 5, "RGB")
bar_colors = [ring.color.getRgb() for ring in bar_server.rings]
bar_values = [ring.config.value for ring in bar_server.rings]
bar_values = [ring.value for ring in bar_server.rings]
assert bar_values == [10, 20, 30, 40, 50]
assert bar_colors == expected_colors
@@ -188,7 +188,7 @@ def test_spiral_bar_scan_update(rpc_server_dock, qtbot):
qtbot.wait(200)
bar_server = dock_server.docks["dock_0"].widgets[0]
assert bar_server.config.num_bars == 1
np.testing.assert_allclose(bar_server.rings[0].config.value, 10, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].value, 10, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.min_value, 0, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.max_value, 10, atol=0.1)
@@ -199,7 +199,7 @@ def test_spiral_bar_scan_update(rpc_server_dock, qtbot):
qtbot.wait(200)
assert bar_server.config.num_bars == 1
np.testing.assert_allclose(bar_server.rings[0].config.value, 16, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].value, 16, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.min_value, 0, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.max_value, 16, atol=0.1)
@@ -218,8 +218,8 @@ def test_spiral_bar_scan_update(rpc_server_dock, qtbot):
qtbot.wait(200)
assert bar_server.config.num_bars == 2
np.testing.assert_allclose(bar_server.rings[0].config.value, final_samx, atol=0.1)
np.testing.assert_allclose(bar_server.rings[1].config.value, final_samy, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].value, final_samx, atol=0.1)
np.testing.assert_allclose(bar_server.rings[1].value, final_samy, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.min_value, init_samx, atol=0.1)
np.testing.assert_allclose(bar_server.rings[1].config.min_value, init_samy, atol=0.1)
np.testing.assert_allclose(bar_server.rings[0].config.max_value, final_samx, atol=0.1)

View File

@@ -17,8 +17,8 @@ def cli_figure():
def test_rpc_call_plot(cli_figure):
fig, mock_rpc_call = cli_figure
fig.plot(x_name="samx", y_name="bpm4i")
mock_rpc_call.assert_called_with("plot", x_name="samx", y_name="bpm4i")
fig.plot("samx", "bpm4i")
mock_rpc_call.assert_called_with("plot", "samx", "bpm4i")
def test_rpc_call_accepts_device_as_input(cli_figure):

View File

@@ -44,8 +44,8 @@ def test_mouse_moved_signals(qtbot):
# Create a slot that will store the emitted values as tuples
emitted_values_1D = []
def slot(coordinates):
emitted_values_1D.append(coordinates)
def slot(x, y_values):
emitted_values_1D.append((x, y_values))
# Connect the signal to the custom slot
crosshair.coordinatesChanged1D.connect(slot)
@@ -59,7 +59,7 @@ def test_mouse_moved_signals(qtbot):
crosshair.mouse_moved(event_mock)
# Assert the expected behavior
assert emitted_values_1D == [(2, [5])]
assert emitted_values_1D == [(2.0, [5.0])]
def test_mouse_moved_signals_outside(qtbot):
@@ -106,8 +106,8 @@ def test_mouse_moved_signals_2D(qtbot):
# Create a slot that will store the emitted values as tuples
emitted_values_2D = []
def slot(coordinates):
emitted_values_2D.append(coordinates)
def slot(x, y):
emitted_values_2D.append((x, y))
# Connect the signal to the custom slot
crosshair.coordinatesChanged2D.connect(slot)

View File

@@ -0,0 +1,115 @@
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
import json
from unittest.mock import MagicMock, patch
import numpy as np
import pyqtgraph as pg
import pytest
import zmq
from bec_widgets.examples.eiger_plot.eiger_plot import EigerPlot
# Common fixture for all tests
@pytest.fixture
def eiger_plot_instance(qtbot):
widget = EigerPlot()
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
@pytest.mark.parametrize(
"fft_checked, rotation_index, transpose_checked, log_checked, expected_image",
[
(False, 0, False, False, np.array([[2, 1], [1, 5]], dtype=float)), # just mask
(False, 1, False, False, np.array([[1, 5], [2, 1]], dtype=float)), # 90 deg rotation
(False, 2, False, False, np.array([[5, 1], [1, 2]], dtype=float)), # 180 deg rotation
(False, 0, True, False, np.array([[2, 1], [1, 5]], dtype=float)), # transposed
(False, 0, False, True, np.array([[0.30103, 0.0], [0.0, 0.69897]], dtype=float)), # log
(True, 0, False, False, np.array([[5.0, 3.0], [3.0, 9.0]], dtype=float)), # FFT
],
)
def test_on_image_update(
qtbot,
eiger_plot_instance,
fft_checked,
rotation_index,
transpose_checked,
log_checked,
expected_image,
):
# Initialize image and mask
eiger_plot_instance.image = np.array([[1, 2], [3, 4]], dtype=float)
eiger_plot_instance.mask = np.array([[0, 1], [1, 0]], dtype=float)
# Mock UI elements
eiger_plot_instance.checkBox_FFT = MagicMock()
eiger_plot_instance.checkBox_FFT.isChecked.return_value = fft_checked
eiger_plot_instance.comboBox_rotation = MagicMock()
eiger_plot_instance.comboBox_rotation.currentIndex.return_value = rotation_index
eiger_plot_instance.checkBox_transpose = MagicMock()
eiger_plot_instance.checkBox_transpose.isChecked.return_value = transpose_checked
eiger_plot_instance.checkBox_log = MagicMock()
eiger_plot_instance.checkBox_log.isChecked.return_value = log_checked
eiger_plot_instance.imageItem = MagicMock()
# Call the method
eiger_plot_instance.on_image_update()
# Validate the transformations
np.testing.assert_array_almost_equal(eiger_plot_instance.image, expected_image, decimal=5)
# Validate that setImage was called
eiger_plot_instance.imageItem.setImage.assert_called_with(
eiger_plot_instance.image, autoLevels=False
)
def test_init_ui(eiger_plot_instance):
assert isinstance(eiger_plot_instance.plot_item, pg.PlotItem)
assert isinstance(eiger_plot_instance.imageItem, pg.ImageItem)
assert isinstance(eiger_plot_instance.hist, pg.HistogramLUTItem)
def test_start_zmq_consumer(eiger_plot_instance):
with patch("threading.Thread") as MockThread:
eiger_plot_instance.start_zmq_consumer()
MockThread.assert_called_once()
MockThread.return_value.start.assert_called_once()
def test_zmq_consumer(eiger_plot_instance, qtbot):
fake_meta = json.dumps({"type": "int32", "shape": (2, 2)}).encode("utf-8")
fake_data = np.array([[1, 2], [3, 4]], dtype="int32").tobytes()
with patch("zmq.Context", autospec=True) as MockContext:
mock_socket = MagicMock()
mock_socket.recv_multipart.side_effect = ((fake_meta, fake_data),)
MockContext.return_value.socket.return_value = mock_socket
# Mocking the update_signal to check if it gets emitted
eiger_plot_instance.update_signal = MagicMock()
with patch("zmq.Poller"):
# will do only 1 iteration of the loop in the thread
eiger_plot_instance._zmq_consumer_exit_event.set()
# Run the method under test
consumer_thread = eiger_plot_instance.start_zmq_consumer()
consumer_thread.join()
# Check if zmq methods are called
# MockContext.assert_called_once()
assert MockContext.call_count == 1
mock_socket.connect.assert_called_with("tcp://129.129.95.38:20000")
mock_socket.setsockopt_string.assert_called_with(zmq.SUBSCRIBE, "")
mock_socket.recv_multipart.assert_called()
# Check if update_signal was emitted
eiger_plot_instance.update_signal.emit.assert_called_once()
# Validate the image data
np.testing.assert_array_equal(
eiger_plot_instance.image, np.array([[1, 2], [3, 4]], dtype="int32")
)

View File

@@ -239,14 +239,14 @@ def test_absolute_save_current_coordinates(motor_absolute_widget):
motor_absolute_widget.coordinates_signal.connect(capture_emit)
# Trigger saving current coordinates
motor_absolute_widget.ui.pushButton_save.click()
motor_absolute_widget.pushButton_save.click()
assert emitted_coordinates == [(motor_x_value, motor_y_value)]
def test_absolute_set_absolute_coordinates(motor_absolute_widget):
motor_absolute_widget.ui.spinBox_absolute_x.setValue(5)
motor_absolute_widget.ui.spinBox_absolute_y.setValue(10)
motor_absolute_widget.spinBox_absolute_x.setValue(5)
motor_absolute_widget.spinBox_absolute_y.setValue(10)
# Connect to the coordinates_signal to capture emitted values
emitted_values = []
@@ -257,7 +257,7 @@ def test_absolute_set_absolute_coordinates(motor_absolute_widget):
motor_absolute_widget.coordinates_signal.connect(capture_coordinates)
# Simulate button click for absolute movement
motor_absolute_widget.ui.pushButton_set.click()
motor_absolute_widget.pushButton_set.click()
assert emitted_values == [(5, 10)]
@@ -265,14 +265,14 @@ def test_absolute_set_absolute_coordinates(motor_absolute_widget):
def test_absolute_go_absolute_coordinates(motor_absolute_widget):
motor_absolute_widget.change_motors("samx", "samy")
motor_absolute_widget.ui.spinBox_absolute_x.setValue(5)
motor_absolute_widget.ui.spinBox_absolute_y.setValue(10)
motor_absolute_widget.spinBox_absolute_x.setValue(5)
motor_absolute_widget.spinBox_absolute_y.setValue(10)
with patch(
"bec_widgets.widgets.motor_control.motor_control.MotorThread.move_absolute",
new_callable=MagicMock,
) as mock_move_absolute:
motor_absolute_widget.ui.pushButton_go_absolute.click()
motor_absolute_widget.pushButton_go_absolute.click()
mock_move_absolute.assert_called_once_with("samx", "samy", (5, 10))
@@ -292,8 +292,8 @@ def test_set_precision(motor_absolute_widget):
motor_absolute_widget.on_config_update(CONFIG_DEFAULT)
motor_absolute_widget.set_precision(2)
assert motor_absolute_widget.ui.spinBox_absolute_x.decimals() == 2
assert motor_absolute_widget.ui.spinBox_absolute_y.decimals() == 2
assert motor_absolute_widget.spinBox_absolute_x.decimals() == 2
assert motor_absolute_widget.spinBox_absolute_y.decimals() == 2
#######################################################
@@ -338,29 +338,29 @@ def test_initialization_and_config_update(motor_relative_widget):
def test_move_motor_relative(motor_relative_widget):
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
# Set step sizes
motor_relative_widget.ui.spinBox_step_x.setValue(1)
motor_relative_widget.ui.spinBox_step_y.setValue(1)
motor_relative_widget.spinBox_step_x.setValue(1)
motor_relative_widget.spinBox_step_y.setValue(1)
# Mock the move_relative method
motor_relative_widget.motor_thread.move_relative = MagicMock()
# Simulate button clicks
motor_relative_widget.ui.toolButton_right.click()
motor_relative_widget.toolButton_right.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_x, 1
)
motor_relative_widget.ui.toolButton_left.click()
motor_relative_widget.toolButton_left.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_x, -1
)
motor_relative_widget.ui.toolButton_up.click()
motor_relative_widget.toolButton_up.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_y, 1
)
motor_relative_widget.ui.toolButton_down.click()
motor_relative_widget.toolButton_down.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_y, -1
)
@@ -376,21 +376,21 @@ def test_precision_update(motor_relative_widget):
motor_relative_widget.precision_signal.connect(capture_precision)
# Update precision
motor_relative_widget.ui.spinBox_precision.setValue(1)
motor_relative_widget.spinBox_precision.setValue(1)
assert emitted_values == [1]
assert motor_relative_widget.ui.spinBox_step_x.decimals() == 1
assert motor_relative_widget.ui.spinBox_step_y.decimals() == 1
assert motor_relative_widget.spinBox_step_x.decimals() == 1
assert motor_relative_widget.spinBox_step_y.decimals() == 1
def test_sync_step_sizes(motor_relative_widget):
motor_relative_widget.on_config_update(CONFIG_DEFAULT)
motor_relative_widget.ui.checkBox_same_xy.setChecked(True)
motor_relative_widget.checkBox_same_xy.setChecked(True)
# Change step size for X
motor_relative_widget.ui.spinBox_step_x.setValue(2)
motor_relative_widget.spinBox_step_x.setValue(2)
assert motor_relative_widget.ui.spinBox_step_y.value() == 2
assert motor_relative_widget.spinBox_step_y.value() == 2
def test_change_motor_relative(motor_relative_widget):

View File

@@ -145,12 +145,12 @@ def test_bar_set_value(spiral_progress_bar):
assert len(spiral_progress_bar.rings) == 5
spiral_progress_bar.set_value([10, 20, 30, 40, 50])
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [10, 20, 30, 40, 50]
# update just one bar
spiral_progress_bar.set_value(90, 1)
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [10, 90, 30, 40, 50]
@@ -166,7 +166,7 @@ def test_bar_set_precision(spiral_progress_bar):
assert ring_precision == [2, 2, 2]
spiral_progress_bar.set_value([10.1234, 20.1234, 30.1234])
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [10.12, 20.12, 30.12]
spiral_progress_bar.set_precision(4, 1)
@@ -174,7 +174,7 @@ def test_bar_set_precision(spiral_progress_bar):
assert ring_precision == [2, 4, 2]
spiral_progress_bar.set_value([10.1234, 20.1234, 30.1234])
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [10.12, 20.1234, 30.12]
@@ -189,7 +189,7 @@ def test_set_min_max_value(spiral_progress_bar):
assert ring_max_values == [10, 10]
spiral_progress_bar.set_value([5, 15])
ring_values = [ring.config.value for ring in spiral_progress_bar.rings]
ring_values = [ring.value for ring in spiral_progress_bar.rings]
assert ring_values == [5, 10]

View File

@@ -0,0 +1,158 @@
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
import threading
from unittest import mock
import numpy as np
import pytest
from bec_lib import messages
from bec_lib.redis_connector import RedisConnector
from pytestqt import qtbot
from bec_widgets.examples.stream_plot.stream_plot import StreamPlot
@pytest.fixture(scope="function")
def stream_app(qtbot):
"""Helper function to set up the StreamPlot widget."""
client = mock.MagicMock()
widget = StreamPlot(client=client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_roi_signals_emitted(qtbot, stream_app):
region = (0.1, 0.9)
with qtbot.waitSignal(stream_app.roi_signal, timeout=1000) as blocker:
stream_app.roi_signal.emit(region)
assert blocker.signal_triggered
assert blocker.args == [region]
def test_update_signals_emitted(qtbot, stream_app):
# Mimic data coming from the data stream
stream_app.plotter_data_x = [list(range(10))] # Replace with the actual x data
stream_app.plotter_data_y = [list(range(10))] # Replace with the actual y data
# Initialize curves
stream_app.init_curves()
with qtbot.waitSignal(stream_app.update_signal, timeout=1000) as blocker:
stream_app.update_signal.emit()
assert blocker.signal_triggered
def test_ui_initialization(qtbot, stream_app):
"""Checking the UI creation."""
# Check if UI elements are initialized correctly
assert stream_app.label_plot is not None
assert stream_app.label_plot_moved is not None
assert stream_app.label_plot_clicked is not None
assert stream_app.label_image_moved is not None
assert stream_app.label_image_clicked is not None
# Check if plots are initialized correctly
assert stream_app.plot is not None
assert stream_app.plot_image is not None
# Check if ROI selector is initialized correctly
assert stream_app.roi_selector is not None
def test_1d_plotting_data(qtbot, stream_app):
# Set up some mock data
x_data = [list(range(10))]
y_data = [list(range(10))]
# Manually set the data attributes
stream_app.plotter_data_x = x_data
stream_app.plotter_data_y = y_data
stream_app.y_value_list = ["Curve 1"]
# Initialize curves and update the plot
stream_app.init_curves()
stream_app.update() # This should update the plot with the new data
# Check the data on the plot
for idx, curve in enumerate(stream_app.curves):
np.testing.assert_array_equal(curve.xData, x_data[0]) # Access the first list of x_data
np.testing.assert_array_equal(
curve.yData, y_data[idx]
) # Access the list of y_data for each curve without additional indexing
def test_flip_even_rows(qtbot, stream_app):
# Create a numpy array with some known data
original_array = np.array(
[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15], [16, 17, 18, 19, 20]]
)
# Call flip_even_rows on the original array
flipped_array = stream_app.flip_even_rows(original_array)
# Expected array flipped along the rows with even indices
expected_array = np.array(
[[1, 2, 3, 4, 5], [10, 9, 8, 7, 6], [11, 12, 13, 14, 15], [20, 19, 18, 17, 16]]
)
# Check that flip_even_rows returned the expected result
np.testing.assert_array_equal(flipped_array, expected_array)
def test_on_dap_update(qtbot, stream_app):
"""2D image rendering by dap update"""
# Create some mock data to be "received" by the slot
data_dict = {"data": {"z": np.random.rand(10, 10)}}
metadata_dict = {}
# Trigger the slot
stream_app.on_dap_update(data_dict, metadata_dict)
# Apply the same transformation to the test data
expected_data = stream_app.flip_even_rows(data_dict["data"]["z"])
# Now check the state of the StreamPlot object
# For example, check the data of the image plot:
np.testing.assert_array_equal(stream_app.img.image, expected_data)
####################
# Until Here
####################
# def test_new_proj(qtbot, stream_app): #TODO this test is not working, does it make sense testing even?
# # Create some mock content to be "received" by the slot
# content_dict = {"signals": {"proj_nr": 1}}
# metadata_dict = {}
#
# # Manually create some mock data that new_proj would work with
# # This step may need to be adjusted to fit the actual behavior of new_proj
# mock_data = {
# "q": np.array([1, 2, 3, 4, 5]),
# "norm_sum": np.array([6, 7, 8, 9, 10]),
# "metadata": "some_metadata",
# }
#
# # Assume the RedisConnector client would return this data when new_proj is called
# mock_message = mock.MagicMock(spec=messages.DeviceMessage)
# mock_message.__getitem__.side_effect = lambda key: mock_data[key]
# stream_app.client.producer.get = mock.MagicMock(return_value=mock_message.dumps())
#
# # Trigger the slot
# stream_app.new_proj(content_dict, metadata_dict)
#
# # Now check the state of the StreamPlot object
# # For example, check that the plotter_data_x attribute was updated correctly:
# np.testing.assert_array_equal(stream_app.plotter_data_x, [mock_data["q"]])
# assert stream_app._current_proj == 1
# assert stream_app._current_q == mock_data["q"]
# assert stream_app._current_norm == mock_data["norm_sum"]
# assert stream_app._current_metadata == mock_data["metadata"]
# def test_connection_creation(qtbot, stream_app): #TODO maybe test connections in a different way?
# assert isinstance(stream_app.producer, RedisConnector)
# assert isinstance(stream_app.data_retriever, threading.Thread)
# assert stream_app.data_retriever.is_alive()