1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-01-01 03:21:19 +01:00

feat: multi window interface created for extreme BL

This commit is contained in:
wyzula-jan
2023-08-30 13:06:01 +02:00
parent a9f16884b0
commit 69c38d67e4
4 changed files with 343 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
xy_pairs: [["samx", ["gauss_bpm", "gauss_adc1"]],
["samx", ["gauss_adc1", "gauss_adc2"]],
["samx", ["gauss_adc2"]]]

View File

@@ -0,0 +1,282 @@
import os
import numpy as np
import pyqtgraph as pg
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QApplication, QWidget, QTableWidgetItem, QTableWidget
from pyqtgraph import mkBrush, mkColor, mkPen
from pyqtgraph.Qt import QtCore, uic
from bec_lib.core import MessageEndpoints
from bec_widgets.qt_utils import Crosshair
# TODO implement:
# - implement scanID database for visualizing previous scans
# - change how dap is handled in bec_dispatcher to handle more workers
class PlotApp(QWidget):
"""
Main class for the PlotApp used to plot two signals from the BEC.
Attributes:
update_signal (pyqtSignal): Signal to trigger plot updates.
update_dap_signal (pyqtSignal): Signal to trigger DAP updates.
Args:
x_value (str): The x device/signal for plotting.
y_values (list of str): List of y device/signals for plotting.
dap_worker (str, optional): DAP process to specify. Set to None to disable.
parent (QWidget, optional): Parent widget.
"""
update_signal = pyqtSignal()
update_dap_signal = pyqtSignal()
def __init__(self, xy_pairs, parent=None):
super(PlotApp, self).__init__(parent)
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "extreme.ui"), self)
# xy pairs for setting number of windows
self.xy_pairs = xy_pairs
# Nested dictionary to hold x and y data for multiple plots
self.data = {}
self.crosshairs = None
self.plots = None
self.curves_data = None
self.grid_coordinates = None
self.scanID = None
# Initialize the UI
self.init_ui()
self.init_curves()
# Connect the update signal to the update plot method
self.proxy_update_plot = pg.SignalProxy(
self.update_signal, rateLimit=25, slot=self.update_plot
)
def init_ui(self) -> None:
"""
Initialize the UI components, create plots and store their grid positions.
This method initializes a dictionary `self.plots` to store the plot objects
along with their corresponding x and y signal names. It also keeps track of
the row and column grid positions for each plot in `self.grid_coordinates`.
"""
self.plots = {}
self.grid_coordinates = [] # List to keep track of grid positions for each plot
for i, (x, ys) in enumerate(self.xy_pairs):
row, col = i // 2, i % 2 # Change these numbers based on your grid layout
plot = self.glw.addPlot(row=row, col=col)
plot.setLabel("bottom", x)
plot.setLabel("left", ", ".join(ys))
plot.addLegend()
self.plots[(x, tuple(ys))] = plot
self.grid_coordinates.append((row, col)) # Store the grid position
self.splitter.setSizes([200, 100])
def init_curves(self) -> None:
"""
Initialize curve data and properties, and update table row labels.
This method initializes a nested dictionary `self.curves_data` to store
the curve objects for each x and y signal pair. It also updates the row labels
in `self.tableWidget_crosshair` to include the grid position for each y-value.
"""
self.curves_data = {} # Nested dictionary to hold curves
row_labels = [] # List to keep track of row labels for the table
for idx, ((x, ys), plot) in enumerate(self.plots.items()):
plot.clear()
self.curves_data[(x, tuple(ys))] = []
colors_ys = PlotApp.golden_angle_color(colormap="CET-R2", num=len(ys))
row, col = self.grid_coordinates[idx] # Retrieve the grid position for this plot
for i, (signal, color) in enumerate(zip(ys, colors_ys)):
pen_curve = mkPen(color=color, width=2, style=QtCore.Qt.DashLine)
brush_curve = mkBrush(color=color)
curve_data = pg.PlotDataItem(
symbolSize=5,
symbolBrush=brush_curve,
pen=pen_curve,
skipFiniteCheck=True,
name=f"{signal}",
)
self.curves_data[(x, tuple(ys))].append(curve_data)
plot.addItem(curve_data)
row_labels.append(f"{signal} - [{row},{col}]") # Add row label with grid position
self.tableWidget_crosshair.setRowCount(len(row_labels))
self.tableWidget_crosshair.setVerticalHeaderLabels(row_labels)
self.hook_crosshair()
def hook_crosshair(self):
"""Attach crosshairs to each plot and connect them to the update_table method."""
self.crosshairs = {} # Store crosshairs for each plot
for (x, ys), plot in self.plots.items():
crosshair = Crosshair(plot, precision=3)
crosshair.coordinatesChanged1D.connect(
lambda x, y, plot=plot: self.update_table(
self.tableWidget_crosshair, x, y, column=0, plot=plot
)
)
crosshair.coordinatesClicked1D.connect(
lambda x, y, plot=plot: self.update_table(
self.tableWidget_crosshair, x, y, column=1, plot=plot
)
)
self.crosshairs[(x, tuple(ys))] = crosshair
def update_table(
self, table_widget: QTableWidget, x: float, y_values: list, column: int, plot: pg.PlotItem
) -> None:
"""
Update the table with coordinates based on cursor movements and clicks.
Args:
table_widget (QTableWidget): The table to be updated.
x (float): The x-coordinate from the plot.
y_values (list): The y-coordinates from the plot.
column (int): The column in the table to be updated.
plot (PlotItem): The plot from which the coordinates are taken.
This method calculates the correct row in the table for each y-value
and updates the cell at (row, column) with the new x and y coordinates.
"""
plot_key = [key for key, value in self.plots.items() if value == plot][0]
_, ys = plot_key # Extract the y-values for the current plot
# Find the starting row for the ys of the current plot
starting_row = 0
for _, other_ys in self.xy_pairs:
if other_ys == list(ys):
break
starting_row += len(other_ys)
# Update the table rows corresponding to the ys of the current plot
for i, y in enumerate(y_values):
row = starting_row + i
table_widget.setItem(row, column, QTableWidgetItem(f"({x}, {y})"))
table_widget.resizeColumnsToContents()
def update_plot(self) -> None:
"""Update the plot data based on the stored data dictionary."""
for (x, ys), curves in self.curves_data.items():
data_x = self.data.get((x, tuple(ys)), {}).get("x", [])
for i, curve in enumerate(curves):
data_y = self.data.get((x, tuple(ys)), {}).get(ys[i], [])
curve.setData(data_x, data_y)
@pyqtSlot(dict, dict)
def on_scan_segment(self, msg, metadata) -> None:
"""
Handle new scan segments and saves data to a dictionary.
Args:
msg (dict): Message received with scan data.
metadata (dict): Metadata of the scan.
"""
current_scanID = msg.get("scanID", None)
if current_scanID is None:
return
if current_scanID != self.scanID:
self.scanID = current_scanID
self.data = {} # Wipe the data for a new scan
self.init_curves() # Re-initialize the curves
for x, ys in self.xy_pairs:
data_x = msg["data"].get(x, {}).get(x, {}).get("value", None)
if data_x is not None:
self.data.setdefault((x, tuple(ys)), {}).setdefault("x", []).append(data_x)
for y in ys:
data_y = msg["data"].get(y, {}).get(y, {}).get("value", None)
if data_y is not None:
self.data.setdefault((x, tuple(ys)), {}).setdefault(y, []).append(data_y)
self.update_signal.emit()
@staticmethod
def golden_ratio(num: int) -> list:
"""Calculate the golden ratio for a given number of angles.
Args:
num (int): Number of angles
"""
phi = 2 * np.pi * ((1 + np.sqrt(5)) / 2)
angles = []
for ii in range(num):
x = np.cos(ii * phi)
y = np.sin(ii * phi)
angle = np.arctan2(y, x)
angles.append(angle)
return angles
@staticmethod
def golden_angle_color(colormap: str, num: int) -> list:
"""
Extract num colors for from the specified colormap following golden angle distribution.
Args:
colormap (str): Name of the colormap
num (int): Number of requested colors
Returns:
list: List of colors with length <num>
Raises:
ValueError: If the number of requested colors is greater than the number of colors in the colormap.
"""
cmap = pg.colormap.get(colormap)
cmap_colors = cmap.color
if num > len(cmap_colors):
raise ValueError(
f"Number of colors requested ({num}) is greater than the number of colors in the colormap ({len(cmap_colors)})"
)
angles = PlotApp.golden_ratio(len(cmap_colors))
color_selection = np.round(np.interp(angles, (-np.pi, np.pi), (0, len(cmap_colors))))
colors = [
mkColor(tuple((cmap_colors[int(ii)] * 255).astype(int))) for ii in color_selection[:num]
]
return colors
if __name__ == "__main__":
import yaml
from bec_widgets import ctrl_c
from bec_widgets.bec_dispatcher import bec_dispatcher
with open("config.yaml", "r") as file:
config = yaml.safe_load(file)
xy_pairs = config["xy_pairs"]
# BECclient global variables
client = bec_dispatcher.client
client.start()
dev = client.device_manager.devices
scans = client.scans
queue = client.queue
app = QApplication([])
plotApp = PlotApp(xy_pairs=xy_pairs)
# Connecting signals from bec_dispatcher
bec_dispatcher.connect_slot(plotApp.on_scan_segment, MessageEndpoints.scan_segment())
ctrl_c.setup(app)
window = plotApp
window.show()
app.exec_()

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MultiWindow</class>
<widget class="QWidget" name="MultiWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1248</width>
<height>564</height>
</rect>
</property>
<property name="windowTitle">
<string>MultiWindow</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="GraphicsLayoutWidget" name="glw"/>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Cursor</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTableWidget" name="tableWidget_crosshair">
<column>
<property name="text">
<string>Moved</string>
</property>
</column>
<column>
<property name="text">
<string>Clicked</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>GraphicsLayoutWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -17,6 +17,7 @@ from bec_lib.core import MessageEndpoints
# TODO implement:
# - implement scanID database for visualizing previous scans
# - multiple signals for different monitors
# - change how dap is handled in bec_dispatcher to handle more workers
class PlotApp(QWidget):