0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

refactor(plots/image): image logic moved to BECImageItem, image updated from bec_dispatcher with register_stream fetching data from dispatcher

This commit is contained in:
wyzula-jan
2024-02-27 18:17:14 +01:00
parent 7ffedd9ceb
commit a21bfec3d9
9 changed files with 456 additions and 430 deletions

View File

@ -385,6 +385,16 @@ class BECFigure(RPCBase, BECFigureClientMixin):
Clear all widgets from the figure and reset to default state
"""
@rpc_call
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
"""
Get the configuration of the widget.
Args:
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
Returns:
dict: The configuration of the plot widget.
"""
class BECCurve(RPCBase):
@rpc_call
@ -468,70 +478,65 @@ class BECCurve(RPCBase):
class BECImageShow(RPCBase):
@rpc_call
def set_vrange(self, vmin: "float", vmax: "float"):
"""
None
"""
@rpc_call
def set_monitor(self, monitor: "str" = None) -> "None":
"""
Set/update monitor device.
Args:
monitor(str): Name of the monitor.
"""
@rpc_call
def set_color_map(self, cmap: "str" = "magma"):
"""
None
"""
@rpc_call
def set_image(self, data: "np.ndarray"):
"""
Set the image to be displayed.
Args:
data(np.ndarray): The image to be displayed.
"""
@rpc_call
def set_processing(
def add_monitor_image(
self,
fft: "bool" = False,
log: "bool" = False,
rotation: "int" = None,
transpose: "bool" = False,
monitor: "str",
color_map: "Optional[str]" = "magma",
color_bar: "Optional[Literal['simple', 'full']]" = "simple",
downsample: "Optional[bool]" = True,
opacity: "Optional[float]" = 1.0,
vrange: "Optional[tuple[int, int]]" = None,
**kwargs
) -> "BECImageItem":
"""
None
"""
@rpc_call
def add_custom_image(
self,
name: "str",
data: "Optional[np.ndarray]" = None,
color_map: "Optional[str]" = "magma",
color_bar: "Optional[Literal['simple', 'full']]" = "simple",
downsample: "Optional[bool]" = True,
opacity: "Optional[float]" = 1.0,
vrange: "Optional[tuple[int, int]]" = None,
**kwargs
):
"""
Set the processing of the monitor data.
None
"""
@rpc_call
def set_vrange(self, vmin: "float", vmax: "float", name: "str" = None):
"""
Set the range of the color bar.
If name is not specified, then set vrange for all images.
Args:
fft(bool): Whether to perform FFT on the monitor data.
log(bool): Whether to perform log on the monitor data.
rotation(int): The rotation angle of the monitor data before displaying.
transpose(bool): Whether to transpose the monitor data before displaying.
vmin(float): Minimum value of the color bar.
vmax(float): Maximum value of the color bar.
name(str): The name of the image.
"""
@rpc_call
def enable_fft(self, enable: "bool" = True):
def set_color_map(self, cmap: "str", name: "str" = None):
"""
None
Set the color map of the image.
If name is not specified, then set color map for all images.
Args:
cmap(str): The color map of the image.
name(str): The name of the image.
"""
@rpc_call
def enable_log(self, enable: "bool" = True):
"""
None
"""
class BECConnector(RPCBase):
@rpc_call
def rotate(self, angle: "int"):
def get_config(self, dict_output: "bool" = True) -> "dict | BaseModel":
"""
None
"""
@rpc_call
def transpose(self):
"""
None
Get the configuration of the widget.
Args:
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
Returns:
dict: The configuration of the plot widget.
"""

View File

@ -2,11 +2,13 @@ import importlib
import select
import subprocess
import uuid
from functools import wraps
from bec_lib import MessageEndpoints, messages
from qtpy.QtCore import QCoreApplication
import bec_widgets.cli.client as client
from bec_lib import MessageEndpoints, messages
from bec_widgets.utils.bec_dispatcher import BECDispatcher
@ -104,7 +106,7 @@ class RPCBase:
"""
parent = self
# pylint: disable=protected-access
while not parent._parent is None:
while parent._parent is not None:
parent = parent._parent
return parent
@ -130,7 +132,7 @@ class RPCBase:
print(f"RPCBase: {rpc_msg}")
# pylint: disable=protected-access
receiver = self._root._gui_id
self._client.producer.send(MessageEndpoints.gui_instructions(receiver), rpc_msg)
self._client.connector.send(MessageEndpoints.gui_instructions(receiver), rpc_msg)
if not wait_for_rpc_response:
return None
@ -166,7 +168,8 @@ class RPCBase:
"""
response = None
while response is None:
response = self._client.producer.get(
response = self._client.connector.get(
MessageEndpoints.gui_instruction_response(request_id)
)
QCoreApplication.processEvents() # keep UI responsive (and execute signals/slots)
return response

View File

@ -109,10 +109,11 @@ if __name__ == "__main__": # pragma: no cover
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.plots import BECPlotBase, BECWaveform1D, BECImageShow # ,BECCurve
from bec_widgets.widgets.plots.waveform1d import BECCurve
from bec_widgets.utils import BECConnector
current_path = os.path.dirname(__file__)
client_path = os.path.join(current_path, "client.py")
clss = [BECPlotBase, BECWaveform1D, BECFigure, BECCurve, BECImageShow]
clss = [BECPlotBase, BECWaveform1D, BECFigure, BECCurve, BECImageShow, BECConnector]
generator = ClientGenerator()
generator.generate_client(clss)
generator.write(client_path)

View File

@ -27,25 +27,22 @@ class BECWidgetsCLIServer:
"""Start the figure window."""
self.fig.start()
@staticmethod
def _rpc_update_handler(msg, parent):
parent.on_rpc_update(msg.value)
def on_rpc_update(self, msg: dict, metadata: dict):
request_id = metadata.get("request_id")
try:
method = msg["action"]
args = msg["parameter"].get("args", [])
kwargs = msg["parameter"].get("kwargs", {})
request_id = metadata.get("request_id")
obj = self.get_object_from_config(msg["parameter"])
res = self.run_rpc(obj, method, args, kwargs)
self.send_response(request_id, True, {"result": res})
except Exception as e:
print(e)
self.send_response(request_id, False, {"error": str(e)})
else:
self.send_response(request_id, True, {"result": res})
def send_response(self, request_id: str, accepted: bool, msg: dict):
self.client.producer.set(
self.client.connector.set(
MessageEndpoints.gui_instruction_response(request_id),
messages.RequestResponseMessage(accepted=accepted, message=msg),
expire=60,

View File

@ -31,6 +31,8 @@ class ConnectionConfig(BaseModel):
class BECConnector:
"""Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
USER_ACCESS = ["get_config"]
def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
# BEC related connections
self.bec_dispatcher = BECDispatcher()

