28 Commits

Author SHA1 Message Date
2bc85654cd Merge pull request 'gitlab refs removed' (#2) from gitlab_hunt into master
Reviewed-on: #2
2025-08-27 08:58:57 +02:00
woznic_n
1278648fc6 gitlab refs removed 2025-08-26 11:59:12 +02:00
d7236401b6 also allow ModuleNotFoundError 2025-03-26 11:24:05 +01:00
bd3ccfa407 Update .gitlab-ci.yml 2024-08-12 15:55:19 +02:00
f97d9a3824 Merge branch 'fix-tests' into 'master'
added libxdamage1

See merge request augustin_s/grum!9
2023-08-16 18:57:38 +02:00
5606d0781f added libxdamage1 2023-08-16 18:50:50 +02:00
7a1a9479c8 updated docstring 2023-05-25 11:16:52 +02:00
7ea180bfb8 Merge branch 'images' into 'master'
Images

See merge request augustin_s/grum!8
2023-05-23 08:45:33 +00:00
fcca087b51 adjusted tests 2023-05-23 10:41:06 +02:00
d958d89da4 added and use constants for default sub window size 2023-05-23 10:19:52 +02:00
dcc554eab8 allow to set color map 2023-05-12 14:59:18 +02:00
cf24d54b62 allow to set levels 2023-05-12 14:35:04 +02:00
0742b9de0f fixed axis labels 2023-05-12 14:28:40 +02:00
ae70ac86a8 added labels to example image 2023-05-12 14:28:07 +02:00
438d5c1eea added image example 2023-05-12 11:17:18 +02:00
2d11fe4ff9 order 2023-04-17 23:36:47 +02:00
dee9cbb934 naming 2023-04-17 23:11:04 +02:00
3ef9032499 use single signal for creating new subwins 2023-04-17 23:09:50 +02:00
f80a7759fb use Description base class; change callbacks to concrete arguments 2023-04-17 22:46:56 +02:00
023ecd7618 added a comment 2023-04-15 13:54:08 +02:00
593241d66f added and use a mapping DescType to MDISubType 2023-04-15 13:51:18 +02:00
839b1a1e21 use DESC_TYPES and stored type to choose DescType on file load 2023-04-15 13:45:35 +02:00
4bb052d665 added description type to class mapping 2023-04-15 13:44:41 +02:00
3c951910b0 extract description type and add it to dict representation 2023-04-15 13:44:00 +02:00
4e95c1f8fd added a Description base class 2023-04-14 23:17:02 +02:00
00b12d800f import order 2023-04-14 18:33:22 +02:00
a7c86eb3b1 moved *Description classes into descs folder 2023-04-14 18:25:10 +02:00
da8d7b0a2b better add_new_desc_to_list; fixed multi plots with marked image; typo 2023-04-14 17:57:33 +02:00
22 changed files with 235 additions and 583 deletions

View File

@@ -6,7 +6,7 @@ stages:
- pip install pytest pytest-random-order pytest-cov
- pip install -e ./
- apt-get update
- apt-get install -y ffmpeg libnss3 libxcomposite1 libxtst6
- apt-get install -y ffmpeg libnss3 libxcomposite1 libxtst6 libxdamage1
tests:
stage: Tests
@@ -24,8 +24,10 @@ tests:
artifacts:
when: always
reports:
cobertura: coverage.xml
junit: report-junit.xml
junit: report.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
tests-3.6:
stage: OptionalTests

View File

@@ -1,6 +1,6 @@
# grum
<img src="https://gitlab.psi.ch/augustin_s/grum/-/wikis/uploads/1a259a1d74e7b79e0230e7bbad3b1284/screenshot2.png" width="50%" />
<img src="https://gitea.psi.ch/SwissFEL/grum/wiki/raw/uploads%2Fe4cd2be847d26bb7ac7100080edbccce%2Fscreenshot.png" width="50%" />
## Overview
@@ -22,11 +22,11 @@ Via the RPC server, new plots can be created and new data appended to existing p
- `new_plot(name, cfg)`
Creates a new plot named `name` in the grum list. The configuration dict `cfg` is used as arguments for the constructor of [`PlotDescription`](https://gitlab.psi.ch/augustin_s/grum/-/blob/master/grum/plotdesc.py#L4).
Creates a new plot named `name` in the grum list. The configuration dict `cfg` is used as arguments for the constructor of [`PlotDescription`](https://gitea.psi.ch/SwissFEL/grum/src/grum/descs/plotdesc.py#L4).
- `append_data(name, point)`
Append data point to the plot named `name`. The new `point` is forwarded to [`PlotDescription.append()`](https://gitlab.psi.ch/augustin_s/grum/-/blob/master/grum/plotdesc.py#L18).
Append data point to the plot named `name`. The new `point` is forwarded to [`PlotDescription.append()`](https://gitea.psi.ch/SwissFEL/grum/src/grum/descs/plotdesc.py#L24).
### Utility functions

View File

@@ -1,183 +0,0 @@
from grum.rpc import RPCClient
from bec_utils import BECMessage, MessageEndpoints
from bec_utils import RedisConnector
from os import listdir
from os.path import isfile, join
import h5py
import time
import numpy as np
from PIL import Image
class BECAdapter:
def __init__(self, redis_connector, recon = False, monitor=False, motor_pos = False) -> None:
super().__init__()
host = "localhost"
port = 8000
self.redis_connector = redis_connector
self.eaccount = "e20631" #change for each new beamtime
# To decide what should be plotted
self.monitor = monitor
self.motor_pos = motor_pos
self.recon = recon
if self.recon:
self.grum_client_recon_abs = RPCClient(host, port)
self.grum_client_recon_phase = RPCClient(host, port)
self.last_plotted_scan_nr = 0
self.latest_reconstructed = 5000
if self.monitor:
self.grum_client_monitor = RPCClient(host ,port)
self.grum_client_monitor.new_plot("monitor",{'xlabel':'time', 'ylabel':"sls_ring_current", "time": True, "frmt":"Line" })
if self.motor_pos:
self.grum_client_motor_pos_static = RPCClient(host, port)
self.grum_client_motor_pos_moving = RPCClient(host, port)
self.scan_nr = None
self.scan_running = False
self.grum_client_motor_pos_static.new_plot("Motor position static", {'xlabel':'average_x_st_fzp', 'ylabel':"average_y_st_fzp" , "frmt": "Dots"})
self.grum_client_motor_pos_moving.new_plot("Motor position move", {'xlabel':'average_x_st_fzp', 'ylabel':"average_y_st_fzp" , 'frmt':"Motor pos move"})
def start(self):
if self.motor_pos:
self.start_scan_status_sub()
self.start_scan_segment_sub()
if self.monitor:
self.start_monitor_sub()
def start_scan_status_sub(self):
self._scan_sub = self.redis_connector.consumer(
MessageEndpoints.scan_status(), cb=self.scan_status_update
)
self._scan_sub.start()
def scan_status_update(self, segment):
# whenever a new scan starts or ends, for now changes scan nr both in start and end
segment = BECMessage.ScanStatusMessage.loads(segment.value)
if segment.content["status"] == "open":
self.scan_nr = segment.content["info"]["scan_number"]
self.positions = segment.content["info"]["positions"]
self.grum_client_motor_pos_moving.new_plot("Motor position move", {"title":"scan number:" + str(self.scan_nr),'xlabel':'average_x_st_fzp', 'ylabel':"average_y_st_fzp" })
xpos = [x for x,y in self.positions]
ypos = [y for x,y in self.positions]
if len(segment.content["info"]["primary"]) == 2:
self.scan_running = True
if self.motor_pos:
print("new plot")
# for point in self.positions
# self.grum_client_motor_pos_static.append_data("Motor position static", self.positions)
self.grum_client_motor_pos_static.new_plot( "Motor position static", { "xs": xpos, "ys":ypos, 'xlabel':'average_x_st_fzp', 'ylabel':"average_y_st_fzp", "frmt": "Dots"})
#Trying to update plots with "open new plots" unticked
# self.grum_client_motor_pos_static.new_plot( "Motor position static", {"xs": xpos[:-1], "ys":ypos[:-1], 'xlabel':'average_x_st_fzp', 'ylabel':"average_y_st_fzp", "frmt": "Dots"})
# self.grum_client_motor_pos_static.append_data( "Motor position static", [xpos[-1], ypos[-1]] )
else:
self.scan_running = False
if len(segment.content["info"]["primary"]) == 1:
print("umv")
def start_scan_segment_sub(self):
self._scan_segment_sub = self.redis_connector.consumer(
MessageEndpoints.scan_segment(), cb=self.scan_segment_update
)
self._scan_segment_sub.start()
def scan_segment_update(self, segment):
if self.scan_running:
segment = BECMessage.ScanMessage.loads(segment.value)
x_val = segment[0].content["data"]["average_x_st_fzp"]["value"]
y_val = segment[0].content["data"]["average_y_st_fzp"]["value"]
point = [x_val, y_val]
if self.motor_pos:
self.grum_client_motor_pos_moving.append_data("Motor position move", point)
def start_monitor_sub(self):
self._monitor_sub = self.redis_connector.consumer(
pattern = MessageEndpoints.device_readback('*'),
cb=self.monitor_update
)
self._monitor_sub.start()
def monitor_update(self, msg):
# Whenever we get a new monitor value
dev = msg.topic.decode().split(MessageEndpoints._device_readback + '/')[-1].split(':sub')[0]
msg = BECMessage.DeviceMessage.loads(msg.value)
if dev == "sls_ring_current":
x = msg.content["signals"][dev]['timestamp']
y = msg.content["signals"][dev]['value']
point = [x,y]
if self.monitor:
self.grum_client_monitor.append_data( "monitor", point)
def plot_img_from_h5(self):
while True:
print('Check for new reconstruction')
self.update_latest_reconstructed()
print('latest_reconstructed: ', self.latest_reconstructed)
print("last plotted scan nr: ", self.last_plotted_scan_nr)
if self.latest_reconstructed > self.last_plotted_scan_nr:
ending_path = self._get_scan_dir(1000, self.latest_reconstructed, leading_zeros = 5)
mypath = "/sls/X12SA/data/" + self.eaccount + "/Data10/analysis/" + ending_path
recon_file = self.get_recon_file(mypath)
with h5py.File(mypath + '/' + recon_file, 'r') as hf:
recon_img_abs, recon_img_phase = self.get_recon_imgs(hf)
print('plotting new image with scan_nr: ', self.latest_reconstructed)
xlab = str(self.latest_reconstructed)
if self.recon:
self.grum_client_recon_abs.new_image( "Absorption", {'image': recon_img_abs, "xlabel": xlab}) #, "levels":[0.9, 1.1]})
# self.grum_client_recon_phase.new_image( f"Phase for scan_nr: {self.latest_reconstructed}", {'image': recon_img_angle, "xlabel": xlab}) #, "colormap":"CET-C1"})
self.grum_client_recon_phase.new_image( "Phase", {'image': recon_img_phase, "xlabel": xlab})#, "levels": [-1,1]}) #, "colormap":"CET-C1"})
self.last_plotted_scan_nr = self.latest_reconstructed
time.sleep(10)
def get_recon_file(self, mypath):
files = [f for f in listdir(mypath) if isfile(join(mypath, f))]
for file in files:
if file.endswith("recons.h5"):
return file
return None
def get_recon_imgs(self, hf):
recon_object = hf.get('reconstruction/object')
# recon_probes = hf.get('reconstruction/probes')
recon_object =np.array(recon_object)
recon_object_abs = np.abs(recon_object)
recon_object_phase = np.angle(recon_object)
recon_object_abs = np.rot90(recon_object_abs,3)
recon_object_phase = np.rot90(recon_object_phase, 3)
recon_img_abs = recon_object_abs.tolist()
recon_img_phase = recon_object_phase.tolist()
return recon_img_abs, recon_img_phase
def update_latest_reconstructed(self):
# mypath = '/sls/X12SA/Data10/' + self.eaccount + '/analysis/online/ptycho/gallery'
mypath = '/sls/X12SA/data/' +self.eaccount+ '/Data10/specES1/ptycho_reconstruct/done' # ptycho_reconstruct argument?
file_list = listdir(mypath)
scans = [file_name.split('_')[1] for file_name in file_list] #0 if you do gallery, 1 if you do done
self.latest_reconstructed = max([int(scan[0:5]) for scan in scans]) # 1:6 for gallery, 0:5 for done
def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None):
if leading_zeros is None:
leading_zeros = len(str(scan_bundle))
floor_dir = scan_number // scan_bundle * scan_bundle
return f"S{floor_dir:0{leading_zeros}d}-{floor_dir+scan_bundle-1:0{leading_zeros}d}/S{scan_number:0{leading_zeros}d}"
if __name__ == "__main__":
print('starts BECAdapter')
print("initializing redis-connector")
redis_url = "129.129.122.75:6379" # for LamNI
redis_connector = RedisConnector(redis_url)
ba = BECAdapter(redis_connector, recon=True) #, motor_pos=True) #, monitor=True)
ba.start()
ba.plot_img_from_h5()

View File

@@ -10,7 +10,7 @@ from .mdi import MDIWindowMode
def main():
app = QApplication(sys.argv)
# ctrl_c.setup(app)
ctrl_c.setup(app)
clargs = handle_clargs()
if not clargs.pop("no_theme"):

View File

@@ -1,10 +1,10 @@
import signal
import socket
# from PyQt5.QtNetwork import QAbstractSocket
from PyQt5.QtNetwork import QAbstractSocket
def setup(app):
# app.signalwatchdog = SignalWatchdog() # need to store to keep socket pair alive
app.signalwatchdog = SignalWatchdog() # need to store to keep socket pair alive
signal.signal(signal.SIGINT, make_quit_handler(app))
@@ -16,25 +16,25 @@ def make_quit_handler(app):
# class SignalWatchdog(QAbstractSocket):
class SignalWatchdog(QAbstractSocket):
# def __init__(self):
# """
# Propagates system signals from Python to QEventLoop
# adapted from https://stackoverflow.com/a/65802260/655404
# """
# super().__init__(QAbstractSocket.SctpSocket, None)
def __init__(self):
"""
Propagates system signals from Python to QEventLoop
adapted from https://stackoverflow.com/a/65802260/655404
"""
super().__init__(QAbstractSocket.SctpSocket, None)
# self.writer, self.reader = writer, reader = socket.socketpair()
# writer.setblocking(False)
self.writer, self.reader = writer, reader = socket.socketpair()
writer.setblocking(False)
# fd_writer = writer.fileno()
# fd_reader = reader.fileno()
fd_writer = writer.fileno()
fd_reader = reader.fileno()
# signal.set_wakeup_fd(fd_writer) # Python hook
# self.setSocketDescriptor(fd_reader) # Qt hook
signal.set_wakeup_fd(fd_writer) # Python hook
self.setSocketDescriptor(fd_reader) # Qt hook
# self.readyRead.connect(lambda: None) # dummy function call that lets the Python interpreter run
self.readyRead.connect(lambda: None) # dummy function call that lets the Python interpreter run

13
grum/descs/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
from .desc import Description
from .imgdesc import ImageDescription
from .plotdesc import PlotDescription
DESC_TYPES = {
ImageDescription.get_type(): ImageDescription,
PlotDescription.get_type(): PlotDescription
}

23
grum/descs/desc.py Normal file
View File

@@ -0,0 +1,23 @@
class Description:
def to_dict(self):
res = {k: v for k, v in self.__dict__.items() if not k.startswith("_") and k != "name" and v is not None}
tn = self.get_type()
res.setdefault("type", tn)
return res
@classmethod
def get_type(cls):
tn = cls.__name__
suffix = "Description"
if not tn.endswith(suffix):
raise ValueError(f'"{tn}" does not end with "{suffix}"')
tn = tn[:-len(suffix)]
tn = tn.casefold()
tn = tn or None
return tn

View File

@@ -1,19 +1,18 @@
import numpy as np
import pyqtgraph as pg
from .theme import pg_legend_style
from .desc import Description
class ImageDescription:
class ImageDescription(Description):
def __init__(self, name, title=None, xlabel=None, ylabel=None, image=None, colormap=None, levels=None):
def __init__(self, name, title=None, xlabel=None, ylabel=None, image=None, levels=None, cmap="viridis"):
self.name = name
self.title = title
self.xlabel = xlabel
self.ylabel = ylabel
self.image = image
self.colormap = colormap
self.levels = levels
self.levels = levels #TODO: might be better to use vmin and vmax
self.cmap = cmap
@property
def data(self):
@@ -23,6 +22,7 @@ class ImageDescription:
def data(self, value):
self.image = value
def append(self, xy):
print("ignored image append")
@@ -31,30 +31,22 @@ class ImageDescription:
def make_plot(self, plotwidget, style):
if self.levels:
res = plotwidget.setImage(self.data, levels = self.levels) #Set levels for histogram
else:
res = plotwidget.setImage(self.data)
res = plotwidget.setImage(self.data, levels=self.levels)
if self.title:
plotwidget.setTitle(self.title)
if self.xlabel:
vbox = plotwidget.getView()
vbox.addItem(pg.LabelItem(self.xlabel, size='50pt'))
plotwidget.getView().setLabel("bottom", self.xlabel)
if self.ylabel:
plotwidget.setLabel("left", self.ylabel)
plotwidget.getView().setLabel("left", self.ylabel)
if self.colormap:
cm = pg.colormap.get(self.colormap)
if self.cmap:
cm = pg.colormap.get(self.cmap)
plotwidget.setColorMap(cm)
return res
def to_dict(self):
return {k: v for k, v in self.__dict__.items() if not k.startswith("_") and k != "name" and v is not None}

50
grum/descs/plotdesc.py Normal file
View File

@@ -0,0 +1,50 @@
from .desc import Description
class PlotDescription(Description):
def __init__(self, name, title=None, xlabel=None, ylabel=None, xs=None, ys=None):
self.name = name
self.title = title
self.xlabel = xlabel
self.ylabel = ylabel
self.xs = [] if xs is None else list(xs)
self.ys = [] if ys is None else list(ys)
@property
def data(self):
return (self.xs, self.ys)
@data.setter
def data(self, value):
self.xs, self.ys = value
def append(self, xy):
x, y = xy
self.xs.append(x)
self.ys.append(y)
def extend(self, data):
xs, ys = data
self.xs.extend(xs)
self.ys.extend(ys)
def make_plot(self, plotwidget, style):
res = plotwidget.plot(self.xs, self.ys, name=self.name, **style)
if self.title:
plotwidget.setTitle(self.title)
if self.xlabel:
plotwidget.setLabel("bottom", self.xlabel)
if self.ylabel:
plotwidget.setLabel("left", self.ylabel)
return res

View File

@@ -1,6 +1,6 @@
import numpy as np
from .plotdesc import PlotDescription
from .descs import PlotDescription, ImageDescription
X = np.arange(100) / 10
@@ -31,4 +31,20 @@ for name, (xs, ys) in exampledata_raw.items():
)
name = "image"
xdim = ydim = 100
size = xdim * ydim
shape = (xdim, ydim)
img = np.arange(size).reshape(shape) / size
img += np.random.random(shape) / 10
exampledata[name] = ImageDescription(
name,
image=img,
xlabel="x",
ylabel="y"
)

View File

@@ -1,26 +1,28 @@
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QMainWindow, QSplitter
import pyqtgraph as pg
from . import assets
from .descs import DESC_TYPES, Description, ImageDescription, PlotDescription
from .dictlist import DictList
from .dictlist.dictlistitem import DictListItem
from .exampledata import exampledata
from .h5filedlg import open_h5_files_dialog, save_h5_file_dialog
from .io import write_dict, read_dict
from .mdi import MDIArea, MDISubMultiPlot, MDISubPlot, MDISubImage, MDIWindowMode, MDISubMultiImage
from .mdi import MDIArea, MDISubMultiPlot, MDISubPlot, MDISubImage, MDIWindowMode
from .menus import BarMenu
from .plotdesc import PlotDescription
from .imgdesc import ImageDescription
from .rpc import RPCServerThread
from .shortcut import shortcut
# from .webview import WebView # doesnt work for CSAXS version of pyqtWebEngine
from .webview import WebView
DESC_TYPE_TO_MDI_SUB_TYPE = {
ImageDescription: MDISubImage,
PlotDescription: MDISubPlot
}
class MainWindow(QMainWindow):
sig_make_new_plot = pyqtSignal(str, PlotDescription)
sig_make_new_image = pyqtSignal(str, ImageDescription)
sig_make_new_subwin = pyqtSignal(str, Description)
def __init__(self, *args, title="grum", host="localhost", port=8000, offline=False, add_examples=False, window_mode=MDIWindowMode.MULTI, **kwargs):
super().__init__(*args, **kwargs)
@@ -32,7 +34,7 @@ class MainWindow(QMainWindow):
self.setWindowIcon(assets.icon())
url = f"http://{host}:{port}/"
# self.webdoc = WebView(url, title=title) # doesnt work for CSAXS version of pyqtWebEngine
self.webdoc = WebView(url, title=title)
self.lst = lst = DictList()
lst.setAlternatingRowColors(True)
@@ -45,21 +47,16 @@ class MainWindow(QMainWindow):
lst_menu.addSeparator()
lst_menu.addAction("Plot selected", self.on_plot_selected)
lst_menu.addSeparator()
lst_menu.addAction("Mark selected as seen",
self.on_mark_selected_as_seen)
lst_menu.addAction("Mark selected as not seen",
self.on_mark_selected_as_not_seen)
lst_menu.addAction("Mark selected as seen", self.on_mark_selected_as_seen)
lst_menu.addAction("Mark selected as not seen", self.on_mark_selected_as_not_seen)
lst_menu.addSeparator()
sort_group = lst_menu.addGroup()
sort_group.addCheckbox(
"Sort by insertion order", triggered=self.on_sort_by_insertion_order, state=True)
sort_group.addCheckbox("Sort by insertion order", triggered=self.on_sort_by_insertion_order, state=True)
sort_group.addCheckbox("Sort by name", triggered=self.on_sort_by_name)
sort_group.addCheckbox("Sort by timestamp",
triggered=self.on_sort_by_timestamp)
sort_group.addCheckbox(
"Sorting disabled", triggered=self.on_sorting_disabled)
sort_group.addCheckbox("Sort by timestamp", triggered=self.on_sort_by_timestamp)
sort_group.addCheckbox("Sorting disabled", triggered=self.on_sorting_disabled)
# TODO: clean up
#TODO: clean up
def on_item_about_to_be_moved():
sort_group.checkboxes["Sorting disabled"].setChecked(True)
self.on_sorting_disabled()
@@ -79,8 +76,7 @@ class MainWindow(QMainWindow):
self.menu_settings = menu = BarMenu(bar, "&Settings")
menu.addCheckbox("Open new plots", state=True)
menu.addSeparator()
menu.addEntrybox("Limit number of entries",
placeholder="Maximum number of entries", triggered=lst.set_nkeep)
menu.addEntrybox("Limit number of entries", placeholder="Maximum number of entries", triggered=lst.set_nkeep)
self.mdi = mdi = MDIArea(bar, window_mode=window_mode)
@@ -92,40 +88,48 @@ class MainWindow(QMainWindow):
self.setCentralWidget(splitter)
if not offline:
self.rst = rst = RPCServerThread(
host, port, doc_title_suffix=title)
self.rst = rst = RPCServerThread(host, port, doc_title_suffix=title)
rst.start()
rst.server.register_function(self.new_image)
rst.server.register_function(self.new_plot)
rst.server.register_function(self.append_data)
rst.server.register_function(self.extend_data)
rst.server.register_function(self.set_data)
rst.server.register_function(self.new_image)
# function for CSAXS beamtime
rst.server.register_function(self.append_image_to_list)
# new function to get list of names from current grum session
rst.server.register_function(self.get_list_names)
# rst.server.register_function(self.plot_multiple_plots)
self.sig_make_new_plot.connect(self.on_make_new_plot)
self.sig_make_new_image.connect(self.on_make_new_image)
self.sig_make_new_subwin.connect(self.on_make_new_subwin)
def keyPressEvent(self, event):
if event.key() == Qt.Key_F1:
# self.webdoc.show() # doesnt work for CSAXS version of pyqtWebEngine
pass
self.webdoc.show()
# Remote API calls
def new_image(self, name, cfg):
"""
Create a new image <name> using the configuration dict <cfg>.
The configuration is forwarded to the constructor of ImageDescription.
Allowed keys are: title, xlabel, ylabel, image, levels, cmap.
"""
desc = self.add_new_desc_to_list(ImageDescription, name, cfg)
if self.menu_settings.checkboxes["Open new plots"].isChecked():
sub = self.mdi.findSubWindow(name)
if sub:
sub.pw.setImage(desc.data) #TODO lacks the list sync
else:
self.sig_make_new_subwin.emit(name, desc)
def new_plot(self, name, cfg):
"""
Create a new plot <name> using the configuration dict <cfg>.
The configuration is forwarded to the constructor of PlotDescription.
Allowed keys are: title, xlabel, ylabel, xs, ys.
"""
desc = self.add_new_desc_to_list(name, cfg)
desc = self.add_new_desc_to_list(PlotDescription, name, cfg)
if self.menu_settings.checkboxes["Open new plots"].isChecked():
if not self.mdi.findSubWindow(name):
self.sig_make_new_plot.emit(name, desc)
self.sig_make_new_subwin.emit(name, desc)
def append_data(self, name, point):
"""
@@ -157,72 +161,13 @@ class MainWindow(QMainWindow):
desc.data = data
self.sync_item_and_plots(item)
def new_image(self, name, cfg):
"""
Create a new image <name> using the configuration dict <cfg>.
The configuration is forwarded to the constructor of ImageDescription.
Allowed keys are: title, xlabel, ylabel, image.
"""
desc = self.add_new_desc_to_list(
name, cfg, Desc=ImageDescription) # TODO: clean up Desc argument
if self.menu_settings.checkboxes["Open new plots"].isChecked():
sub = self.mdi.findSubWindow(name)
if sub:
sub.pw.setImage(desc.data) # TODO lacks the list sync
vbox = sub.pw.getView()
allchildren = vbox.allChildren()
for child in allchildren:
if isinstance(child, pg.LabelItem):
vbox.removeItem(child)
vbox.addItem(pg.LabelItem(desc.xlabel, size='50pt'))
else:
self.sig_make_new_image.emit(name, desc)
def append_image_to_list(self, img):
desc = self.add_new_desc_to_list(img[0], img[1], Desc=ImageDescription)
def get_list_names(self):
return [key for key in self.lst.lst.items.keys()]
# def plot_multiple_plots(self, aa):
# name = "plot 1"
# cfg = {"xs": [0, 1], "ys": [3, 7]}
# desc = self.add_new_desc_to_list(name, cfg)
# name = "plot 2"
# cfg = {"xs": [0, 3], "ys": [2,0]}
# desc2 = self.add_new_desc_to_list(name, cfg)
# items = [self.lst.get("plot 1"), self.lst.get("plot 2")]
# self.plot_multiple_items(items)
# descs = {i.name: i for i in items}
# names = descs.keys()
# name = " | ".join(names)
# self.activate_or_make_subwin(MDISubMultiPlot, name, descs)
# if self.menu_settings.checkboxes["Open new plots"].isChecked():
# if not self.mdi.findSubWindow(name):
# self.sig_make_new_plot.emit(name, desc)
# self.make_subwin(MDISubMultiPlot, name, **descs)
# if not self.mdi.findSubWindow(name):
# sub = MDISubMultiPlot(name, descs)
# print("multi name", name)
# print("multi descs", descs, *descs)
# print("MULTI sub", sub)
# print("multi mdi", self.mdi)
# self.mdi.add(sub)
# Signal callbacks
def on_make_new_plot(self, *args, **kwargs):
self.make_subwin(MDISubPlot, *args, **kwargs)
def on_make_new_image(self, *args, **kwargs):
self.make_subwin(MDISubImage, *args, **kwargs)
def on_make_new_subwin(self, name, desc):
DescType = type(desc)
MDISubType = DESC_TYPE_TO_MDI_SUB_TYPE[DescType]
self.make_subwin(MDISubType, name, desc)
def on_dclick_list_item(self, item):
self.plot_single_item(item)
@@ -234,18 +179,17 @@ class MainWindow(QMainWindow):
if len(selected) == 1:
item = selected[0]
self.plot_single_item(item)
# TODO: what should be the typecheck?
elif type(selected[0].value) == ImageDescription:
self.plot_multiple_images(selected)
else:
self.plot_multiple_items(selected)
def on_mark_selected_as_seen(self):
self.lst.set_alarm_for_selected(False)
def on_mark_selected_as_not_seen(self):
self.lst.set_alarm_for_selected(True)
def on_sort_by_insertion_order(self):
self.lst.enable_sort_by_insertion_order()
@@ -258,6 +202,7 @@ class MainWindow(QMainWindow):
def on_sorting_disabled(self):
self.lst.disable_sorting()
def on_file_open(self):
fns = open_h5_files_dialog(self)
if not fns:
@@ -265,9 +210,11 @@ class MainWindow(QMainWindow):
for fn in fns:
data = read_dict(fn)
for k, v in data.items():
Desc = ImageDescription if "image" in v else PlotDescription # TODO
self.add_new_desc_to_list(k, v, Desc=Desc)
for name, cfg in data.items():
tn = cfg.pop("type")
DescType = DESC_TYPES[tn]
self.add_new_desc_to_list(DescType, name, cfg)
def on_file_save(self):
fn = save_h5_file_dialog(self)
@@ -281,10 +228,11 @@ class MainWindow(QMainWindow):
write_dict(fn, data)
# Plumbing
def add_new_desc_to_list(self, name, cfg, Desc=PlotDescription): # TODO
desc = Desc(name, **cfg)
def add_new_desc_to_list(self, DescType, name, cfg):
desc = DescType(name, **cfg)
self.lst.set(name, desc)
return desc
@@ -303,33 +251,26 @@ class MainWindow(QMainWindow):
item.timestamps.access.update()
item.set_alarm(False)
name, desc = item.key, item.value
MDISubType = MDISubImage if isinstance(
desc, ImageDescription) else MDISubPlot # TODO
DescType = type(desc)
MDISubType = DESC_TYPE_TO_MDI_SUB_TYPE[DescType]
self.activate_or_make_subwin(MDISubType, name, desc)
def plot_multiple_items(self, items):
for i in items:
i.timestamps.access.update()
i.set_alarm(False)
items = (i for i in items if isinstance(i.value, PlotDescription)) #TODO: for now, only overlay plots
descs = {i.key: i.value for i in items}
names = descs.keys()
name = " | ".join(names)
self.activate_or_make_subwin(MDISubMultiPlot, name, descs)
def plot_multiple_images(self, images):
for i in images:
i.timestamps.access.update()
i.set_alarm(False)
descs = {i.key: i.value for i in images}
names = descs.keys()
name = " | ".join(names)
self.activate_or_make_subwin(MDISubMultiImage, name, descs)
# TODO: the following two could be methods to MDIArea?
#TODO: the following two could be methods of MDIArea?
def activate_or_make_subwin(self, MDISubType, name, *args, **kwargs):
sub = self.mdi.findSubWindow(name)
if sub: # TODO check type? what to do for mismatches?
if sub: #TODO check type? what to do for mismatches?
self.mdi.setActiveSubWindow(sub)
else:
self.make_subwin(MDISubType, name, *args, **kwargs)
@@ -337,3 +278,6 @@ class MainWindow(QMainWindow):
def make_subwin(self, MDISubType, name, *args, **kwargs):
sub = MDISubType(name, *args, **kwargs)
self.mdi.add(sub)

View File

@@ -1,6 +1,6 @@
from .mdiarea import MDIArea, MDIWindowMode
from .mdisubplot import MDISubPlot, MDISubMultiPlot
from .mdisubimg import MDISubImage, MDISubMultiImage
from .mdisubimg import MDISubImage

View File

@@ -1,33 +1,26 @@
import pyqtgraph as pg
from .mdisubwin import MDISubWindow
from ..theme import pg_plot_style
from PyQt5.QtWidgets import QMainWindow, QWidget, QPushButton, QGridLayout, QVBoxLayout, QApplication, QSlider
from PyQt5.QtCore import Qt, QThread, QTimer
class MDISubImageBase(MDISubWindow):
def __init__(self, name, *args, **kwargs):
super().__init__(name, *args, **kwargs)
class MDISubImage(MDISubImageBase):
class MDISubImage(MDISubWindow):
def __init__(self, name, desc, *args, **kwargs):
super().__init__(name, *args, **kwargs)
self.pw = pw = pg.ImageView()
self.pw = pw = pg.ImageView(view=pg.PlotItem()) # for axis ticks and labels, view needs to be a PlotItem
self.setWidget(pw)
# connect to plot mouse-over event
pw.scene.sigMouseMoved.connect(self.on_hover)
style = pg_plot_style()
# cm = pg.colormap.get('CET-C1')
# pw.setColorMap(cm)
plot = desc.make_plot(self.pw, style)
self.plots = {name: plot}
self.image = desc.data
def on_hover(self, event):
coord = self.pw.imageItem.mapFromScene(event)
x = coord.x()
@@ -42,56 +35,4 @@ class MDISubImage(MDISubImageBase):
self.setToolTip(f"x = {x}\ny = {y}\nz = {z}")
class MDISubMultiImage(MDISubImageBase):
def __init__(self, name, descs, *args, **kwargs):
super().__init__(name, *args, **kwargs)
self.pw = pw = pg.ImageView()
self.central_widget = QWidget()
self.button_next = QPushButton('Next', self.central_widget)
self.button_previous = QPushButton('Previous', self.central_widget)
self.slider = QSlider(Qt.Horizontal)
self.slider.setRange(0,len(descs)-1)
self._create_layout()
self._connect_signals()
names = [names for names, _ in descs.items()]
self.descriptions = [desc for _, desc in descs.items()]
desc = self.descriptions[0]
style = pg_plot_style()
plot = desc.make_plot(self.pw, style)
self.plots = {name: plot}
self.image = desc.data
def _create_layout(self):
self.layout = QGridLayout(self.central_widget)
self.layout.addWidget(self.button_next,1,2)
self.layout.addWidget(self.button_previous, 1,0)
self.layout.addWidget(self.pw, 0,0, 1,3)
self.layout.addWidget(self.slider, 1,1)
self.setWidget(self.central_widget)
def _connect_signals(self):
self.button_next.clicked.connect(lambda:self.update_slider(1))
self.button_previous.clicked.connect(lambda:self.update_slider(-1))
self.slider.valueChanged.connect(self.update_img)
def update_slider(self, value):
self.slider.setValue(self.slider.value()+value)
def update_img(self, value):
self.pw.setImage(self.descriptions[value].data)
self.set_title_in_image()
def set_title_in_image(self):
if self.descriptions[self.slider.value()].xlabel:
vbox = self.pw.getView()
allchildren = vbox.allChildren()
for child in allchildren:
if isinstance(child, pg.LabelItem):
vbox.removeItem(child)
vbox.addItem(pg.LabelItem(self.descriptions[self.slider.value()].xlabel, size='50pt'))

View File

@@ -4,12 +4,17 @@ from PyQt5.QtWidgets import QMdiSubWindow
from .. import assets
SUB_WIN_WIDTH = 640
SUB_WIN_HEIGHT = 480
class MDISubWindow(QMdiSubWindow):
def __init__(self, title, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowTitle(title)
self.setWindowIcon(assets.char())
self.resize(SUB_WIN_WIDTH, SUB_WIN_HEIGHT)
# without this, the SubWindow is not removed from the subWindowList
self.setAttribute(Qt.WA_DeleteOnClose)

View File

@@ -1,78 +0,0 @@
from grum.rpc import RPCClient
from os import listdir
from os.path import isfile, join
import h5py
import numpy as np
import argparse
class plotPhases:
"""
A possibility to plot reconstructions in a scan number range
"""
def __init__(self, start, end, step=1):
self.start = start
self.end = end
self.step =step
self.eaccount = "e20631"
self.grum_client = RPCClient("localhost", 8000) # this is outgoing
# self.image_description_list = []
def plot_all_phases(self):
for scan_nr in range(self.start, self.end + 1, self.step):
print("retrieving data for scan: ", scan_nr)
ending_path = self._get_scan_dir(1000, scan_nr, leading_zeros = 5)
mypath = "/sls/X12SA/data/" + self.eaccount + "/Data10/analysis/" + ending_path
recon_file = self.get_recon_file(mypath)
with h5py.File(mypath + '/' + recon_file, 'r') as hf:
recon_img_phase = self.get_recon_phase(hf)
xlab = str(scan_nr)
print("appending scan nr: ", scan_nr)
self.grum_client.append_image_to_list([f"{scan_nr} Phase", {'image': recon_img_phase, "xlabel": xlab}])
# self.image_description_list.append([f"{scan_nr} Phase", {'image': recon_img_phase, "xlabel": xlab}])
# print("calling plot images from list")
# self.grum_client.append_images_to_list(self.image_description_list)
def get_recon_phase(self, hf):
recon_object = hf.get('reconstruction/object')
recon_object = np.array(recon_object)
recon_object_angle = np.angle(recon_object)
recon_object_angle = np.rot90(recon_object_angle, 3)
recon_img_angle = recon_object_angle.tolist()
return recon_img_angle
def get_recon_file(self, mypath):
files = [f for f in listdir(mypath) if isfile(join(mypath, f))]
for file in files:
if file.endswith("recons.h5"):
return file
print("no file for this scan nr")
return None
def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None):
if leading_zeros is None:
leading_zeros = len(str(scan_bundle))
floor_dir = scan_number // scan_bundle * scan_bundle
return f"S{floor_dir:0{leading_zeros}d}-{floor_dir+scan_bundle-1:0{leading_zeros}d}/S{scan_number:0{leading_zeros}d}"
if __name__ == "__main__":
start = 5037 # default
end = 5038 # default
step = 1
parser = argparse.ArgumentParser(description="description", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-s", "--startend", nargs =2, help="set start, end")
clargs = parser.parse_args()
if clargs:
start = int(clargs.startend[0])
end = int(clargs.startend[1])
print('starts plotting phase for scans: ',start, ' to ', end)
pp = plotPhases(start, end)
pp.plot_all_phases()

View File

@@ -1,66 +0,0 @@
from pyqtgraph import DateAxisItem
from collections import deque
class PlotDescription:
def __init__(self, name, title=None, xlabel=None, ylabel=None, xs=None, ys=None, frmt=None, time=False):
self.name = name
self.title = title
self.xlabel = xlabel
self.ylabel = ylabel
self.xs = deque(maxlen = 5000) if xs is None else deque(xs, maxlen = 5000)
self.ys = deque(maxlen = 5000) if ys is None else deque(ys, maxlen = 5000)
self.format = frmt
self.time = time
@property
def data(self):
return (self.xs, self.ys)
@data.setter
def data(self, value):
self.xs, self.ys = value
def append(self, xy):
x, y = xy
self.xs.append(x)
self.ys.append(y)
def extend(self, data):
xs, ys = data
self.xs.extend(xs)
self.ys.extend(ys)
def make_plot(self, plotwidget, style):
if self.format:
res = self.plot_with_format(plotwidget, style)
else:
res = plotwidget.plot(self.xs, self.ys, name=self.name, **style)
if self.time:
axis = DateAxisItem()
plotwidget.setAxisItems({'bottom':axis})
if self.title:
plotwidget.setTitle(self.title)
if self.xlabel:
plotwidget.setLabel("bottom", self.xlabel)
if self.ylabel:
plotwidget.setLabel("left", self.ylabel)
return res
def to_dict(self):
return {k: v for k, v in self.__dict__.items() if not k.startswith("_") and k != "name" and v is not None}
def plot_with_format(self, plotwidget, style):
if self.format == "Line":
res = plotwidget.plot(self.xs, self.ys, name=self.name, pen="w")
elif self.format == "Dots":
res = plotwidget.plot(self.xs, self.ys, name=self.name, pen=None, symbol="o", symbolSize = 5)
elif self.format == "Motor pos move":
res = plotwidget.plot(self.xs, self.ys, name=self.name, pen='r', symbol="o", symbolSize = 10)
else:
res = plotwidget.plot(self.xs, self.ys, name=self.name, **style)

View File

@@ -2,7 +2,7 @@ from PyQt5.QtCore import QUrl
try:
from PyQt5.QtWebKitWidgets import QWebView
except ImportError:
except (ImportError, ModuleNotFoundError):
from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView

Binary file not shown.

View File

@@ -4,9 +4,9 @@ version = 0.0.1
description = GUI for Remote Unified Monitoring
long_description = file: README.md
long_description_content_type = text/markdown
url = https://gitlab.psi.ch/augustin_s/grum
url = https://gitea.psi.ch/SwissFEL/grum
project_urls =
Bug Tracker = https://gitlab.psi.ch/augustin_s/grum/issues
Bug Tracker = https://gitea.psi.ch/SwissFEL/grum/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License

View File

@@ -1,18 +1,8 @@
import os
import pathlib
import subprocess
from setuptools import setup
CURRENT_PATH = pathlib.Path(__file__).parent.resolve()
# bec_utils = os.path.join(os.getenv("BEC_PATH", f"{CURRENT_PATH}/../bec"), "bec_utils")
if __name__ == "__main__":
# subprocess.run(f"pip install -e {bec_utils}", shell=True, check=True)
setup(
install_requires=["pyqt5==5.12", "pyqtgraph", "h5py", "PyQtWebEngine==5.12", "Pillow"], #this version works for comp1 CSAXS, no later version
install_requires=["pyqt5", "pyqtgraph", "h5py", "PyQtWebEngine"],
entry_points={"console_scripts": ["grum=grum:main"]},
)

View File

@@ -14,7 +14,7 @@ from grum.mainwin import MainWindow
from grum.mdi import MDIArea, MDISubMultiPlot, MDISubPlot
from grum.menus import BarMenu
from grum.menus.rclickmenu import RClickMenu
from grum.plotdesc import PlotDescription
from grum.descs import Description, PlotDescription
from grum.rpc import RPCServerThread
@@ -47,7 +47,7 @@ class TestMainWin:
for key in mw.lst.lst.items:
assert isinstance(mw.lst.lst.get(key), DictListItem)
assert isinstance(mw.lst.lst.get(key).value, PlotDescription)
assert isinstance(mw.lst.lst.get(key).value, Description)
assert isinstance(mw.lst.menu, RClickMenu)
assert isinstance(mw.menu_settings, BarMenu)
@@ -65,7 +65,7 @@ class TestMainWin:
xlabel = "xlabel"
ylabel = "ylabel"
cfg = {"title": title, "xlabel": xlabel, "ylabel": ylabel}
spy_sig_make_new_plot = QSignalSpy(mw.sig_make_new_plot)
spy_sig_make_new_subwin = QSignalSpy(mw.sig_make_new_subwin)
mw.new_plot(name, cfg=cfg)
@@ -76,28 +76,28 @@ class TestMainWin:
assert mw.lst.lst.get(name).value.ylabel == ylabel
assert mw.menu_settings.checkboxes["Open new plots"].isChecked()
assert len(spy_sig_make_new_plot) == 1 # assert called once
assert spy_sig_make_new_plot[0][0] == name # assert called with name
assert isinstance(spy_sig_make_new_plot[0][1], PlotDescription)
assert len(spy_sig_make_new_subwin) == 1 # assert called once
assert spy_sig_make_new_subwin[0][0] == name # assert called with name
assert isinstance(spy_sig_make_new_subwin[0][1], PlotDescription)
mw.menu_settings.checkboxes["Open new plots"].setChecked(False)
assert mw.menu_settings.checkboxes["Open new plots"].isChecked() == False
spy_sig_make_new_plot = QSignalSpy(mw.sig_make_new_plot)
spy_sig_make_new_subwin = QSignalSpy(mw.sig_make_new_subwin)
mw.new_plot("new_name", cfg)
assert len(spy_sig_make_new_plot) == 0 # assert not called
assert len(spy_sig_make_new_subwin) == 0 # assert not called
mw.menu_settings.checkboxes["Open new plots"].setChecked(True)
assert mw.menu_settings.checkboxes["Open new plots"].isChecked() == True
spy_sig_make_new_plot = QSignalSpy(mw.sig_make_new_plot)
spy_sig_make_new_subwin = QSignalSpy(mw.sig_make_new_subwin)
new_name_item = mw.lst.lst.get("new_name")
sub = MDISubPlot("new_name", new_name_item.value)
mw.mdi.add(sub)
mw.new_plot("new_name", cfg)
assert len(spy_sig_make_new_plot) == 0 # assert not called
assert len(spy_sig_make_new_subwin) == 0 # assert not called
def test_append_data(self):
@@ -122,17 +122,19 @@ class TestMainWin:
assert sine_item.set_alarm.call_args[0][0] == False
def test_on_make_new_plot(self):
def test_on_make_new_subwin(self):
mw = self.mw
mw.make_subwin = mock.MagicMock()
args = (1, 2, "name")
kwargs = {"title": "plot_title"}
name = "test"
cfg = {"title": "title"}
mw.on_make_new_plot(args, kwargs)
desc = PlotDescription(name, *cfg)
mw.make_subwin.assert_called_once_with(MDISubPlot, args, kwargs)
mw.on_make_new_subwin(name, desc)
mw.make_subwin.assert_called_once_with(MDISubPlot, name, desc)
def test_on_dclick_list_item(self):

View File

@@ -6,7 +6,7 @@ import pyqtgraph as pg
from grum import theme
from grum.mainwin import MainWindow
from grum.mdi.mdisubplot import MDISubPlot
from grum.plotdesc import PlotDescription
from grum.descs import PlotDescription
from grum.theme import pg_plot_style
@@ -78,6 +78,7 @@ def test_to_dict():
"xs": [1, 2],
"ylabel": "plot_ylabel",
"ys": [3, 4],
"type": "plot"
}