1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-19 23:05:36 +02:00

Compare commits

..

62 Commits

Author SHA1 Message Date
semantic-release
93f21eefd7 0.27.0
Automatically generated by python-semantic-release
2023-09-25 08:27:18 +00:00
wyzula-jan
44cc881ac9 fix: epics removed from requirements 2023-09-25 10:26:07 +02:00
wyzula-jan
ee3cae6472 Merge branch 'motor_go_end' 2023-09-25 10:21:52 +02:00
wyzula-jan
b78152b149 fix: motor_example.py load .csv logic fixed 2023-09-22 14:29:14 +02:00
wyzula-jan
85841cdf1f fix: motor_example.py export .csv logic fixed 2023-09-22 14:17:22 +02:00
wyzula-jan
ed3f656d5e refactor: motor_example.py removed old table related functions 2023-09-22 14:10:24 +02:00
wyzula-jan
a4fb6bd1d2 perf: motor_example.py replot logic optimizes 2023-09-22 13:52:48 +02:00
wyzula-jan
05f48de3f1 fix: motor_example.py precision in duplicate table fixed 2023-09-22 13:46:53 +02:00
wyzula-jan
401fec8539 fix: motor_example.py duplicate table fixed 2023-09-22 13:43:57 +02:00
wyzula-jan
b13509e9eb fix: motor_example.py manual changing coordinates in start/stop works again 2023-09-22 11:22:26 +02:00
wyzula-jan
a15860abac fix: motor_example.py replot points logic simplified 2023-09-22 10:59:08 +02:00
wyzula-jan
673ed325d1 fix: motor_example.py new independent mapping relying on the table 2023-09-22 09:39:58 +02:00
wyzula-jan
63f52fc841 fix: extreme.py formatting fixed 2023-09-21 11:27:27 +02:00
wyzula-jan
200e8b2351 Merge branch 'fix-line-plot' 2023-09-21 11:23:13 +02:00
wyzula-jan
e4f23f5101 fix: line_plot.py ROI interactions fixed 2023-09-21 11:22:43 +02:00
e21536
b41d63ea4d fix: online changes e21543 2023-09-21 10:43:45 +02:00
wyzula-jan
418480f1fc fix: motor_example.py user is blocked to duplicate last entry in start/end mode if end coordinate was not defined 2023-09-19 14:16:41 +02:00
semantic-release
6955b6e292 0.26.7
Automatically generated by python-semantic-release
2023-09-19 12:07:32 +00:00
wyzula-jan
abe35bf967 fix: eiger_plot_hist.py removed 2023-09-19 14:06:24 +02:00
semantic-release
174ab8fd8b 0.26.6
Automatically generated by python-semantic-release
2023-09-19 12:03:25 +00:00
wyzula-jan
7ff72b4086 docs: extreme.py updated documentation 2023-09-19 14:01:05 +02:00
wyzula-jan
cb144c7c2c fix: extreme.py saved to .yaml works correctly for different scans configurations 2023-09-19 12:09:17 +02:00
wyzula-jan
5f3d55b760 refactor: extreme.py plot init moved to config_init 2023-09-19 12:06:24 +02:00
wyzula-jan
a6940235be refactor: extreme.py changed initialisation of config 2023-09-19 11:57:43 +02:00
wyzula-jan
4287ac8885 fix: extreme.py fixed logic of loading new config.yaml during app operation 2023-09-19 11:51:57 +02:00
wyzula-jan
08f508f4c3 fix: motor_example.py - new more robust logic for getting coordinates for table go buttons 2023-09-14 17:11:24 +02:00
wyzula-jan
6124eab971 refactor: motor_example.py - function to connect buttons in the table 2023-09-14 15:50:15 +02:00
wyzula-jan
65b045e1a2 feat: motor_example.py in start/end mode new button allowing user to go to end position 2023-09-14 15:22:03 +02:00
semantic-release
bd28aa0361 0.26.5
Automatically generated by python-semantic-release
2023-09-13 08:05:28 +00:00
wyzula-jan
a5c6ffaa02 fix: motor_example.py help extended 2023-09-13 10:04:22 +02:00
wyzula-jan
34c785b92c refactor: extreme config example 2023-09-13 09:50:56 +02:00
semantic-release
7ad1cb47f3 0.26.4
Automatically generated by python-semantic-release
2023-09-12 15:43:45 +00:00
wyzula-jan
7cb56e9e7f fix: logic fixed 2023-09-12 17:41:47 +02:00
semantic-release
4fabee69d8 0.26.3
Automatically generated by python-semantic-release
2023-09-12 15:05:43 +00:00
wyzula-jan
230ccba909 Merge remote-tracking branch 'origin/master' 2023-09-12 17:04:37 +02:00
wyzula-jan
b867f25c78 fix: import works for both modes 2023-09-12 17:04:25 +02:00
semantic-release
9b715c69c0 0.26.2
Automatically generated by python-semantic-release
2023-09-12 14:54:55 +00:00
wyzula-jan
cacc076959 fix: import with start/stop mode works again 2023-09-12 16:53:50 +02:00
semantic-release
7df7aadea8 0.26.1
Automatically generated by python-semantic-release
2023-09-12 14:02:16 +00:00
wyzula-jan
56e619d239 Merge remote-tracking branch 'origin/master' 2023-09-12 16:01:12 +02:00
wyzula-jan
0e634ee2ac fix: removed scipy from eiger_plot.py 2023-09-12 15:59:38 +02:00
semantic-release
19746c0b76 0.26.0
Automatically generated by python-semantic-release
2023-09-12 13:57:21 +00:00
wyzula-jan
7b844c805d Merge branch 'extreme-feedback' 2023-09-12 15:56:08 +02:00
wyzula-jan
723503851b refactor: config_example.yaml 2023-09-12 15:55:59 +02:00
wyzula-jan
57e69907d5 feat: plot different signals and plot configurations based on different scans 2023-09-12 14:54:18 +02:00
semantic-release
f03dac0167 0.25.1
Automatically generated by python-semantic-release
2023-09-12 10:01:01 +00:00
wyzula-jan
8ff983f16e fix: specific config for csaxs 2023-09-12 11:59:34 +02:00
wyzula-jan
10ccf0cc97 fix: mode lock in config to disable changing the mode for users 2023-09-12 11:57:29 +02:00
semantic-release
c510f4eb63 0.25.0
Automatically generated by python-semantic-release
2023-09-12 09:44:32 +00:00
wyzula-jan
12b46a71a2 Merge branch 'cSAX-feedback' 2023-09-12 11:43:24 +02:00
semantic-release
8d860ec3d1 0.24.2
Automatically generated by python-semantic-release
2023-09-12 06:52:49 +00:00
e20643
265744076c fix: changes e20643 2023-09-12 08:51:24 +02:00
wyzula-jan
2123361ada fix: extra columns works again 2023-09-11 17:50:03 +02:00
wyzula-jan
f2fde2cf5c feat: comboBox to switch between entries mode 2023-09-11 17:04:38 +02:00
wyzula-jan
14a0c92fb9 refactor: changed order of columns 2023-09-11 16:42:12 +02:00
wyzula-jan
702e758812 refactor: align_table_center as a static method 2023-09-11 11:35:21 +02:00
wyzula-jan
63e3896725 fix: resize table is user controlled 2023-09-11 11:21:09 +02:00
semantic-release
ddaafa6a04 0.24.1
Automatically generated by python-semantic-release
2023-09-08 16:05:03 +00:00
wyzula-jan
f79a143417 Merge remote-tracking branch 'origin/master' 2023-09-08 18:04:03 +02:00
wyzula-jan
3b12f1bc1d fix: typo fixed in mca_plot.py 2023-09-08 18:03:54 +02:00
semantic-release
a7934d58d8 0.24.0
Automatically generated by python-semantic-release
2023-09-08 15:57:09 +00:00
wyzula-jan
ae040727fc feat: histogramLUT for mca_plot 2023-09-08 17:56:07 +02:00
14 changed files with 968 additions and 516 deletions

View File