View File

@ -13,6 +13,7 @@ from pydantic import Field
from pyqtgraph.Qt import uic
from qtpy.QtWidgets import QApplication, QWidget
from qtpy.QtWidgets import QVBoxLayout, QMainWindow
from qtpy.QtCore import Signal as pyqtSignal
from bec_widgets.utils import BECConnector, BECDispatcher, ConnectionConfig
from bec_widgets.widgets.plots import (
@ -102,8 +103,11 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
"change_layout",
"change_theme",
"clear_all",
"get_config",
]
clean_signal = pyqtSignal()
def __init__(
self,
parent: Optional[QWidget] = None,
@ -128,16 +132,6 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
# Container to keep track of the grid
self.grid = []
def change_theme(self, theme: Literal["dark", "light"]) -> None:
"""
Change the theme of the figure widget.
Args:
theme(Literal["dark","light"]): The theme to set for the figure widget.
"""
qdarktheme.setup_theme(theme)
self.setBackground("k" if theme == "dark" else "w")
self.config.theme = theme
def add_plot(
self, widget_id: str = None, row: int = None, col: int = None, config=None, **axis_kwargs
) -> BECWaveform1D:
@ -250,36 +244,46 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
else:
image = self.add_image(color_bar=color_bar, **axis_kwargs)
# Setting appearance
if vrange is not None:
image.set_vrange(vmin=vrange[0], vmax=vrange[1])
if color_map is not None:
image.set_color_map(color_map)
# Setting data
# Setting data #TODO check logic if monitor or data are already created
if monitor is not None and data is None:
image.set_monitor(monitor)
image.add_monitor_image(
monitor=monitor, color_map=color_map, vrange=vrange, color_bar=color_bar
)
elif data is not None and monitor is None:
image.set_image(data)
image.add_custom_image(
name="custom", data=data, color_map=color_map, vrange=vrange, color_bar=color_bar
)
elif data is None and monitor is None:
# Setting appearance
if vrange is not None:
image.set_vrange(vmin=vrange[0], vmax=vrange[1])
if color_map is not None:
image.set_color_map(color_map)
return image
else:
raise ValueError("Invalid input. Provide either monitor name or custom data.")
return image
def add_image(
self,
widget_id: str = None,
row: int = None,
col: int = None,
config=None,
color_map: str = "magma",
color_map: str = "magma", # TODO fix passing additional kwargs
color_bar: Literal["simple", "full"] = "full",
vrange: tuple[float, float] = None,
**axis_kwargs,
) -> BECImageShow:
if config is None:
# config = ImageConfig(color_map=color_map, color_bar=color_bar, vrange=vrange)
config = ImageConfig(color_map=color_map, color_bar=color_bar, vrange=vrange)
widget_id = self._generate_unique_widget_id()
config = ImageConfig(
widget_class="BECImageShow",
gui_id=widget_id,
parent_id=self.gui_id,
color_map=color_map,
color_bar=color_bar,
vrange=vrange,
)
return self.add_widget(
widget_type="ImShow",
widget_id=widget_id,
@ -377,6 +381,16 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
else:
raise ValueError("Must provide either widget_id or coordinates for removal.")
def change_theme(self, theme: Literal["dark", "light"]) -> None:
"""
Change the theme of the figure widget.
Args:
theme(Literal["dark","light"]): The theme to set for the figure widget.
"""
qdarktheme.setup_theme(theme)
self.setBackground("k" if theme == "dark" else "w")
self.config.theme = theme
def _find_first_widget_by_class(
self, widget_class: Type[BECPlotBase], can_fail: bool = True
) -> BECPlotBase | None:
@ -561,16 +575,17 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
)
def cleanup(self):
"""Cleanup the figure widget."""
self.clear_all()
self.close()
# def cleanup(self):
# """Cleanup the figure widget."""
# self.clear_all()
# self.clean_signal.emit()
def start(self):
import sys
app = QApplication(sys.argv)
win = BECFigureMainWindow(bec_figure=self)
win = QMainWindow()
win.setCentralWidget(self)
win.show()
sys.exit(app.exec_())
@ -583,9 +598,18 @@ class BECFigureMainWindow(QMainWindow):
self.figure = bec_figure
self.setCentralWidget(self.figure)
self.figure.clean_signal.connect(self.confirm_close)
self.safe_close = False
def confirm_close(self):
self.safe_close = True
def closeEvent(self, event):
self.figure.cleanup()
super().closeEvent(event)
if self.safe_close == True:
print("Safe close")
event.accept()
##################################################
@ -627,15 +651,17 @@ class DebugWindow(QWidget): # pragma: no cover:
self._init_ui()
self.splitter.setSizes([200, 100])
self.safe_close = False
# self.figure.clean_signal.connect(self.confirm_close)
# console push
self.console.kernel_manager.kernel.shell.push(
{
"fig": self.figure,
"w1": self.w1,
"w2": self.w2,
"w3": self.w3,
"w4": self.w4,
# "w1": self.w1,
# "w2": self.w2,
# "w3": self.w3,
# "w4": self.w4,
"bec": self.figure.client,
"scans": self.figure.client.scans,
"dev": self.figure.client.device_manager.devices,
@ -649,7 +675,7 @@ class DebugWindow(QWidget): # pragma: no cover:
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
# add stuff to figure
self._init_figure()
# self._init_figure()
self.console_layout = QVBoxLayout(self.widget_console)
self.console = JupyterConsoleWidget()
@ -708,19 +734,20 @@ class DebugWindow(QWidget): # pragma: no cover:
# )
# Image setting for w3
self.w3.set_monitor("eiger")
# self.w3.add_color_bar("simple")
self.w3.add_monitor_image("eiger", vrange=(0, 100), color_bar="full")
# Image setting for w4
self.w4.add_monitor_image("eiger", vrange=(0, 100), color_map="viridis")
self.w4.set_monitor("eiger")
# self.w4.add_color_bar("full")
def closeEvent(self, event):
self.figure.cleanup()
self.close()
super().closeEvent(event)
# def confirm_close(self):
# self.safe_close = True
#
# def closeEvent(self, event):
# self.figure.cleanup()
# if self.safe_close == True:
# print("Safe close")
# event.accept()
if __name__ == "__main__": # pragma: no cover

View File

@ -1,23 +1,22 @@
from __future__ import annotations
import time
from typing import Literal, Optional
from collections import defaultdict
from typing import Literal, Optional, Any
import numpy as np
import pyqtgraph as pg
from pydantic import Field, BaseModel
from qtpy.QtCore import QThread
from qtpy.QtCore import QThread, QObject
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_lib import MessageEndpoints, RedisConnector
from bec_widgets.utils import ConnectionConfig, BECConnector, BECDispatcher
from bec_lib import MessageEndpoints
from bec_widgets.utils import ConnectionConfig, BECConnector
from bec_widgets.widgets.plots import BECPlotBase, WidgetConfig
class MonitorConfig(BaseModel):
monitor: Optional[str] = Field(None, description="The name of the monitor.")
class PostProcessingConfig(BaseModel):
fft: Optional[bool] = Field(False, description="Whether to perform FFT on the monitor data.")
log: Optional[bool] = Field(False, description="Whether to perform log on the monitor data.")
center_of_mass: Optional[bool] = Field(
@ -30,43 +29,38 @@ class MonitorConfig(BaseModel):
None, description="The rotation angle of the monitor data before displaying."
)
# TODO Decide if usefully to include port and host
host: Optional[str] = Field(None, description="The host of the monitor.")
port: Optional[int] = Field(None, description="The port of the monitor.")
class ImageItemConfig(ConnectionConfig):
# color_map: Optional[str] = Field("magma", description="The color map of the image.")
source: Optional[str] = Field(None, description="The source of the curve.")
signals: MonitorConfig = Field(
default_factory=MonitorConfig, description="The configuration of the monitor."
monitor: Optional[str] = Field(None, description="The name of the monitor.")
color_map: Optional[str] = Field("magma", description="The color map of the image.")
downsample: Optional[bool] = Field(True, description="Whether to downsample the image.")
opacity: Optional[float] = Field(1.0, description="The opacity of the image.")
vrange: Optional[tuple[int, int]] = Field(
None, description="The range of the color bar. If None, the range is automatically set."
)
color_bar: Optional[Literal["simple", "full"]] = Field(
"simple", description="The type of the color bar."
)
post_processing: PostProcessingConfig = Field(
default_factory=PostProcessingConfig, description="The post processing of the image."
)
class ImageConfig(WidgetConfig):
color_map: Optional[str] = Field("magma", description="The color map of the image.")
color_bar: Optional[Literal["simple", "full"]] = Field(
"simple", description="The type of the color bar."
)
vrange: Optional[tuple[int, int]] = Field(
None, description="The range of the color bar. If None, the range is automatically set."
)
images: dict[str, ImageItemConfig] = Field(
{},
description="The configuration of the images. The key is the name of the image.",
description="The configuration of the images. The key is the name of the image (source).",
)
# TODO Decide if implement or not
# x_transpose_axis:
# y_transpose_axis:
class BECImageItem(BECConnector, pg.ImageItem): # TODO decide how complex it should be
USER_ACCESS = []
USER_ACCESS = ["set", "set_color_map", "set_auto_downsample", "set_monitor", "set_vrange"]
def __init__(
self,
config: Optional[ImageItemConfig] = None,
gui_id: Optional[str] = None,
parent_image: Optional[BECImageItem] = None,
**kwargs,
):
if config is None:
@ -74,109 +68,85 @@ class BECImageItem(BECConnector, pg.ImageItem): # TODO decide how complex it sh
self.config = config
else:
self.config = config
# config.widget_class = self.__class__.__name__
super().__init__(config=config, gui_id=gui_id)
pg.ImageItem.__init__(self)
self.parent_image = parent_image
self.colorbar_bar = None
self._add_color_bar(
self.config.color_bar, self.config.vrange
) # TODO can also support None to not have any colorbar
self.apply_config()
if kwargs:
self.set(**kwargs)
def apply_config(self):
...
# self.set_color_map(self.config.color_map)
self.set_color_map(self.config.color_map)
self.set_auto_downsample(self.config.downsample)
if self.config.vrange is not None:
self.set_vrange(vrange=self.config.vrange)
# self.set_color_bar(self.config.color_bar)
def set(self, **kwargs):
pass
method_map = {
"downsample": self.set_auto_downsample,
"color_map": self.set_color_map,
"monitor": self.set_monitor,
"vrange": self.set_vrange,
}
for key, value in kwargs.items():
if key in method_map:
method_map[key](value)
else:
print(f"Warning: '{key}' is not a recognized property.")
def set_color_map(self, cmap: str = "magma"):
"""
Set the color map of the image.
Args:
cmap(str): The color map of the image.
"""
self.setColorMap(cmap)
# self.config.color_map = cmap
self.config.color_map = cmap
def set_auto_downsample(self, auto: bool = True):
"""
Set the auto downsample of the image.
Args:
auto(bool): Whether to downsample the image.
"""
self.setAutoDownsample(auto)
self.config.downsample = auto
class BECImageShow(BECPlotBase):
USER_ACCESS = [
"set_vrange",
"set_monitor",
"set_color_map",
"set_image",
"set_processing",
"enable_fft",
"enable_log",
"rotate",
"transpose",
]
def __init__(
self,
parent: Optional[QWidget] = None,
parent_figure=None,
config: Optional[WidgetConfig] = None,
client=None,
gui_id: Optional[str] = None,
monitor: Optional[str] = None,
):
if config is None:
config = ImageConfig(widget_class=self.__class__.__name__)
super().__init__(
parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
)
# Items to be added to the plot
self.image = None
self.color_bar = None
# Args to pass
self.monitor = monitor
self.scanID = None
# init image and image thread
self._init_image()
self._init_image_thread(monitor=self.monitor)
# Dispatcher
self.bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
def find_widget_by_id(self, item_id: str):
if self.image.gui_id == item_id:
return self.image
def apply_config(self): # TODO implement
...
def _init_image(self):
self.image = BECImageItem()
self.plot_item.addItem(self.image)
self.config.images["device_monitor"] = self.image.config
# customising ImageItem
self._add_color_bar(style=self.config.color_bar, vrange=self.config.vrange)
self.set_color_map(cmap=self.config.color_map)
def _init_image_thread(self, monitor: str = None):
self.monitor = monitor
self.image.config.signals.monitor = monitor
self.image_thread = ImageThread(client=self.client, monitor=monitor)
self.image_thread.config = self.image.config.signals
self.proxy_update_plot = pg.SignalProxy(
self.image_thread.image_updated, rateLimit=25, slot=self.on_image_update
)
# self.image_thread.start()
def set_monitor(self, monitor: str):
"""
Set the monitor of the image.
Args:
monitor(str): The name of the monitor.
"""
self.config.monitor = monitor
def _add_color_bar(
self, style: Literal["simple,full"] = "simple", vrange: tuple[int, int] = (0, 100)
self, color_bar_style: str = "simple", vrange: Optional[tuple[int, int]] = None
):
if style == "simple":
"""
Add color bar to the layout.
Args:
style(Literal["simple,full"]): The style of the color bar.
vrange(tuple[int,int]): The range of the color bar.
"""
if color_bar_style == "simple":
self.color_bar = pg.ColorBarItem(colorMap=self.config.color_map)
if vrange is not None:
self.color_bar.setLevels(low=vrange[0], high=vrange[1])
self.color_bar.setImageItem(self.image)
self.addItem(self.color_bar, row=0, col=1)
self.color_bar.setImageItem(self)
self.parent_image.addItem(self.color_bar) # , row=0, col=1)
self.config.color_bar = "simple"
elif style == "full":
elif color_bar_style == "full":
# Setting histogram
self.color_bar = pg.HistogramLUTItem()
self.color_bar.setImageItem(self.image)
self.color_bar.setImageItem(self)
self.color_bar.gradient.loadPreset(self.config.color_map)
if vrange is not None:
self.color_bar.setLevels(min=vrange[0], max=vrange[1])
@ -185,19 +155,24 @@ class BECImageShow(BECPlotBase):
)
# Adding histogram to the layout
self.addItem(self.color_bar, row=0, col=1)
self.parent_image.addItem(self.color_bar) # , row=0, col=1)
# save settings
self.config.color_bar = "full"
else:
raise ValueError("style should be 'simple' or 'full'")
# def color_bar_switch(self, style: Literal["simple,full"] = "simple"): #TODO check if possible
# if style == "simple" and self.config.color_bar == "full":
# self.color_bar.remove()
def set_vrange(self, vmin: float, vmax: float):
self.image.setLevels([vmin, vmax])
def set_vrange(self, vmin: float = None, vmax: float = None, vrange: tuple[int, int] = None):
"""
Set the range of the color bar.
Args:
vmin(float): Minimum value of the color bar.
vmax(float): Maximum value of the color bar.
"""
if vrange is not None:
vmin, vmax = vrange
self.setLevels([vmin, vmax])
self.config.vrange = (vmin, vmax)
if self.color_bar is not None:
if self.config.color_bar == "simple":
self.color_bar.setLevels(low=vmin, high=vmax)
@ -205,213 +180,226 @@ class BECImageShow(BECPlotBase):
self.color_bar.setLevels(min=vmin, max=vmax)
self.color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
def set_monitor(self, monitor: str = None) -> None:
"""
Set/update monitor device.
Args:
monitor(str): Name of the monitor.
"""
self.image_thread.set_monitor(monitor)
self.image.config.signals.monitor = monitor
def set_color_map(self, cmap: str = "magma"):
self.image.set_color_map(cmap)
if self.color_bar is not None:
if self.config.color_bar == "simple":
self.color_bar.setColorMap(cmap)
elif self.config.color_bar == "full":
self.color_bar.gradient.loadPreset(cmap)
# def set_zmq(self, address: str = None): # TODO to be implemented
# ...
def set_processing(
self, fft: bool = False, log: bool = False, rotation: int = None, transpose: bool = False
):
"""
Set the processing of the monitor data.
Args:
fft(bool): Whether to perform FFT on the monitor data.
log(bool): Whether to perform log on the monitor data.
rotation(int): The rotation angle of the monitor data before displaying.
transpose(bool): Whether to transpose the monitor data before displaying.
"""
self.image.config.signals.fft = fft
self.image.config.signals.log = log
self.image.config.signals.rotation = rotation
self.image.config.signals.transpose = transpose
self.image_thread.update_config(self.image.config.signals)
def enable_fft(self, enable: bool = True): # TODO enable processing of already taken images
self.image.config.signals.fft = enable
self.image_thread.update_config(self.image.config.signals)
def enable_log(self, enable: bool = True):
self.image.config.signals.log = enable
self.image_thread.update_config(self.image.config.signals)
def rotate(self, angle: int): # TODO fine tune, can be byt any angle not just by 90deg?
self.image.config.signals.rotation = angle
self.image_thread.update_config(self.image.config.signals)
def transpose(self): # TODO do enable or not?
self.image.config.signals.transpose = not self.image.config.signals.transpose
self.image_thread.update_config(self.image.config.signals)
# def enable_center_of_mass(self, enable: bool = True): #TODO check and enable
# self.image.config.signals.center_of_mass = enable
# self.image_thread.update_config(self.image.config.signals)
@pyqtSlot(np.ndarray)
def on_image_update(self, image):
# print(f"Image updated: {image.shape}")
print(f"Image updated")
self.image.updateImage(image[0])
@pyqtSlot(dict, dict)
def on_scan_segment(self, msg, metadata):
"""
Serves as a trigger to image acquisition.
Update the scanID and start the image thread if the scanID is different from the current one.
Args:
msg(dict): The content of the message.
metadata(dict): The metadata of the message.
"""
current_scanID = msg.get("scanID", None)
if current_scanID is None:
return
if current_scanID != self.scanID:
self.scanID = current_scanID
self.image_thread.start()
max_points = metadata.get("num_points", None)
current_point = msg.get("point_id") + 1
print(f"Current point: {current_point} out of {max_points}")
if current_point == max_points:
if self.image_thread.isRunning():
self.image_thread.stop()
print("image thread done")
def set_image(self, data: np.ndarray):
"""
Set the image to be displayed.
Args:
data(np.ndarray): The image to be displayed.
"""
self.imageItem.setImage(data)
if self.image_thread.isRunning():
self.image_thread.stop()
def cleanup(self): # TODO test
self.image_thread.stop()
self.image_thread.wait()
print("ImageThread stopped")
class ImageThread(QThread):
image_updated = pyqtSignal(np.ndarray)
class BECImageShow(BECPlotBase):
USER_ACCESS = ["add_monitor_image", "add_custom_image", "set_vrange", "set_color_map"]
def __init__(
self,
parent=None,
parent: Optional[QWidget] = None,
parent_figure=None,
config: Optional[ImageConfig] = None,
client=None,
monitor: str = None,
monitor_config: MonitorConfig = None,
host: str = None,
port: int | str = None,
gui_id: Optional[str] = None,
):
super().__init__(parent=parent)
if config is None:
config = ImageConfig(widget_class=self.__class__.__name__)
super().__init__(
parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
)
bec_dispatcher = BECDispatcher()
self.client = bec_dispatcher.client if client is None else client
self.dev = self.client.device_manager.devices
self.scans = self.client.scans
self.queue = self.client.queue
self.images = defaultdict(dict)
# Monitor Device
self.monitor = monitor
self.config = monitor_config
# Connection
self.host = host
self.port = str(port)
if self.host is None:
self.host = self.client.connector.host
self.port = self.client.connector.port
self.connector = RedisConnector(f"{self.host}:{self.port}")
self.running = False
if self.monitor is not None:
self.set_monitor(self.monitor)
def update_config(self, config: MonitorConfig | dict): # TODO include monitor update in config?
def find_widget_by_id(self, item_id: str) -> BECImageItem:
"""
Update the monitor configuration.
Find the widget by its gui_id.
Args:
config(MonitorConfig|dict): The new monitor configuration.
"""
if isinstance(config, dict):
config = MonitorConfig(**config)
self.config = config
item_id(str): The gui_id of the widget.
def set_monitor(self, monitor: str = None) -> None: # TODO check monitor update with the config
Returns:
BECImageItem: The widget with the given gui_id.
"""
Set/update monitor device.
for source, images in self.images.items():
for key, value in images.items():
if key == item_id and isinstance(value, BECImageItem):
return value
elif isinstance(value, dict):
result = self.find_widget_by_id(item_id)
if result is not None:
return result
def change_gui_id(self, new_gui_id: str):
"""
Change the GUI ID of the image widget and update the parent_id in all associated curves.
Args:
monitor(str): Name of the monitor.
new_gui_id (str): The new GUI ID to be set for the image widget.
"""
self.monitor = monitor
print(f"Stream consumer started for device: {monitor}")
# Update the gui_id in the waveform widget itself
self.gui_id = new_gui_id
self.config.gui_id = new_gui_id
def process_FFT(self, data: np.ndarray) -> np.ndarray: # TODO check functionality
return np.abs(np.fft.fftshift(np.fft.fft2(data)))
for source, images in self.images.items():
for id, image_item in images.items():
image_item.config.parent_id = new_gui_id
def rotation(self, data: np.ndarray, angle: int) -> np.ndarray:
return np.rot90(data, k=angle, axes=(0, 1))
def add_monitor_image(
self,
monitor: str,
color_map: Optional[str] = "magma",
color_bar: Optional[Literal["simple", "full"]] = "simple",
downsample: Optional[bool] = True,
opacity: Optional[float] = 1.0,
vrange: Optional[tuple[int, int]] = None,
# post_processing: Optional[PostProcessingConfig] = None,
**kwargs,
) -> BECImageItem:
image_source = "device_monitor"
def transpose(self, data: np.ndarray) -> np.ndarray:
return np.transpose(data)
def log(self, data: np.ndarray) -> np.ndarray:
return np.log10(np.abs(data))
# def center_of_mass(self, data: np.ndarray) -> tuple: # TODO check functionality
# return np.unravel_index(np.argmax(data), data.shape)
def post_processing(self, data: np.ndarray) -> np.ndarray:
if self.config.fft:
data = self.process_FFT(data)
if self.config.rotation is not None:
data = self.rotation(data, self.config.rotation)
if self.config.transpose:
data = self.transpose(data)
if self.config.log:
data = self.log(data)
return data
def run(self):
self.running = True
self.get_image()
def get_image(self):
if self.monitor is None:
return
while self.running:
data = self.connector.get_last(
topic=MessageEndpoints.device_monitor(device=self.monitor)
image_exits = self._check_image_id(monitor, self.images)
if image_exits:
raise ValueError(
f"Monitor with ID '{monitor}' already exists in widget '{self.gui_id}'."
)
if data is not None:
data = data.content["data"]
data = self.post_processing(data)
image_config = ImageItemConfig(
widget_class="BECImageItem",
parent_id=self.gui_id,
color_map=color_map,
color_bar=color_bar,
downsample=downsample,
opacity=opacity,
vrange=vrange,
# post_processing=post_processing,
**kwargs,
)
self.image_updated.emit(data)
print("ImageThread is running")
image = self._add_image_object(source=image_source, name=monitor, config=image_config)
self._connect_device_monitor(monitor)
return image
time.sleep(0.01)
def add_custom_image(
self,
name: str,
data: Optional[np.ndarray] = None,
color_map: Optional[str] = "magma",
color_bar: Optional[Literal["simple", "full"]] = "simple",
downsample: Optional[bool] = True,
opacity: Optional[float] = 1.0,
vrange: Optional[tuple[int, int]] = None,
# post_processing: Optional[PostProcessingConfig] = None,
**kwargs,
):
image_source = "device_monitor"
def stop(self):
self.running = False
image_exits = self._check_curve_id(name, self.images)
if image_exits:
raise ValueError(f"Monitor with ID '{name}' already exists in widget '{self.gui_id}'.")
image_config = ImageItemConfig(
widget_class="BECImageItem",
parent_id=self.gui_id,
monitor=name,
color_map=color_map,
color_bar=color_bar,
downsample=downsample,
opacity=opacity,
vrange=vrange,
# post_processing=post_processing,
**kwargs,
)
image = self._add_image_object(source=image_source, config=image_config, data=data)
return image
def set_vrange(self, vmin: float, vmax: float, name: str = None):
"""
Set the range of the color bar.
If name is not specified, then set vrange for all images.
Args:
vmin(float): Minimum value of the color bar.
vmax(float): Maximum value of the color bar.
name(str): The name of the image.
"""
if name is None:
for source, images in self.images.items():
for id, image in images.items():
image.set_vrange(vmin, vmax)
else:
image = self.find_widget_by_id(name)
image.set_vrange(vmin, vmax)
def set_color_map(self, cmap: str, name: str = None):
"""
Set the color map of the image.
If name is not specified, then set color map for all images.
Args:
cmap(str): The color map of the image.
name(str): The name of the image.
"""
if name is None:
for source, images in self.images.items():
for id, image in images.items():
image.set_color_map(cmap)
else:
image = self.find_widget_by_id(name)
image.set_color_map(cmap)
@pyqtSlot(dict)
def on_image_update(self, msg: dict):
data = msg["data"]
device = msg["device"]
# TODO postprocessing
image_to_update = self.images["device_monitor"][device]
image_to_update.updateImage(data)
def _connect_device_monitor(self, monitor: str):
"""
Connect to the device monitor.
Args:
monitor(str): The name of the monitor.
"""
image_item = self.find_widget_by_id(monitor)
try:
previous_monitor = image_item.config.monitor
except AttributeError:
previous_monitor = None
if previous_monitor != monitor:
if previous_monitor:
self.bec_dispatcher.disconnect_slot(
self.on_image_update, MessageEndpoints.device_monitor(previous_monitor)
)
if monitor:
self.bec_dispatcher.connect_slot(
self.on_image_update, MessageEndpoints.device_monitor(monitor)
)
image_item.set_monitor(monitor)
def _add_image_object(
self, source: str, name: str, config: ImageItemConfig, data=None
) -> BECImageItem: # TODO fix types
image = BECImageItem(config=config, parent_image=self)
self.plot_item.addItem(image)
self.images[source][name] = image
self.config.images[name] = config
if data is not None:
image.setImage(data)
return image
def _check_image_id(self, val: Any, dict_to_check: dict) -> bool:
"""
Check if val is in the values of the dict_to_check or in the values of the nested dictionaries.
Args:
val(Any): Value to check.
dict_to_check(dict): Dictionary to check.
Returns:
bool: True if val is in the values of the dict_to_check or in the values of the nested dictionaries, False otherwise.
"""
if val in dict_to_check.keys():
return True
for key in dict_to_check:
if isinstance(dict_to_check[key], dict):
if self._check_image_id(val, dict_to_check[key]):
return True
return False
def cleanup(self):
"""
Clean up the widget.
"""
print(f"Cleaning up {self.gui_id}")
for monitor in self.images["device_monitor"]:
self.bec_dispatcher.disconnect_slot(
self.on_image_update, MessageEndpoints.device_monitor(monitor)
)

View File

@ -21,7 +21,6 @@ from bec_widgets.widgets.plots import BECPlotBase, WidgetConfig
class SignalData(BaseModel):
"""The data configuration of a signal in the 1D waveform widget for x and y axis."""
# TODO add validator on name and entry
name: str
entry: str
unit: Optional[str] = None # todo implement later
@ -31,7 +30,7 @@ class SignalData(BaseModel):
class Signal(BaseModel):
"""The configuration of a signal in the 1D waveform widget."""
source: str # TODO add validator on the source type
source: str
x: SignalData
y: SignalData
@ -263,11 +262,15 @@ class BECWaveform1D(BECPlotBase):
self.add_legend()
self.apply_config(self.config)
# TODO check config assigning
# TODO check the functionality of config generator
def find_widget_by_id(
self, item_id: str
): # TODO implement this on level of BECConnector and all other widgets
def find_widget_by_id(self, item_id: str) -> BECCurve:
"""
Find the curve by its ID.
Args:
item_id(str): ID of the curve.
Returns:
BECCurve: The curve object.
"""
for curve in self.plot_item.curves:
if curve.gui_id == item_id:
return curve

View File

@ -23,18 +23,18 @@ def test_plot_base_axes_by_separate_methods(bec_figure):
plot_base.set_x_scale("log")
plot_base.set_y_scale("log")
assert plot_base.titleLabel.text == "Test Title"
assert plot_base.plot_item.titleLabel.text == "Test Title"
assert plot_base.config.axis.title == "Test Title"
assert plot_base.getAxis("bottom").labelText == "Test x Label"
assert plot_base.plot_item.getAxis("bottom").labelText == "Test x Label"
assert plot_base.config.axis.x_label == "Test x Label"
assert plot_base.getAxis("left").labelText == "Test y Label"
assert plot_base.plot_item.getAxis("left").labelText == "Test y Label"
assert plot_base.config.axis.y_label == "Test y Label"
assert plot_base.config.axis.x_lim == (1, 100)
assert plot_base.config.axis.y_lim == (5, 500)
assert plot_base.ctrl.xGridCheck.isChecked() == True
assert plot_base.ctrl.yGridCheck.isChecked() == True
assert plot_base.ctrl.logXCheck.isChecked() == True
assert plot_base.ctrl.logYCheck.isChecked() == True
assert plot_base.plot_item.ctrl.xGridCheck.isChecked() == True
assert plot_base.plot_item.ctrl.yGridCheck.isChecked() == True
assert plot_base.plot_item.ctrl.logXCheck.isChecked() == True
assert plot_base.plot_item.ctrl.logYCheck.isChecked() == True
def test_plot_base_axes_added_by_kwargs(bec_figure):
@ -50,13 +50,13 @@ def test_plot_base_axes_added_by_kwargs(bec_figure):
y_scale="log",
)
assert plot_base.titleLabel.text == "Test Title"
assert plot_base.plot_item.titleLabel.text == "Test Title"
assert plot_base.config.axis.title == "Test Title"
assert plot_base.getAxis("bottom").labelText == "Test x Label"
assert plot_base.plot_item.getAxis("bottom").labelText == "Test x Label"
assert plot_base.config.axis.x_label == "Test x Label"
assert plot_base.getAxis("left").labelText == "Test y Label"
assert plot_base.plot_item.getAxis("left").labelText == "Test y Label"
assert plot_base.config.axis.y_label == "Test y Label"
assert plot_base.config.axis.x_lim == (1, 100)
assert plot_base.config.axis.y_lim == (5, 500)
assert plot_base.ctrl.logXCheck.isChecked() == True
assert plot_base.ctrl.logYCheck.isChecked() == True
assert plot_base.plot_item.ctrl.logXCheck.isChecked() == True
assert plot_base.plot_item.ctrl.logYCheck.isChecked() == True