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 | Date | |
|---|---|---|---|
|
|
93f21eefd7 | ||
|
|
44cc881ac9 | ||
|
|
ee3cae6472 | ||
|
|
b78152b149 | ||
|
|
85841cdf1f | ||
|
|
ed3f656d5e | ||
|
|
a4fb6bd1d2 | ||
|
|
05f48de3f1 | ||
|
|
401fec8539 | ||
|
|
b13509e9eb | ||
|
|
a15860abac | ||
|
|
673ed325d1 | ||
|
|
63f52fc841 | ||
|
|
200e8b2351 | ||
|
|
e4f23f5101 | ||
|
|
b41d63ea4d | ||
|
|
418480f1fc | ||
|
|
6955b6e292 | ||
|
|
abe35bf967 | ||
|
|
174ab8fd8b | ||
|
|
7ff72b4086 | ||
|
|
cb144c7c2c | ||
|
|
5f3d55b760 | ||
|
|
a6940235be | ||
|
|
4287ac8885 | ||
|
|
08f508f4c3 | ||
|
|
6124eab971 | ||
|
|
65b045e1a2 | ||
|
|
bd28aa0361 | ||
|
|
a5c6ffaa02 | ||
|
|
34c785b92c | ||
|
|
7ad1cb47f3 | ||
|
|
7cb56e9e7f | ||
|
|
4fabee69d8 | ||
|
|
230ccba909 | ||
|
|
b867f25c78 | ||
|
|
9b715c69c0 | ||
|
|
cacc076959 | ||
|
|
7df7aadea8 | ||
|
|
56e619d239 | ||
|
|
0e634ee2ac | ||
|
|
19746c0b76 | ||
|
|
7b844c805d | ||
|
|
723503851b | ||
|
|
57e69907d5 | ||
|
|
f03dac0167 | ||
|
|
8ff983f16e | ||
|
|
10ccf0cc97 | ||
|
|
c510f4eb63 | ||
|
|
12b46a71a2 | ||
|
|
8d860ec3d1 | ||
|
|
265744076c | ||
|
|
2123361ada | ||
|
|
f2fde2cf5c | ||
|
|
14a0c92fb9 | ||
|
|
702e758812 | ||
|
|
63e3896725 | ||
|
|
ddaafa6a04 | ||
|
|
f79a143417 | ||
|
|
3b12f1bc1d | ||
|
|
a7934d58d8 | ||
|
|
ae040727fc |
115
CHANGELOG.md
115
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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_())
|
||||
@@ -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__":
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
91
bec_widgets/examples/extreme/config_scans_example.yaml
Normal file
91
bec_widgets/examples/extreme/config_scans_example.yaml
Normal 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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
17
bec_widgets/examples/motor_movement/csaxs_config.yaml
Normal file
17
bec_widgets/examples/motor_movement/csaxs_config.yaml
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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_()
|
||||
|
||||
4
setup.py
4
setup.py
@@ -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__,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user