@@ -2,6 +2,121 @@
<!--next-version-placeholder-->
## v0.27.0 (2023-09-25)
### Feature
* Motor_example.py in start/end mode new button allowing user to go to end position ([`65b045e`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/65b045e1a26a0f799311e9dca25e2a9dfd7f7147))
### Fix
* Epics removed from requirements ([`44cc881`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/44cc881ac9e69c68f1f5296fea62a14daa55d4e3))
* Motor_example.py load .csv logic fixed ([`b78152b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/b78152b14999ba5c07d7cd2ef2e3309df1ba5ca6))
* Motor_example.py export .csv logic fixed ([`85841cd`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/85841cdf1fc44472cfcc7e3e6529a41018140896))
* Motor_example.py precision in duplicate table fixed ([`05f48de`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/05f48de3f1f6793de3f6a8bc2c5e3ad3261dfcf0))
* Motor_example.py duplicate table fixed ([`401fec8`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/401fec85395886ea2816b6993bf8084b6e652967))
* Motor_example.py manual changing coordinates in start/stop works again ([`b13509e`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/b13509e9eb88b55a59b141b0cec06f3c8a983151))
* Motor_example.py replot points logic simplified ([`a15860a`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/a15860abac984328382868b0a953960c44792c41))
* Motor_example.py new independent mapping relying on the table ([`673ed32`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/673ed325d1f56505035899549ea555497823a31f))
* Extreme.py formatting fixed ([`63f52fc`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/63f52fc8419cd53856a32af6be3f548f8e077cd1))
* Line_plot.py ROI interactions fixed ([`e4f23f5`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/e4f23f51012b54cde5cd41bb9ab356a277ef4b2f))
* Online changes e21543 ([`b41d63e`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/b41d63ea4d6e15c80a7baab7a70c607079152d0a))
* Motor_example.py user is blocked to duplicate last entry in start/end mode if end coordinate was not defined ([`418480f`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/418480f1fcdc72c05887e8b73c24f76e1e8475b2))
* Motor_example.py - new more robust logic for getting coordinates for table go buttons ([`08f508f`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/08f508f4c3c3e1f3c2d4c6dda0d8e6693e9331b5))
### Performance
* Motor_example.py replot logic optimizes ([`a4fb6bd`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/a4fb6bd1d2c819740077cdc7291daf28a9e4abdd))
## v0.26.7 (2023-09-19)
### Fix
* Eiger_plot_hist.py removed ([`abe35bf`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/abe35bf96757a38395733bddbd8702a29fd26f42))
## v0.26.6 (2023-09-19)
### Fix
* Extreme.py saved to .yaml works correctly for different scans configurations ([`cb144c7`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/cb144c7c2cba50fc49ba53b0a9e3293b549665be))
* Extreme.py fixed logic of loading new config.yaml during app operation ([`4287ac8`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/4287ac888591abf27a4e4ce8c23f94d54bc6c2a9))
### Documentation
* Extreme.py updated documentation ([`7ff72b4`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/7ff72b4086e5e340d591d130f011f83fc8370315))
## v0.26.5 (2023-09-13)
### Fix
* Motor_example.py help extended ([`a5c6ffa`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/a5c6ffaa024a0dd6901976c81ea9146e5be016ec))
## v0.26.4 (2023-09-12)
### Fix
* Logic fixed ([`7cb56e9`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/7cb56e9e7f2cbeee5a141c4a52a3489c26963839))
## v0.26.3 (2023-09-12)
### Fix
* Import works for both modes ([`b867f25`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/b867f25c780ba97393ca65fe76c1cb492f365ded))
## v0.26.2 (2023-09-12)
### Fix
* Import with start/stop mode works again ([`cacc076`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/cacc076959cdd55218b74de2974d890e583c3d94))
## v0.26.1 (2023-09-12)
### Fix
* Removed scipy from eiger_plot.py ([`0e634ee`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/0e634ee2ac58b8be43b7f4e64fbc08ef08675aa1))
## v0.26.0 (2023-09-12)
### Feature
* Plot different signals and plot configurations based on different scans ([`57e6990`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/57e69907d55f7693e97d48026f3bb426adfb4870))
## v0.25.1 (2023-09-12)
### Fix
* Specific config for csaxs ([`8ff983f`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/8ff983f16e78d881582d4aaaa0261e10d9d62bf2))
* Mode lock in config to disable changing the mode for users ([`10ccf0c`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/10ccf0cc977cae30c0c185a920e15b9cf2def58f))
## v0.25.0 (2023-09-12)
### Feature
* ComboBox to switch between entries mode ([`f2fde2c`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/f2fde2cf5c4b219520eb0257c1c8e02ce66cde87))
### Fix
* Extra columns works again ([`2123361`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/2123361ada9767333792d34de56d6f1447f67cda))
* Resize table is user controlled ([`63e3896`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/63e389672560e505159de2014846d1506b05633f))
## v0.24.2 (2023-09-12)
### Fix
* Changes e20643 ([`2657440`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/265744076cc53bd054b45c12de3bb24b23e1845c))
## v0.24.1 (2023-09-08)
### Fix
* Typo fixed in mca_plot.py ([`3b12f1b`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/3b12f1bc1d65772fc3613f62013809445dcead7a))
## v0.24.0 (2023-09-08)
### Feature
* HistogramLUT for mca_plot ([`ae04072`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/ae040727fc60160de8b50ac1af51fba676106e52))
## v0.23.0 (2023-09-08)
### Feature

View File

@@ -1,128 +0,0 @@
import threading
import numpy as np
import pyqtgraph as pg
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QCheckBox
import zmq
import json
import h5py
import os
class EigerPlot(QWidget):
update_signale = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.mask_file = os.path.expanduser('~/Data10/software/radial_integration_scipts/bad_pix_map_Eiger9M.h5')
pg.setConfigOptions(background="w", foreground="k", antialias=True)
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.hist_lim = [0,20]
self.glw = pg.GraphicsLayoutWidget()
self.use_fft = False
# self.glw.show()
# self.setCentralItem(self.glw)
self.checkBox_FFT = QCheckBox("FFT")
self.checkBox_FFT.stateChanged.connect(self.on_fft_changed)
self.layout.addWidget(self.checkBox_FFT)
self.layout.addWidget(self.glw)
self.plot_item = pg.PlotItem()
self.plot_item.setAspectLocked(True)
self.imageItem = pg.ImageItem()
self.plot_item.addItem(self.imageItem)
self.glw.addItem(self.plot_item)
self.hist = pg.HistogramLUTItem()
self.hist.setImageItem(self.imageItem)
self.hist.setLevels(min=self.hist_lim[0],max=self.hist_lim[1])
self.hist.setHistogramRange(self.hist_lim[0] - 0.1 * self.hist_lim[0],self.hist_lim[1] + 0.1 * self.hist_lim[1])
self.hist.disableAutoHistogramRange()
self.hist.gradient.loadPreset('magma')
self.glw.addItem(self.hist)
# self.plot_item.addItem(self.hist)
# add plot and histogram to glw
# self.glw.addItem(self.plot_item)
# self.glw.addItem(self.hist)
# self.imageItem.setImage([[0,1,2],[4,5,6]])
self.update_signale.connect(self.on_image_update)
self.mask = None
self._load_mask()
self.start_zmq_consumer()
def start_zmq_consumer(self):
consumer_thread = threading.Thread(target=self.zmq_consumer, daemon=True).start()
def _load_mask(self):
with h5py.File(self.mask_file, "r") as f:
self.mask = f["data"][...]
def zmq_consumer(self):
try:
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, "")
while True:
raw_meta, raw_data = receiver.recv_multipart()
meta = json.loads(raw_meta.decode('utf-8'))
self.image = np.frombuffer(raw_data, dtype=meta['type']).reshape(meta['shape'])
self.update_signale.emit()
finally:
receiver.disconnect(live_stream_url)
receiver.context.term()
@pyqtSlot()
def on_fft_changed(self):
self.update_signale.emit()
@pyqtSlot()
def on_image_update(self):
# if self.checkBox_FFT.isChecked():
# img = np.log10(np.abs(np.fft.fftshift(np.fft.fft2(self.image*(1-self.mask.T)))))
# else:
img = np.log10(self.image*(1-self.mask)+1)
self.imageItem.setImage(img,autoLevels=False)
# hardcoded hist level
# self.hist.setLevels(min=self.hist_lim[0],max=self.hist_lim[1])
# self.hist.setHistogramRange(self.hist_lim[0] - 0.1 * self.hist_lim[0],self.hist_lim[1] + 0.1 * self.hist_lim[1])
if __name__ == "__main__":
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
plot = EigerPlot()
plot.show()
sys.exit(app.exec_())

View File

@@ -1,8 +1,6 @@
import json
import json
import os
import threading
import time
import h5py
import numpy as np
@@ -21,7 +19,9 @@ from PyQt5.QtWidgets import (
QFrame,
)
from pyqtgraph.Qt import uic
from scipy.stats import multivariate_normal
# from scipy.stats import multivariate_normal
class EigerPlot(QWidget):
@@ -68,7 +68,7 @@ class EigerPlot(QWidget):
def hook_signals(self):
# Buttons
self.pushButton_test.clicked.connect(self.start_sim_stream)
# self.pushButton_test.clicked.connect(self.start_sim_stream)
self.pushButton_mask.clicked.connect(self.load_mask_dialog)
self.pushButton_delete_mask.clicked.connect(self.delete_mask)
self.pushButton_help.clicked.connect(self.show_help_dialog)
@@ -173,7 +173,7 @@ class EigerPlot(QWidget):
self.image = np.transpose(self.image)
if self.checkBox_log.isChecked():
self.image = np.log(self.image)
self.image = np.log10(self.image)
self.imageItem.setImage(self.image, autoLevels=False)
@@ -262,30 +262,30 @@ class EigerPlot(QWidget):
###############################
# 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)
# 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__":

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>808</width>
<height>675</height>
<width>874</width>
<height>762</height>
</rect>
</property>
<property name="windowTitle">
@@ -172,13 +172,6 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_test">
<property name="text">
<string>Simulated Stream</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -1,77 +1,130 @@
plot_settings:
background_color: "black"
num_columns: 3
num_columns: 2
colormap: "plasma"
#TODO add more settings
# - plot size
scan_types: False # True to show scan types
# example to use without scan_type -> only one general configuration
plot_data:
- plot_name: "BPM plot"
x:
label: 'Motor X'
signals:
- name: "samx"
# entry: "samx"
y:
label: 'BPM'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- name: "gauss_adc1"
entry: "gauss_adc1"
- name: "gauss_adc2"
entry: "gauss_adc2"
- plot_name: "BPM plot 2"
x:
label: 'Motor X'
signals:
- name: "samx"
entry: "samx"
y:
label: 'BPM'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- name: "gauss_adc1"
entry: "gauss_adc1"
- plot_name: "BPM plot 3"
x:
label: 'Motor X'
signals:
- name: "samx"
entry: "samx"
y:
label: 'BPM'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- name: "gauss_adc1"
entry: "gauss_adc1"
- plot_name: "ADC plot"
- plot_name: "BPM4i plots vs samy"
x:
label: 'Motor Y'
signals:
- name: "samy"
# entry: "samy" # here I also forgot to specify entry
y:
label: 'ADC'
label: 'bpm4i'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- name: "samx"
- name: "bpm4i"
entry: "bpm4i"
# I will not specify entry, because I want to take hint from gauss_adc2
- plot_name: "Multi"
- plot_name: "BPM4i plots vs samx"
x:
label: 'Motor Y'
signals:
- name: "samy"
# entry: "samy" # here I also forgot to specify entry
y:
label: 'bpm4i'
signals:
- name: "bpm4i"
entry: "bpm4i"
# I will not specify entry, because I want to take hint from gauss_adc2
- plot_name: "MCS Channel 4 (Cyberstar) vs samx"
x:
label: 'Motor X'
signals:
- name: "samx"
entry: "samx"
y:
label: 'Multi'
label: 'mcs4 cyberstar'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- name: "samx"
entry: ["samx", "samx_setpoint"]
# entry: ["samx","incorect"] #multiple entries for one device
- name: "mcs"
entry: "mca4"
- plot_name: "MCS Channel 4 (Cyberstar) vs samy"
x:
label: 'Motor X'
signals:
- name: "samy"
entry: "samy"
y:
label: 'mcs4 cyberstar'
signals:
- name: "mcs"
entry: "mca4"
# example to use with scan_type -> different configuration for different scan types
#plot_data:
# line_scan:
# - plot_name: "BPM plot"
# x:
# label: 'Motor X'
# signals:
# - name: "samx"
# # entry: "samx"
# y:
# label: 'BPM'
# signals:
# - name: "gauss_bpm"
# entry: "gauss_bpm"
# - name: "gauss_adc1"
# entry: "gauss_adc1"
# - name: "gauss_adc2"
# entry: "gauss_adc2"
#
# - plot_name: "Multi"
# x:
# label: 'Motor X'
# signals:
# - name: "samx"
# entry: "samx"
# y:
# label: 'Multi'
# signals:
# - name: "gauss_bpm"
# entry: "gauss_bpm"
# - name: "samx"
# entry: ["samx", "samx_setpoint"]
#
# grid_scan:
# - plot_name: "Grid plot 1"
# x:
# label: 'Motor X'
# signals:
# - name: "samx"
# entry: "samx"
# y:
# label: 'BPM'
# signals:
# - name: "gauss_bpm"
# entry: "gauss_bpm"
# - name: "gauss_adc1"
# entry: "gauss_adc1"
# - plot_name: "Grid plot 2"
# x:
# label: 'Motor X'
# signals:
# - name: "samx"
# entry: "samx"
# y:
# label: 'BPM'
# signals:
# - name: "gauss_bpm"
# entry: "gauss_bpm"
# - name: "gauss_adc1"
# entry: "gauss_adc1"
#
# - plot_name: "Grid plot 3"
# x:
# label: 'Motor Y'
# signals:
# - name: "samy"
# entry: "samy"
# y:
# label: 'BPM'
# signals:
# - name: "gauss_bpm"
# entry: "gauss_bpm"

View File

@@ -0,0 +1,91 @@
plot_settings:
background_color: "black"
num_columns: 2
colormap: "plasma"
scan_types: True # True to show scan types
# example to use with scan_type -> different configuration for different scan types
plot_data:
line_scan:
- plot_name: "BPM plot"
x:
label: 'Motor X'
signals:
- name: "samx"
# entry: "samx"
y:
label: 'BPM'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- name: "gauss_adc1"
entry: "gauss_adc1"
- name: "gauss_adc2"
entry: "gauss_adc2"
- plot_name: "Multi"
x:
label: 'Motor X'
signals:
- name: "samx"
entry: "samx"
y:
label: 'Multi'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- name: "samx"
entry: ["samx", "samx_setpoint"]
grid_scan:
- plot_name: "Grid plot 1"
x:
label: 'Motor X'
signals:
- name: "samx"
entry: "samx"
y:
label: 'BPM'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- name: "gauss_adc1"
entry: "gauss_adc1"
- plot_name: "Grid plot 2"
x:
label: 'Motor X'
signals:
- name: "samx"
entry: "samx"
y:
label: 'BPM'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- name: "gauss_adc1"
entry: "gauss_adc1"
- plot_name: "Grid plot 3"
x:
label: 'Motor Y'
signals:
- name: "samy"
entry: "samy"
y:
label: 'BPM'
signals:
- name: "gauss_bpm"
entry: "gauss_bpm"
- plot_name: "Grid plot 4"
x:
label: 'Motor Y'
signals:
- name: "samy"
entry: "samy"
y:
label: 'BPM'
signals:
- name: "gauss_adc3"
entry: "gauss_adc3"

View File

@@ -1,16 +1,16 @@
import os
import numpy as np
import pyqtgraph
import pyqtgraph as pg
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QApplication, QWidget, QTableWidgetItem, QTableWidget, QFileDialog
from pyqtgraph import mkBrush, mkColor, mkPen
from pyqtgraph import ColorButton
from pyqtgraph import mkBrush, mkPen
from pyqtgraph.Qt import QtCore, uic
from pyqtgraph.Qt import QtWidgets
from bec_lib.core import MessageEndpoints
from bec_widgets.qt_utils import Crosshair, Colors
from pyqtgraph.Qt import QtWidgets
from pyqtgraph import ColorButton
# TODO implement:
@@ -29,43 +29,57 @@ class PlotApp(QWidget):
name and entry, for a particular plot.
Args:
plot_settings (dict): Dictionary containing global plot settings such as background color.
plot_data (list of dict): List of dictionaries specifying the signals to plot.
Each dictionary should contain:
- 'x': Dictionary specifying the x-axis settings including
a 'signals' list with 'name' and 'entry' fields.
If there are multiple entries for one device name, they can be passed as a list.
- 'y': Similar to 'x', but for the y-axis.
Example:
[
{
'plot_name': 'Plot 1',
'x': {'label': 'X Label', 'signals': [{'name': 'x1', 'entry': 'x1_entry'}]},
'y': {'label': 'Y Label', 'signals': [{'name': 'y1', 'entry': 'y1_entry'}]}
},
...
]
config (dict): Configuration dictionary containing all settings for the plotting app.
It should include the following keys:
- 'plot_settings': Dictionary containing global plot settings.
- 'plot_data': List of dictionaries specifying the signals to plot.
parent (QWidget, optional): Parent widget.
Example:
General Plot Configuration:
{
'plot_settings': {'background_color': 'black', 'num_columns': 2, 'colormap': 'plasma', 'scan_types': False},
'plot_data': [
{
'plot_name': 'Plot A',
'x': {'label': 'X-axis', 'signals': [{'name': 'device_x', 'entry': 'entry_x'}]},
'y': {'label': 'Y-axis', 'signals': [{'name': 'device_y', 'entry': 'entry_y'}]}
}
]
}
Different Scans Mode Configuration:
{
'plot_settings': {'background_color': 'black', 'num_columns': 2, 'colormap': 'plasma', 'scan_types': True},
'plot_data': {
'scan_type_1': [
{
'plot_name': 'Plot 1',
'x': {'label': 'X-axis', 'signals': [{'name': 'device_x1', 'entry': 'entry_x1'}]},
'y': {'label': 'Y-axis', 'signals': [{'name': 'device_y1', 'entry': 'entry_y1'}]}
}
],
'scan_type_2': [
{
'plot_name': 'Plot 2',
'x': {'label': 'X-axis', 'signals': [{'name': 'device_x2', 'entry': 'entry_x2'}]},
'y': {'label': 'Y-axis', 'signals': [{'name': 'device_y2', 'entry': 'entry_y2'}]}
}
]
}
}
"""
update_signal = pyqtSignal()
update_dap_signal = pyqtSignal()
def __init__(self, plot_settings: dict, plot_data: list, parent=None):
def __init__(self, config: dict, parent=None):
super(PlotApp, self).__init__(parent)
# YAML config
self.plot_settings = plot_settings
self.plot_data = plot_data
# Setting global plot settings
self.init_plot_background(self.plot_settings["background_color"])
# Loading UI
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "extreme.ui"), self)
# Nested dictionary to hold x and y data for multiple plots
self.data = {}
self.crosshairs = None
@@ -76,12 +90,9 @@ class PlotApp(QWidget):
self.user_colors = {} # key: (plot_name, y_name, y_entry), value: color
# Initialize the UI
self.init_ui(self.plot_settings["num_columns"])
self.spinBox_N_columns.setValue(
self.plot_settings["num_columns"]
) # TODO has to be checked if it will not setup more columns than plots
self.spinBox_N_columns.setMaximum(len(self.plot_data))
# YAML config
self.init_config(config)
self.splitter.setSizes([400, 100])
# Buttons
@@ -96,6 +107,35 @@ class PlotApp(QWidget):
# Change layout of plots when the number of columns is changed in GUI
self.spinBox_N_columns.valueChanged.connect(lambda x: self.init_ui(x))
def init_config(self, config: dict) -> None:
"""
Initializes or update the configuration settings for the PlotApp.
Args:
config (dict): Dictionary containing plot settings and data configurations.
"""
# YAML config
self.plot_settings = config.get("plot_settings", {})
self.plot_data_config = config.get("plot_data", {})
self.scan_types = self.plot_settings.get("scan_types", False)
if self.scan_types is False:
self.plot_data = self.plot_data_config # TODO logic has to be improved
else:
self.plot_data = {}
# Setting global plot settings
self.init_plot_background(self.plot_settings["background_color"])
# Initialize the UI
if self.scan_types is False:
self.init_ui(self.plot_settings["num_columns"])
self.spinBox_N_columns.setValue(
self.plot_settings["num_columns"]
) # TODO has to be checked if it will not setup more columns than plots
self.spinBox_N_columns.setMaximum(len(self.plot_data))
def init_plot_background(self, background_color: str) -> None:
"""
Initialize plot settings based on the background color.
@@ -195,6 +235,7 @@ class PlotApp(QWidget):
curve_list = []
for i, (y_config, color) in enumerate(zip(y_configs, colors_ys)):
print(y_config)
y_name = y_config["name"]
y_entries = y_config.get("entry", [y_name])
@@ -245,15 +286,14 @@ class PlotApp(QWidget):
self.tableWidget_crosshair.setVerticalHeaderLabels(row_labels)
self.hook_crosshair()
# def change_curve_color(self, btn, curve):
# """Change the color of a curve."""
# color = btn.color()
# pen_curve = mkPen(color=color, width=2, style=QtCore.Qt.DashLine)
# brush_curve = mkBrush(color=color)
# curve.setPen(pen_curve)
# curve.setSymbolBrush(brush_curve)
def change_curve_color(self, btn, plot_name, y_name, y_entry, curve):
def change_curve_color(
self,
btn: pyqtgraph.ColorButton,
plot_name: str,
y_name: str,
y_entry: str,
curve: pyqtgraph.PlotDataItem,
) -> None:
"""
Change the color of a curve and update the corresponding ColorButton.
@@ -271,7 +311,7 @@ class PlotApp(QWidget):
curve.setSymbolBrush(brush_curve)
self.user_colors[(plot_name, y_name, y_entry)] = color
def hook_crosshair(self):
def hook_crosshair(self) -> None:
"""Attach crosshairs to each plot and connect them to the update_table method."""
self.crosshairs = {}
for plot_name, plot in self.plots.items():
@@ -341,7 +381,7 @@ class PlotApp(QWidget):
@pyqtSlot(dict, dict)
def on_scan_segment(self, msg, metadata) -> None:
"""
Handle new scan segments and saves data to a dictionary.
Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
Args:
msg (dict): Message received with scan data.
@@ -353,6 +393,19 @@ class PlotApp(QWidget):
return
if current_scanID != self.scanID:
if self.scan_types is False:
self.plot_data = self.plot_data_config
elif self.scan_types is True:
currentName = metadata.get("scan_name")
self.plot_data = self.plot_data_config.get(currentName, [])
# Init UI
self.init_ui(self.plot_settings["num_columns"])
self.spinBox_N_columns.setValue(
self.plot_settings["num_columns"]
) # TODO has to be checked if it will not setup more columns than plots
self.spinBox_N_columns.setMaximum(len(self.plot_data))
self.scanID = current_scanID
self.data = {}
self.init_curves()
@@ -436,7 +489,8 @@ class PlotApp(QWidget):
with open(file_path, "w") as file:
yaml.dump(
{"plot_settings": self.plot_settings, "plot_data": self.plot_data}, file
{"plot_settings": self.plot_settings, "plot_data": self.plot_data_config},
file,
)
print(f"Settings saved to {file_path}")
except Exception as e:
@@ -455,13 +509,9 @@ class PlotApp(QWidget):
with open(file_path, "r") as file:
config = yaml.safe_load(file)
self.plot_settings = config.get("plot_settings", {})
self.plot_data = config.get("plot_data", {})
# Reinitialize the UI and plots
# TODO implement, change background works only before loading .ui file
# self.init_plot_background(self.plot_settings["background_color"])
self.init_ui(self.plot_settings["num_columns"])
self.init_curves()
# YAML config
self.init_config(config)
print(f"Settings loaded from {file_path}")
except FileNotFoundError:
print(f"The file {file_path} was not found.")
@@ -473,7 +523,7 @@ if __name__ == "__main__":
import yaml
import argparse
from bec_widgets import ctrl_c
# from bec_widgets import ctrl_c
from bec_widgets.bec_dispatcher import bec_dispatcher
parser = argparse.ArgumentParser(description="Plotting App")
@@ -486,9 +536,6 @@ if __name__ == "__main__":
with open(args.config, "r") as file:
config = yaml.safe_load(file)
plot_settings = config.get("plot_settings", {})
plot_data = config.get("plot_data", {})
except FileNotFoundError:
print(f"The file {args.config} was not found.")
exit(1)
@@ -505,11 +552,11 @@ if __name__ == "__main__":
queue = client.queue
app = QApplication([])
plotApp = PlotApp(plot_settings=plot_settings, plot_data=plot_data)
plotApp = PlotApp(config=config)
# Connecting signals from bec_dispatcher
bec_dispatcher.connect_slot(plotApp.on_scan_segment, MessageEndpoints.scan_segment())
ctrl_c.setup(app)
# ctrl_c.setup(app)
window = plotApp
window.show()

View File

@@ -17,9 +17,11 @@ class StreamApp(QWidget):
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.scanID = None
self.stream_consumer = None
@@ -29,7 +31,7 @@ class StreamApp(QWidget):
self.start_device_consumer()
# self.start_device_consumer(self.device) # for simulation
# self.start_device_consumer(self.device) # for simulation
self.new_scanID.connect(self.create_new_stream_consumer)
self.update_signal.connect(self.plot_new)
@@ -40,13 +42,26 @@ class StreamApp(QWidget):
self.setLayout(self.layout)
# Create plot
# self.glw = pg.GraphicsLayoutWidget()
self.plot_widget = pg.PlotWidget(title="MCA readout")
self.image_item = pg.ImageItem()
self.plot_widget.addItem(self.image_item)
self.glw = pg.GraphicsLayoutWidget()
self.layout.addWidget(self.glw)
# Add widgets to the layout
self.layout.addWidget(self.plot_widget)
# 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, scanID: str):
@@ -81,23 +96,26 @@ class StreamApp(QWidget):
# self.device_consumer.start()
def plot_new(self):
self.image_item.setImage(self.data.T)
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 = BECMessage.DeviceMessage.loads(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
if parent.data is None:
parent.data = np.array([row])
else:
parent.data = np.vstack((parent.data, row))
# 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()
@@ -116,8 +134,8 @@ class StreamApp(QWidget):
if current_scanID != parent.scanID:
parent.scanID = current_scanID
parent.data = None
parent.image_item.clear()
#parent.data = None
#parent.imageItem.clear()
parent.new_scanID.emit(current_scanID)
print(f"New scanID: {current_scanID}")
@@ -129,10 +147,10 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Stream App.")
parser.add_argument(
"--port", type=str, default="localhost:6379", help="Port for RedisConnector"
"--port", type=str, default="pc15543:6379", help="Port for RedisConnector"
)
parser.add_argument("--device", type=str, default="mca", help="Device name")
parser.add_argument("--sub_device", type=str, default="mca1", help="Sub-device name")
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()

View File

@@ -7,8 +7,11 @@ plot_motors:
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]: 1
- Step [y]: 1
- Exposure time [s]: 1
- step_x [mu]: 25
- step_y [mu]: 25
- exp_time [s]: 1
- start: 1
- tilt [deg]: 0

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

View File

@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>1409</width>
<width>1561</width>
<height>748</height>
</rect>
</property>
@@ -506,6 +506,44 @@
<string>Coordinates</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Entries Mode:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_mode">
<item>
<property name="text">
<string>Individual</string>
</property>
</item>
<item>
<property name="text">
<string>Start/Stop</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="tableWidget_coordinates">
<property name="selectionMode">
@@ -514,6 +552,11 @@
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Show</string>
</property>
</column>
<column>
<property name="text">
<string>Move</string>
@@ -521,7 +564,7 @@
</column>
<column>
<property name="text">
<string>Show</string>
<string>Tag</string>
</property>
</column>
<column>
@@ -534,33 +577,56 @@
<string>Y</string>
</property>
</column>
<column>
<property name="text">
<string>Tag</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_exportCSV">
<property name="text">
<string>Export CSV</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_importCSV">
<property name="text">
<string>Import CSV</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_help">
<property name="text">
<string>Help</string>
</property>
</widget>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_resize_table">
<property name="text">
<string>Resize Table</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="checkBox_resize_auto">
<property name="text">
<string>Resize Auto</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="pushButton_importCSV">
<property name="text">
<string>Import CSV</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="pushButton_exportCSV">
<property name="text">
<string>Export CSV</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="pushButton_help">
<property name="text">
<string>Help</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="pushButton_duplicate">
<property name="text">
<string>Duplicate Last Entry</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>

View File

@@ -13,7 +13,6 @@ from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
QApplication,
QWidget,
QTableWidget,
QFileDialog,
QDialog,
QVBoxLayout,
@@ -21,6 +20,7 @@ from PyQt5.QtWidgets import (
QPushButton,
QFrame,
)
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtWidgets import QShortcut
from pyqtgraph.Qt import QtWidgets, uic, QtCore
@@ -71,6 +71,7 @@ class MotorApp(QWidget):
self.scatter_size = plot_motors.get("scatter_size", 5)
self.precision = plot_motors.get("precision", 2)
self.extra_columns = plot_motors.get("extra_columns", None)
self.mode_lock = plot_motors.get("mode_lock", False)
# Saved motors from config file
self.selected_motors = selected_motors
@@ -84,6 +85,10 @@ class MotorApp(QWidget):
self.init_ui()
self.tag_N = 1 # position label for saved coordinates
# State tracking for entries
self.last_selected_index = -1
self.is_next_entry_end = False
# Get all motors available
self.motor_thread.retrieve_all_motors() # TODO link to combobox that it always refresh
@@ -255,16 +260,25 @@ class MotorApp(QWidget):
)
self.motor_map.setZValue(0)
self.saved_motor_positions = np.array([]) # to track saved motor positions
self.saved_point_visibility = [] # to track visibility of saved motor positions
self.saved_motor_map = pg.ScatterPlotItem(
self.saved_motor_map_start = pg.ScatterPlotItem(
size=self.scatter_size, pen=pg.mkPen(None), brush=pg.mkBrush(255, 0, 0, 255)
)
self.saved_motor_map.setZValue(1) # for saved motor positions
self.saved_motor_map_end = pg.ScatterPlotItem(
size=self.scatter_size, pen=pg.mkPen(None), brush=pg.mkBrush(0, 0, 255, 255)
)
self.saved_motor_map_individual = pg.ScatterPlotItem(
size=self.scatter_size, pen=pg.mkPen(None), brush=pg.mkBrush(0, 255, 0, 255)
)
self.saved_motor_map_start.setZValue(1) # for saved motor positions
self.saved_motor_map_end.setZValue(1) # for saved motor positions
self.saved_motor_map_individual.setZValue(1) # for saved motor positions
self.plot_map.addItem(self.motor_map)
self.plot_map.addItem(self.saved_motor_map)
self.plot_map.addItem(self.saved_motor_map_start)
self.plot_map.addItem(self.saved_motor_map_end)
self.plot_map.addItem(self.saved_motor_map_individual)
self.plot_map.showGrid(x=True, y=True)
def init_ui_motor_control(self) -> None:
@@ -408,25 +422,48 @@ class MotorApp(QWidget):
def init_ui_table(self) -> None:
"""Initialize the table validators for x and y coordinates and table signals"""
# Validators
self.double_delegate = DoubleValidationDelegate(self.tableWidget_coordinates)
self.tableWidget_coordinates.setItemDelegateForColumn(2, self.double_delegate)
self.tableWidget_coordinates.setItemDelegateForColumn(3, self.double_delegate)
# Signals
self.tableWidget_coordinates.itemChanged.connect(self.update_saved_coordinates)
# Init Default mode
self.mode_switch()
# Buttons
self.pushButton_exportCSV.clicked.connect(
lambda: self.export_table_to_csv(self.tableWidget_coordinates)
)
self.pushButton_importCSV.clicked.connect(
lambda: self.load_table_from_csv(self.tableWidget_coordinates, precision=self.precision)
)
self.pushButton_resize_table.clicked.connect(
lambda: self.resizeTable(self.tableWidget_coordinates)
)
self.pushButton_duplicate.clicked.connect(
lambda: self.duplicate_last_row(self.tableWidget_coordinates)
)
self.pushButton_help.clicked.connect(self.show_help_dialog)
# Mode switch
self.comboBox_mode.currentIndexChanged.connect(self.mode_switch)
# Manual Edit
self.tableWidget_coordinates.itemChanged.connect(self.handle_manual_edit)
def init_mode_lock(self) -> None:
if self.mode_lock is False:
return
elif self.mode_lock == "Individual":
self.comboBox_mode.setCurrentIndex(0)
self.comboBox_mode.setEnabled(False)
elif self.mode_lock == "Start/Stop":
self.comboBox_mode.setCurrentIndex(1)
self.comboBox_mode.setEnabled(False)
else:
self.mode_lock = False
print(f"Warning: Mode lock '{self.mode_lock}' not recognized.")
print(f"Unlocking mode lock.")
def init_ui(self) -> None:
"""Setup all ui elements"""
@@ -437,6 +474,7 @@ class MotorApp(QWidget):
self.init_ui_motor_connections() # Motor Connections
self.init_keyboard_shortcuts() # Keyboard Shortcuts
self.init_ui_table() # Table validators for x and y coordinates
self.init_mode_lock() # Mode lock
def init_motor_map(self):
# Get motor limits
@@ -534,26 +572,83 @@ class MotorApp(QWidget):
self.toolButton_up.setShortcut("")
self.toolButton_down.setShortcut("")
def mode_switch(self):
current_index = self.comboBox_mode.currentIndex()
if self.tableWidget_coordinates.rowCount() > 0:
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Warning)
msgBox.setText(
"Switching modes will delete all table entries. Do you want to continue?"
)
msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
returnValue = msgBox.exec()
if returnValue == QMessageBox.Cancel:
self.comboBox_mode.blockSignals(True) # Block signals
self.comboBox_mode.setCurrentIndex(self.last_selected_index)
self.comboBox_mode.blockSignals(False) # Unblock signals
return
self.tableWidget_coordinates.setRowCount(0) # Wipe table
# Clear saved points from map
self.saved_motor_map_start.clear()
self.saved_motor_map_end.clear()
self.saved_motor_map_individual.clear()
if current_index == 0: # 'individual' is selected
header = ["Show", "Move", "Tag", "X", "Y"]
self.tableWidget_coordinates.setColumnCount(len(header))
self.tableWidget_coordinates.setHorizontalHeaderLabels(header)
self.tableWidget_coordinates.setItemDelegateForColumn(3, self.double_delegate)
self.tableWidget_coordinates.setItemDelegateForColumn(4, self.double_delegate)
elif current_index == 1: # 'start/stop' is selected
header = [
"Show",
"Move [start]",
"Move [end]",
"Tag",
"X [start]",
"Y [start]",
"X [end]",
"Y [end]",
]
self.tableWidget_coordinates.setColumnCount(len(header))
self.tableWidget_coordinates.setHorizontalHeaderLabels(header)
self.tableWidget_coordinates.setItemDelegateForColumn(3, self.double_delegate)
self.tableWidget_coordinates.setItemDelegateForColumn(4, self.double_delegate)
self.tableWidget_coordinates.setItemDelegateForColumn(5, self.double_delegate)
self.tableWidget_coordinates.setItemDelegateForColumn(6, self.double_delegate)
self.last_selected_index = current_index # Save the last selected index
def generate_table_coordinate(
self, table: QtWidgets.QTableWidget, coordinates: tuple, tag: str = None, precision: int = 0
) -> None:
current_row_count = table.rowCount()
table.setRowCount(current_row_count + 1)
# To not call replot points during table generation
self.replot_lock = True
current_index = self.comboBox_mode.currentIndex()
if current_index == 1 and self.is_next_entry_end:
target_row = table.rowCount() - 1 # Last row
else:
new_row_count = table.rowCount() + 1
table.setRowCount(new_row_count)
target_row = new_row_count - 1 # New row
# Create QDoubleValidator
validator = QDoubleValidator()
validator.setDecimals(precision)
# Checkbox for visibility switch -> always first column
checkBox = QtWidgets.QCheckBox()
checkBox.setChecked(True)
button = QtWidgets.QPushButton("Go")
checkBox.stateChanged.connect(
lambda state, widget=checkBox: self.toggle_point_visibility(state, widget)
)
table.setItem(current_row_count, 4, QtWidgets.QTableWidgetItem(str(tag)))
table.setCellWidget(current_row_count, 1, checkBox)
checkBox.stateChanged.connect(lambda: self.replot_based_on_table(table))
table.setCellWidget(target_row, 0, checkBox)
# Apply validator to x and y coordinate QTableWidgetItem
item_x = QtWidgets.QTableWidgetItem(str(f"{coordinates[0]:.{precision}f}"))
@@ -561,152 +656,241 @@ class MotorApp(QWidget):
item_x.setFlags(item_x.flags() | Qt.ItemIsEditable)
item_y.setFlags(item_y.flags() | Qt.ItemIsEditable)
table.setItem(
current_row_count, 2, QtWidgets.QTableWidgetItem(str(f"{coordinates[0]:.{precision}f}"))
)
table.setItem(
current_row_count, 3, QtWidgets.QTableWidgetItem(str(f"{coordinates[1]:.{precision}f}"))
)
# Mode switch
if current_index == 1: # start/stop mode
# Create buttons for start and end coordinates
button_start = QPushButton("Go [start]")
button_end = QPushButton("Go [end]")
table.setCellWidget(current_row_count, 0, button)
# Add buttons to table
table.setCellWidget(target_row, 1, button_start)
table.setCellWidget(target_row, 2, button_end)
button.clicked.connect(partial(self.move_to_row_coordinates, table, current_row_count))
button_end.setEnabled(
self.is_next_entry_end
) # Enable only if end coordinate is present
brushes = [
pg.mkBrush(255, 165, 0, 255) if visible else pg.mkBrush(255, 165, 0, 0)
for visible in self.saved_point_visibility
]
# Connect buttons to the slot
button_start.clicked.connect(self.move_to_row_coordinates)
button_end.clicked.connect(self.move_to_row_coordinates)
# Set Tag
table.setItem(target_row, 3, QtWidgets.QTableWidgetItem(str(tag)))
# Add coordinates to table
col_index = 8
if self.is_next_entry_end:
table.setItem(target_row, 6, item_x)
table.setItem(target_row, 7, item_y)
else:
table.setItem(target_row, 4, item_x)
table.setItem(target_row, 5, item_y)
self.is_next_entry_end = not self.is_next_entry_end
else: # Individual mode
button_start = QPushButton("Go")
table.setCellWidget(target_row, 1, button_start)
button_start.clicked.connect(self.move_to_row_coordinates)
# Set Tag
table.setItem(target_row, 2, QtWidgets.QTableWidgetItem(str(tag)))
col_index = 5
table.setItem(target_row, 3, item_x)
table.setItem(target_row, 4, item_y)
# Adding extra columns
if self.extra_columns:
col_index = 5 # Starting index for extra columns
table.setColumnCount(col_index + len(self.extra_columns))
for col_dict in self.extra_columns:
for col_name, default_value in col_dict.items():
if current_row_count == 0:
item = QtWidgets.QTableWidgetItem(str(default_value))
else:
prev_item = table.item(current_row_count - 1, col_index)
item_text = prev_item.text() if prev_item else ""
item = QtWidgets.QTableWidgetItem(item_text)
# TODO simplify nesting
if current_index != 1 or self.is_next_entry_end:
if self.extra_columns:
table.setColumnCount(col_index + len(self.extra_columns))
for col_dict in self.extra_columns:
for col_name, default_value in col_dict.items():
if target_row == 0:
item = QtWidgets.QTableWidgetItem(str(default_value))
item.setFlags(item.flags() | Qt.ItemIsEditable)
table.setItem(current_row_count, col_index, item)
else:
prev_item = table.item(target_row - 1, col_index)
item_text = prev_item.text() if prev_item else ""
item = QtWidgets.QTableWidgetItem(item_text)
if current_row_count == 0:
table.setHorizontalHeaderItem(
col_index, QtWidgets.QTableWidgetItem(col_name)
)
item.setFlags(item.flags() | Qt.ItemIsEditable)
table.setItem(target_row, col_index, item)
col_index += 1
if target_row == 0 or (current_index == 1 and not self.is_next_entry_end):
table.setHorizontalHeaderItem(
col_index, QtWidgets.QTableWidgetItem(col_name)
)
self.saved_motor_map.setData(pos=self.saved_motor_positions, brush=brushes)
col_index += 1
self.align_table_center(table)
if self.checkBox_resize_auto.isChecked():
table.resizeColumnsToContents()
# Unlock Replot
self.replot_lock = False
# Replot the saved motor map
self.replot_based_on_table(table)
def duplicate_last_row(self, table: QtWidgets.QTableWidget) -> None:
if self.is_next_entry_end is True:
msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Warning)
msgBox.setText("The end coordinates were not set for previous entry!")
msgBox.setStandardButtons(QMessageBox.Ok)
returnValue = msgBox.exec()
if returnValue == QMessageBox.Ok:
return
last_row = table.rowCount() - 1
if last_row == -1:
return
# Get the tag and coordinates from the last row
tag = table.item(last_row, 2).text() if table.item(last_row, 2) else None
mode_index = self.comboBox_mode.currentIndex()
if mode_index == 1: # start/stop mode
x_start = float(table.item(last_row, 4).text()) if table.item(last_row, 4) else None
y_start = float(table.item(last_row, 5).text()) if table.item(last_row, 5) else None
x_end = float(table.item(last_row, 6).text()) if table.item(last_row, 6) else None
y_end = float(table.item(last_row, 7).text()) if table.item(last_row, 7) else None
# Duplicate the 'start' coordinates
self.generate_table_coordinate(table, (x_start, y_start), tag, precision=self.precision)
# Duplicate the 'end' coordinates
self.generate_table_coordinate(table, (x_end, y_end), tag, precision=self.precision)
else: # individual mode
x = float(table.item(last_row, 3).text()) if table.item(last_row, 3) else None
y = float(table.item(last_row, 4).text()) if table.item(last_row, 4) else None
# Duplicate the coordinates
self.generate_table_coordinate(table, (x, y), tag, precision=self.precision)
self.align_table_center(table)
if self.checkBox_resize_auto.isChecked():
table.resizeColumnsToContents()
def handle_manual_edit(self, item):
table = item.tableWidget()
row, col = item.row(), item.column()
mode_index = self.comboBox_mode.currentIndex()
# Determine the columns where the x and y coordinates are stored based on the mode.
coord_cols = [3, 4] if mode_index == 0 else [4, 5, 6, 7]
if col not in coord_cols:
return # Only proceed if the edited columns are coordinate columns
# Replot based on the table
self.replot_based_on_table(table)
@staticmethod
def align_table_center(table: QtWidgets.QTableWidget) -> None:
for row in range(table.rowCount()):
for col in range(table.columnCount()):
item = table.item(row, col)
if item:
item.setTextAlignment(Qt.AlignCenter)
table.resizeColumnsToContents()
def move_to_row_coordinates(self):
# Find out the mode and decide columns accordingly
mode = self.comboBox_mode.currentIndex()
def move_to_row_coordinates(self, table, row):
x = float(table.item(row, 2).text())
y = float(table.item(row, 3).text())
# Get the button that emitted the signal# Get the button that emitted the signal
button = self.sender()
# Find the row and column where the button is located
row = self.tableWidget_coordinates.indexAt(button.pos()).row()
col = self.tableWidget_coordinates.indexAt(button.pos()).column()
# Decide which coordinates to move to based on the column
if mode == 1:
if col == 1: # Go to 'start' coordinates
x_col, y_col = 4, 5
elif col == 2: # Go to 'end' coordinates
x_col, y_col = 6, 7
else: # Default case
x_col, y_col = 3, 4 # For "individual" mode
# Fetch and move coordinates
x = float(self.tableWidget_coordinates.item(row, x_col).text())
y = float(self.tableWidget_coordinates.item(row, y_col).text())
self.move_motor_absolute(x, y)
def toggle_point_visibility(self, state, checkBox_widget):
parent = checkBox_widget.parent()
while not isinstance(parent, QTableWidget):
parent = parent.parent()
def replot_based_on_table(self, table):
if self.replot_lock is True:
return
table = parent
print("Replot Triggered")
start_points = []
end_points = []
individual_points = []
# self.rectangles = [] #TODO introduce later
pos = checkBox_widget.pos()
item = table.indexAt(pos)
row_index = item.row()
for row in range(table.rowCount()):
visibility = table.cellWidget(row, 0).isChecked()
if not visibility:
continue
# print(f"Row {row_index} visibility changed to {state == Qt.Checked}")
if self.comboBox_mode.currentIndex() == 1: # start/stop mode
x_start = float(table.item(row, 4).text()) if table.item(row, 4) else None
y_start = float(table.item(row, 5).text()) if table.item(row, 5) else None
x_end = float(table.item(row, 6).text()) if table.item(row, 6) else None
y_end = float(table.item(row, 7).text()) if table.item(row, 7) else None
self.saved_point_visibility[row_index] = state == Qt.Checked
if x_start is not None and y_start is not None:
start_points.append([x_start, y_start])
print(f"added start points:{start_points}")
if x_end is not None and y_end is not None:
end_points.append([x_end, y_end])
print(f"added end points:{end_points}")
# Generate brushes based on visibility state
brushes = [
pg.mkBrush(255, 165, 0, 255) if visible else pg.mkBrush(255, 165, 0, 0)
for visible in self.saved_point_visibility
]
else: # individual mode
x_ind = float(table.item(row, 3).text()) if table.item(row, 3) else None
y_ind = float(table.item(row, 4).text()) if table.item(row, 4) else None
if x_ind is not None and y_ind is not None:
individual_points.append([x_ind, y_ind])
print(f"added individual points:{individual_points}")
# brushed_rgb = [brush.color().getRgb() for brush in brushes]
if start_points:
self.saved_motor_map_start.setData(pos=np.array(start_points))
print("plotted start")
if end_points:
self.saved_motor_map_end.setData(pos=np.array(end_points))
print("plotted end")
if individual_points:
self.saved_motor_map_individual.setData(pos=np.array(individual_points))
print("plotted individual")
# print(f"Poinst: {self.saved_motor_positions}")
# print(f"Brushes: {brushed_rgb}")
# TODO will be adapted with logic to handle start/end points
def draw_rectangles(self, start_points, end_points):
for start, end in zip(start_points, end_points):
self.draw_rectangle(start, end)
self.saved_motor_map.setData(pos=self.saved_motor_positions, brush=brushes)
def update_saved_coordinates(self):
"""
Update the saved coordinates and replot them.
"""
rows = self.tableWidget_coordinates.rowCount()
# Initialize an empty array to hold new coordinates
new_saved_positions = np.empty((0, 2))
new_visibility = []
for row in range(rows):
x = (
float(self.tableWidget_coordinates.item(row, 2).text())
if self.tableWidget_coordinates.item(row, 2) is not None
else None
)
y = (
float(self.tableWidget_coordinates.item(row, 3).text())
if self.tableWidget_coordinates.item(row, 3) is not None
else None
)
# Only add the point if both x and y are not None
if x is not None and y is not None:
new_saved_positions = np.vstack((new_saved_positions, [x, y]))
checkbox = self.tableWidget_coordinates.cellWidget(row, 1)
new_visibility.append(checkbox.isChecked())
# Update saved positions and visibility
self.saved_motor_positions = new_saved_positions
self.saved_point_visibility = new_visibility
# Replot saved positions based on new data
brushes = [
pg.mkBrush(255, 165, 0, 255) if visible else pg.mkBrush(255, 165, 0, 0)
for visible in self.saved_point_visibility
]
self.saved_motor_map.setData(pos=self.saved_motor_positions, brush=brushes)
def draw_rectangle(self, start, end):
pass
def delete_selected_row(self):
selected_rows = self.tableWidget_coordinates.selectionModel().selectedRows()
rows_to_delete = [row.row() for row in selected_rows]
rows_to_delete.sort(reverse=True) # Sort in descending order
# Remove the row from the table
for row_index in rows_to_delete:
self.saved_motor_positions = np.delete(self.saved_motor_positions, row_index, axis=0)
del self.saved_point_visibility[row_index]
# Update the plot
brushes = [
pg.mkBrush(255, 165, 0, 255) if visible else pg.mkBrush(255, 165, 0, 0)
for visible in self.saved_point_visibility
]
self.saved_motor_map.setData(pos=self.saved_motor_positions, brush=brushes)
# Remove the row from the table
self.tableWidget_coordinates.removeRow(row_index)
# Update the 'Go' buttons
for row in range(self.tableWidget_coordinates.rowCount()):
button = self.tableWidget_coordinates.cellWidget(row, 0)
button.clicked.disconnect()
button.clicked.connect(
partial(self.move_to_row_coordinates, self.tableWidget_coordinates, row)
)
# Replot the saved motor map
self.replot_based_on_table(self.tableWidget_coordinates)
def resizeTable(self, table):
table.resizeColumnsToContents()
def export_table_to_csv(self, table: QtWidgets.QTableWidget):
options = QFileDialog.Options()
@@ -721,9 +905,11 @@ class MotorApp(QWidget):
with open(filePath, mode="w", newline="") as file:
writer = csv.writer(file)
col_offset = 2 if self.comboBox_mode.currentIndex() == 0 else 3
# Write the header
header = []
for col in range(2, table.columnCount()):
for col in range(col_offset, table.columnCount()):
header_item = table.horizontalHeaderItem(col)
header.append(header_item.text() if header_item else "")
writer.writerow(header)
@@ -731,7 +917,7 @@ class MotorApp(QWidget):
# Write the content
for row in range(table.rowCount()):
row_data = []
for col in range(2, table.columnCount()):
for col in range(col_offset, table.columnCount()):
item = table.item(row, col)
row_data.append(item.text() if item else "")
writer.writerow(row_data)
@@ -750,48 +936,26 @@ class MotorApp(QWidget):
# Wipe the current table
table.setRowCount(0)
# Dynamically update self.extra_columns
new_extra_columns = []
for col_name in header:
if col_name not in ["X", "Y", "Tag"]:
new_extra_columns.append({col_name: ""})
self.extra_columns = new_extra_columns
# Set column count and headers
table.setColumnCount(5 + len(self.extra_columns))
new_headers = ["Button", "Checkbox", "X", "Y", "Tag"] + [
col for col in header if col not in ["X", "Y", "Tag"]
]
for index, col_name in enumerate(new_headers):
header_item = QtWidgets.QTableWidgetItem(col_name)
header_item.setTextAlignment(Qt.AlignCenter)
table.setHorizontalHeaderItem(index, header_item)
# Populate data
for row_data in reader:
current_row = table.rowCount()
table.insertRow(current_row)
tag = row_data[0]
button = QtWidgets.QPushButton("Go")
checkBox = QtWidgets.QCheckBox()
checkBox.setChecked(True)
if self.comboBox_mode.currentIndex() == 0: # Individual mode
x = float(row_data[1])
y = float(row_data[2])
self.generate_table_coordinate(table, (x, y), tag, precision)
button.clicked.connect(
partial(self.move_to_row_coordinates, table, current_row)
)
checkBox.stateChanged.connect(
lambda state, widget=checkBox: self.toggle_point_visibility(state, widget)
)
elif self.comboBox_mode.currentIndex() == 1: # Start/Stop mode
x_start = float(row_data[1])
y_start = float(row_data[2])
x_end = float(row_data[3])
y_end = float(row_data[4])
table.setCellWidget(current_row, 0, button)
table.setCellWidget(current_row, 1, checkBox)
self.generate_table_coordinate(table, (x_start, y_start), tag, precision)
self.generate_table_coordinate(table, (x_end, y_end), tag, precision)
# Populate data
for col, data in enumerate(row_data):
item = QtWidgets.QTableWidgetItem(data)
item.setTextAlignment(Qt.AlignCenter)
table.setItem(current_row, col + 2, item)
table.resizeColumnsToContents()
if self.checkBox_resize_auto.isChecked():
table.resizeColumnsToContents()
def save_absolute_coordinates(self):
self.generate_table_coordinate(
@@ -884,10 +1048,18 @@ class MotorApp(QWidget):
layout.addWidget(QLabel("Import/Export of Table:"))
layout.addWidget(
QLabel(
"When importing a table, the first three columns must be X, Y, and Tag. Failing to do so will break the table."
"Create additional table columns in config yaml file.\n"
"Be sure to load the correct config file with console argument -c.\n"
"When importing a table, the first three columns must be [Tag, X, Y] in the case of Individual mode \n"
"and [Tag, X [start], Y [start], X [end], Y [end] in the case of Start/Stop mode.\n"
"Failing to do so will break the table!"
)
)
layout.addWidget(
QLabel(
"Note: Importing a table will overwrite the current table. Import in correct mode."
)
)
layout.addWidget(QLabel("Note: Importing a table will overwrite the current table."))
# Another Separator
another_separator = QFrame()
@@ -1156,7 +1328,6 @@ if __name__ == "__main__":
selected_motors = config.get("selected_motors", {})
plot_motors = config.get("plot_motors", {})
# extra_columns = config.get("plot_motors", {}).get("extra_columns", [])
except FileNotFoundError:
print(f"The file {args.config} was not found.")

View File

@@ -274,6 +274,12 @@ class BasicPlot(QtWidgets.QWidget):
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:
@@ -360,7 +366,11 @@ class BasicPlot(QtWidgets.QWidget):
@pyqtSlot(dict, dict)
def on_dap_update(self, data: dict, metadata: dict):
self.img.setImage(data["z"])
data_test = data
flipped_data = self.flip_even_rows(data["z"])
self.img.setImage(flipped_data)
@pyqtSlot(dict)
def new_proj(self, data):
@@ -394,15 +404,11 @@ if __name__ == "__main__":
value = parser.parse_args()
print(f"Plotting signals for: {', '.join(value.signals)}")
client = bec_dispatcher.client
# client.start()
app = QtWidgets.QApplication([])
ctrl_c.setup(app)
plot = BasicPlot(y_value_list=value.signals)
# bec_dispatcher.connect(plot)
bec_dispatcher.connect_proj_id(plot.new_proj)
bec_dispatcher.connect_dap_slot(plot.on_dap_update, "px_dap_worker")
plot.roi_signal.connect(lambda x: print(f"signal from ROI {x}"))
plot.roi_signal.connect(lambda x: bec_dispatcher.getStuff(x))
plot.show()
# client.callbacks.register("scan_segment", plot, sync=False)
app.exec_()

View File

@@ -1,10 +1,10 @@
from setuptools import setup
__version__ = "0.23.0"
__version__ = "0.27.0"
if __name__ == "__main__":
setup(
install_requires=["pyqt5", "pyqtgraph", "bec_lib"],
install_requires=["pyqt5", "pyqtgraph", "bec_lib", "zmq", "h5py"],
extras_require={"dev": ["pytest", "pytest-random-order", "coverage", "pytest-qt", "black"]},
version=__version__,
)