76 Commits

Author SHA1 Message Date
stalbe_j
d6ac86bfcc get list names, thoughts about multiplotting 2023-05-12 09:05:06 +02:00
e20631
10c9332940 fix webview pipeline 2023-05-09 10:33:00 +02:00
e20631
b6c47a903f fix webview pipeline 2023-05-09 10:13:35 +02:00
e20631
b464daffca cleanup after beamtime 2023-05-09 10:05:38 +02:00
e20632
1bd1029cfd changes to add to list 2023-05-04 13:09:21 +02:00
e20632
4f8d84c316 change scan nr for image in submultiimage, set level for reconstruction 2023-05-03 15:46:48 +02:00
e20633
4414202f92 added possibility to plot multiple images 2023-05-03 13:24:07 +02:00
e20633
9228562c9a Merge branch 'recon_plot' of https://gitlab.psi.ch/augustin_s/grum into recon_plot 2023-04-25 13:39:18 +02:00
e20633
76da81b509 changes for new CSAXS experiment 2023-04-25 13:38:51 +02:00
e20642
cac184be05 update for new experiment CSAXS 2023-04-25 13:36:02 +02:00
e20642
99ac3d85b9 motor plot almost done, trying to add title, didn't work 2023-04-21 13:26:20 +02:00
e20642
372d3ff4c8 made motorplot work, added scan number to recon 2023-04-20 15:28:12 +02:00
Stalberg Jonathan
68abc169e0 changing setup 2023-04-18 15:15:36 +02:00
Stalberg Jonathan
a9abbb227c change to a deque for monitor 2023-04-18 13:43:30 +02:00
Stalberg Jonathan
a07fad5cb7 removed commandbuffer and made it possible to run multiple plots at the same time 2023-04-18 11:45:54 +02:00
stalbe_j
00f06b5d85 add commandbuffer 2023-04-17 13:48:19 +02:00
Stalberg Jonathan
16f149a937 comment 2023-04-17 12:17:53 +02:00
Stalberg Jonathan
221855e72c started to implement monitor and motor pos 2023-04-17 12:16:17 +02:00
Stalberg Jonathan
830879cbe6 changes at beamline 2023-04-14 18:15:06 +02:00
stalbe_j
61e732e941 Merge branch 'images' of gitlab.psi.ch:augustin_s/grum into recon_plot 2023-04-14 14:34:10 +02:00
Stalberg Jonathan
59de72ba04 logic for continous running 2023-04-14 14:31:54 +02:00
7470f1388b first try on adding 2D image support 2023-04-14 14:17:46 +02:00
stalbe_j
1ccde730e0 changes to BECAdapter 2023-04-14 14:06:06 +02:00
stalbe_j
de1e1a3fec rename 2023-04-14 12:29:47 +02:00
stalbe_j
37c77f57d2 added BECAdapter 2023-04-14 12:24:21 +02:00
Stalberg Jonathan
b68762550c cleanup 2023-04-14 11:11:31 +02:00
stalbe_j
a094567f29 comment print 2023-04-14 09:10:01 +02:00
stalbe_j
facf92f498 changing directory name 2023-04-14 09:08:24 +02:00
stalbe_j
28ebdae35b testing if qtwebkitwidgets is the problem 2023-04-14 08:59:25 +02:00
stalbe_j
b570f0c885 testing if ctrl_c is the problem 2023-04-14 08:45:10 +02:00
stalbe_j
b455b24c39 testing if ctrl_c is the problem 2023-04-14 08:44:18 +02:00
stalbe_j
e75119093c first puch for recon_plot 2023-04-14 08:22:05 +02:00
a80b568894 enable PYTHONFAULTHANDLER for segfault tracebacks 2023-03-18 11:48:03 +01:00
2edc68151d Merge branch 'gitlab-ci-update' into 'master'
ci: improved ci pipeline; added 3.6 to 3.11 tests

See merge request augustin_s/grum!6
2023-03-14 20:26:39 +00:00
aa03291e42 Merge branch 'tests' into 'master'
Tests

See merge request augustin_s/grum!7
2023-03-14 20:26:23 +00:00
5d97dd6224 added test report artifact 2023-03-14 11:52:03 +01:00
bed086f95f do not test the default 3.8 twice 2023-03-14 11:06:10 +01:00
aa79f1b0a1 removed the version check again 2023-03-14 10:58:31 +01:00
bbb6d6c00c lower the required version 2023-03-14 10:47:06 +01:00
c6f034f2d3 require PyQtWebEngine only for Python version were it exists 2023-03-14 10:42:34 +01:00
a624cf37a4 added some more versions 2023-03-14 09:40:32 +01:00
efd023ae3a a bit of formatting and naming 2023-03-14 09:35:44 +01:00
d65e97c9a7 neater 2023-02-10 12:31:42 +01:00
4fcc10f3da added possibility to extend data 2023-02-10 11:21:54 +01:00
c781601246 added set_data; refactored sync_item_and_plots 2023-02-10 10:10:12 +01:00
831b03a744 added data setter 2023-02-10 10:09:43 +01:00
b94294b579 fixed __dir__ 2023-02-10 10:09:22 +01:00
b14f6e68b4 allow tab completion for the exposed functions on the client side 2023-02-08 21:03:03 +01:00
76e3a04d98 added addEntrybox for max #entries 2023-02-04 18:54:36 +01:00
c620f17344 added addEntrybox 2023-02-04 18:53:53 +01:00
495a9a43bc added eviction, attached it to list changes signal 2023-02-04 18:52:49 +01:00
ab1d0d524e correctly set descending order (except for "sort by text") 2023-02-03 00:59:52 +01:00
cf4fbad5e7 update timestamps; added "Sort by timestamp" 2023-02-03 00:56:07 +01:00
5ea830bf2b attached timestamps object 2023-02-03 00:55:08 +01:00
87ae26247d attached timestamps object 2023-02-03 00:54:00 +01:00
23afec2aa4 fill xs and ys as argument 2023-02-03 00:53:19 +01:00
e4ada964e3 (trying to) disable sorting if item manually moved 2023-02-02 17:27:20 +01:00
9b50ade340 introduced "Sort by insertion order" (the default) / "Sorting disabled" keeps current order 2023-02-02 16:48:18 +01:00
26c338c54e added a comment 2023-02-02 14:59:51 +01:00
721f2a864a added docstrings for API methods 2023-02-02 14:59:42 +01:00
4c533489f6 added wildcard (*?) support 2023-02-02 13:28:52 +01:00
c7c17ef221 change sort key on the class (not the instances) 2023-02-02 11:59:05 +01:00
3ba74a1a1e added a tests run script 2023-02-01 16:17:58 +01:00
9e6896c30f fixed position of *args 2023-02-01 16:10:47 +01:00
829c9e2ded use different name inside function 2023-02-01 16:10:24 +01:00
9a66433a3e made LINE_COLORS tuple instead of list 2023-02-01 16:09:59 +01:00
9db8d5b87e cleanup 2023-02-01 15:31:31 +01:00
b380d840fc attach sorting options to menu 2023-02-01 15:26:38 +01:00
31cbe8ac33 added possibility to enabling/disabling sorting for the list 2023-02-01 15:26:17 +01:00
b02d66b95f added possibility for having a sort key 2023-02-01 15:25:37 +01:00
660e8208ca added offline mode (does not start the RPC server) 2023-01-29 16:32:41 +01:00
f491c71462 adjusted test 2023-01-29 14:19:34 +01:00
16c7b4f71c added maximize/restore to MDISubWindow, changed frame_on/frame_off from FramelessWindowHint to custom hint 2023-01-29 14:05:18 +01:00
7d169fdd53 ci: improved ci pipeline; added 3.9 and 3.10 tests 2023-01-28 17:29:56 +01:00
aeaabd028f connect tooltip display with xy coords to plot mouse-over event 2023-01-27 18:50:34 +01:00
4e3d8b9da6 introduced MDISubPlotBase 2023-01-27 18:34:17 +01:00
29 changed files with 955 additions and 115 deletions

View File

@@ -1,29 +1,62 @@
stages:
- Test
- Tests
- OptionalTests
.install-grum-test: &install-grum-test
- pip install pytest pytest-random-order pytest-cov
- pip install -e ./
- apt-get update
- apt-get install -y ffmpeg libnss3 libxcomposite1 libxtst6
tests:
stage: Test
stage: Tests
image: python:3.8
variables:
QT_QPA_PLATFORM: "offscreen"
XDG_RUNTIME_DIR: "/tmp/runtime-root"
PYTHONFAULTHANDLER: 1
script:
- pip install pytest pytest-random-order pytest-cov
- pip install -e ./
- pip install PyQtWebEngine
- apt-get update
- apt-get install -y ffmpeg libnss3 libxcomposite1 libxtst6
# - python -m unittest discover -f ./tests
# - coverage run --source=./grum -m unittest discover -f ./tests
- coverage run --source=./grum -m pytest ./tests
- *install-grum-test
- coverage run --source=./grum -m pytest ./tests --junitxml=report-junit.xml
- coverage report
- coverage xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
when: always
reports:
cobertura: coverage.xml
junit: report-junit.xml
tests-3.6:
stage: OptionalTests
image: python:3.6
needs: ["tests"]
allow_failure: true
variables:
QT_QPA_PLATFORM: "offscreen"
XDG_RUNTIME_DIR: "/tmp/runtime-root"
PYTHONFAULTHANDLER: 1
script:
- *install-grum-test
- pytest ./tests
tests-3.7:
extends: "tests-3.6"
image: python:3.7
#tests-3.8:
# extends: "tests-3.6"
# image: python:3.8
tests-3.9:
extends: "tests-3.6"
image: python:3.9
tests-3.10:
extends: "tests-3.6"
image: python:3.10
tests-3.11:
extends: "tests-3.6"
image: python:3.11

183
grum/BECAdapter.py Normal file
View File

@@ -0,0 +1,183 @@
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"):
@@ -25,13 +25,14 @@ def handle_clargs():
DESC = "grum - GUI for Remote Unified Monitoring"
parser = argparse.ArgumentParser(description=DESC, formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-H", "--host", default="localhost", help="Server host name")
parser.add_argument("-P", "--port", default=8000, type=int, help="Server port number")
parser.add_argument("-H", "--host", default="localhost", help="RPC server host name")
parser.add_argument("-P", "--port", default=8000, type=int, help="RPC server port number")
parser.add_argument("-o", "--offline", action="store_true", help="offline mode (do not run RPC server)")
parser.add_argument("-w", "--window-mode", default="multi", choices=MDIWindowMode.values(), type=unambiguous_window_mode, help="Set the initial window mode")
parser.add_argument("-w", "--window-mode", default="multi", choices=MDIWindowMode.values(), type=unambiguous_window_mode, help="set the initial window mode")
parser.add_argument("-e", "--examples", dest="add_examples", action="store_true", help="Add example data")
parser.add_argument("--no-theme", action="store_true", help="Disable theming")
parser.add_argument("-e", "--examples", dest="add_examples", action="store_true", help="add example data")
parser.add_argument("--no-theme", action="store_true", help="disable theming")
return parser.parse_args().__dict__

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

View File

@@ -1,4 +1,7 @@
from PyQt5.QtWidgets import QWidget, QLineEdit, QVBoxLayout
import fnmatch
import re
from PyQt5.QtWidgets import QWidget, QVBoxLayout
from .dictlistwidget import DictListWidget
from .searchbox import SearchBox
@@ -31,11 +34,26 @@ class DictList(QWidget):
def hide_not_matching(self, pattern):
pattern = pattern.casefold()
g = Globber(pattern)
for name, itm in self.lst.items.items():
name = name.casefold()
state = (pattern not in name)
itm.setHidden(state)
state = g.match(name)
itm.setHidden(not state)
class Globber:
def __init__(self, pattern):
pattern = pattern.casefold()
pattern = "*" + pattern + "*"
regex = fnmatch.translate(pattern)
self.pattern = re.compile(regex)
def match(self, string):
string = string.casefold()
matches = self.pattern.match(string)
state = bool(matches)
return state

View File

@@ -3,6 +3,7 @@ from PyQt5.QtWidgets import QListWidgetItem
from PyQt5.QtGui import QIcon, QPixmap, QPainter
from ..theme import DOT_PEN, DOT_BRUSH, DOT_SIZE
from .timestamps import Timestamps
class DictListItem(QListWidgetItem):
@@ -11,9 +12,25 @@ class DictListItem(QListWidgetItem):
super().__init__(key, *args, **kwargs)
self.key = key
self.value = value
self.timestamps = Timestamps()
self.set_alarm(False)
_sort_key = None
@classmethod # need to update the class so that all instances are always consistent
def set_sort_key(cls, sk):
cls._sort_key = staticmethod(sk) # need to attach as staticmethod, otherwise self is inserted as first argument
def __lt__(self, other):
assert self._sort_key == other._sort_key
sk = self._sort_key
if sk:
return sk(self) < sk(other)
else:
return super().__lt__(other)
def set_alarm(self, state):
self.set_bold(state)
self.set_dot(state)

View File

@@ -14,8 +14,12 @@ class DictListWidget(QListWidget):
self.setSelectionMode(QListWidget.ExtendedSelection)
self.items = {}
self._add_menu()
shortcut(self, "Del", self.delete_selected, context=Qt.WidgetShortcut)
self.nkeep = None
self.model().rowsInserted.connect(self.on_evict)
def _add_menu(self):
self.menu = menu = RClickMenu(self)
@@ -72,4 +76,69 @@ class DictListWidget(QListWidget):
i.set_alarm(state)
def set_sort_key(self, sk, reverse=True):
DictListItem.set_sort_key(sk)
if sk is None:
self.setSortingEnabled(False)
else:
self.setSortingEnabled(True)
order = Qt.DescendingOrder if reverse else Qt.AscendingOrder
self.sortItems(order)
def enable_sort_by_insertion_order(self):
# map order in dict to indices
mapping = enumerate(self.items.values())
mapping = {item.text(): index for index, item in mapping}
def unsort(x):
return mapping[x.text()]
self.set_sort_key(unsort)
self.disable_sorting()
def enable_sort_by_text(self):
self.set_sort_key(lambda x: x.text(), reverse=False)
def enable_sort_by_timestamp(self):
# fall back to name for identical timestamps
self.set_sort_key(lambda x: (x.timestamps.max(), x.text()))
def disable_sorting(self):
self.set_sort_key(None)
def set_nkeep(self, n):
self.nkeep = n
self.on_evict()
def on_evict(self):
if self.nkeep:
self.evict(self.nkeep)
def evict(self, nkeep):
items = self.items.values()
# map order in dict to indices
mapping = enumerate(items)
mapping = {item.text(): index for index, item in mapping}
def sk(x):
# fall back to insertion order for identical timestamps
return (x.timestamps.max(), mapping[x.text()])
items = sorted(items, key=sk, reverse=True)
items_to_evict = items[nkeep:]
for i in items_to_evict:
self.items.pop(i.key)
self.deleteItem(i)

View File

@@ -47,7 +47,7 @@ class SearchBox(QWidget):
class SquareButton(QPushButton):
def resizeEvent(self, e):
def resizeEvent(self, _event):
height = self.height()
self.setMinimumWidth(height)

View File

@@ -20,15 +20,15 @@ for i in range(20):
exampledata = {}
for k, v in exampledata_raw.items():
pd = PlotDescription(
k,
# title=k,
for name, (xs, ys) in exampledata_raw.items():
exampledata[name] = PlotDescription(
name,
# title=name,
xlabel="x",
ylabel="y"
ylabel="y",
xs=xs,
ys=ys
)
pd.xs, pd.ys = v
exampledata[k] = pd

60
grum/imgdesc.py Normal file
View File

@@ -0,0 +1,60 @@
import numpy as np
import pyqtgraph as pg
from .theme import pg_legend_style
class ImageDescription:
def __init__(self, name, title=None, xlabel=None, ylabel=None, image=None, colormap=None, levels=None):
self.name = name
self.title = title
self.xlabel = xlabel
self.ylabel = ylabel
self.image = image
self.colormap = colormap
self.levels = levels
@property
def data(self):
return np.asarray(self.image)
@data.setter
def data(self, value):
self.image = value
def append(self, xy):
print("ignored image append")
def extend(self, data):
print("ignored image extend")
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)
if self.title:
plotwidget.setTitle(self.title)
if self.xlabel:
vbox = plotwidget.getView()
vbox.addItem(pg.LabelItem(self.xlabel, size='50pt'))
if self.ylabel:
plotwidget.setLabel("left", self.ylabel)
if self.colormap:
cm = pg.colormap.get(self.colormap)
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}

View File

@@ -31,7 +31,7 @@ def read_dict(fn):
data = node[()]
if isinstance(data, bytes):
data = data.decode("utf-8")
res[node.name] = data
res[name] = data
with h5py.File(fn, "r") as f:
f.visititems(visit)
@@ -44,7 +44,7 @@ def unflatten_dict(d, sep="/"):
for k, v in d.items():
current = res
levels = k.split(sep)
for l in levels[1:-1]:
for l in levels[:-1]:
if l not in current:
current[l] = {}
current = current[l]

View File

@@ -1,30 +1,38 @@
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QMainWindow, QSplitter
import pyqtgraph as pg
from . import assets
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, MDIWindowMode
from .mdi import MDIArea, MDISubMultiPlot, MDISubPlot, MDISubImage, MDIWindowMode, MDISubMultiImage
from .menus import BarMenu
from .plotdesc import PlotDescription
from .imgdesc import ImageDescription
from .rpc import RPCServerThread
from .shortcut import shortcut
from .webview import WebView
# from .webview import WebView # doesnt work for CSAXS version of pyqtWebEngine
class MainWindow(QMainWindow):
sig_make_new_plot = pyqtSignal(str, PlotDescription)
sig_make_new_image = pyqtSignal(str, ImageDescription)
def __init__(self, *args, title="grum", host="localhost", port=8000, add_examples=False, window_mode=MDIWindowMode.MULTI, **kwargs):
def __init__(self, *args, title="grum", host="localhost", port=8000, offline=False, add_examples=False, window_mode=MDIWindowMode.MULTI, **kwargs):
super().__init__(*args, **kwargs)
if offline:
title = f"{title} (offline)"
self.setWindowTitle(title)
self.setWindowIcon(assets.icon())
url = f"http://{host}:{port}/"
self.webdoc = WebView(url, title=title)
# self.webdoc = WebView(url, title=title) # doesnt work for CSAXS version of pyqtWebEngine
self.lst = lst = DictList()
lst.setAlternatingRowColors(True)
@@ -37,8 +45,26 @@ 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 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)
# TODO: clean up
def on_item_about_to_be_moved():
sort_group.checkboxes["Sorting disabled"].setChecked(True)
self.on_sorting_disabled()
self.lst.model().rowsAboutToBeMoved.connect(on_item_about_to_be_moved)
shortcut(self, "Ctrl+P", self.on_plot_selected)
@@ -52,6 +78,9 @@ 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)
self.mdi = mdi = MDIArea(bar, window_mode=window_mode)
@@ -62,45 +91,139 @@ class MainWindow(QMainWindow):
self.setCentralWidget(splitter)
self.rst = rst = RPCServerThread(host, port, doc_title_suffix=title)
rst.start()
rst.server.register_function(self.new_plot)
rst.server.register_function(self.append_data)
if not offline:
self.rst = rst = RPCServerThread(
host, port, doc_title_suffix=title)
rst.start()
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)
def keyPressEvent(self, event):
if event.key() == Qt.Key_F1:
self.webdoc.show()
# self.webdoc.show() # doesnt work for CSAXS version of pyqtWebEngine
pass
# Remote API calls
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)
if self.menu_settings.checkboxes["Open new plots"].isChecked():
if not self.mdi.findSubWindow(name):
self.sig_make_new_plot.emit(name, desc)
def append_data(self, name, point):
"""
Append a new data point <point> to the (existing) plot <name>.
The point is forwarded to the append method of PlotDescription.
"""
item = self.lst.get(name)
desc = item.value
desc.append(point)
alarm = True
for sub in self.mdi.subWindowList():
if name in sub.plots:
plot = sub.plots[name]
plot.setData(*desc.data)
alarm = False
item.set_alarm(alarm)
self.sync_item_and_plots(item)
def extend_data(self, name, data):
"""
Extend the current data of the (existing) plot <name>.by <data>
The data is forwarded to the extend method of PlotDescription.
"""
item = self.lst.get(name)
desc = item.value
desc.extend(data)
self.sync_item_and_plots(item)
def set_data(self, name, data):
"""
Set <data> as the data of the (existing) plot <name>.
The data is assigned to the data attribute of PlotDescription.
"""
item = self.lst.get(name)
desc = item.value
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_dclick_list_item(self, item):
self.plot_single_item(item)
@@ -111,6 +234,9 @@ 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)
@@ -120,6 +246,17 @@ class MainWindow(QMainWindow):
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()
def on_sort_by_name(self):
self.lst.enable_sort_by_text()
def on_sort_by_timestamp(self):
self.lst.enable_sort_by_timestamp()
def on_sorting_disabled(self):
self.lst.disable_sorting()
def on_file_open(self):
fns = open_h5_files_dialog(self)
@@ -129,8 +266,8 @@ class MainWindow(QMainWindow):
for fn in fns:
data = read_dict(fn)
for k, v in data.items():
self.add_new_desc_to_list(k, v)
Desc = ImageDescription if "image" in v else PlotDescription # TODO
self.add_new_desc_to_list(k, v, Desc=Desc)
def on_file_save(self):
fn = save_h5_file_dialog(self)
@@ -144,33 +281,55 @@ class MainWindow(QMainWindow):
write_dict(fn, data)
# Plumbing
def add_new_desc_to_list(self, name, cfg):
desc = PlotDescription(name, **cfg)
def add_new_desc_to_list(self, name, cfg, Desc=PlotDescription): # TODO
desc = Desc(name, **cfg)
self.lst.set(name, desc)
return desc
def sync_item_and_plots(self, item):
name, desc = item.key, item.value
alarm = True
for sub in self.mdi.subWindowList():
if name in sub.plots:
plot = sub.plots[name]
plot.setData(*desc.data)
alarm = False
item.timestamps.modification.update()
item.set_alarm(alarm)
def plot_single_item(self, item):
item.timestamps.access.update()
item.set_alarm(False)
name, desc = item.key, item.value
self.activate_or_make_subwin(MDISubPlot, name, desc)
MDISubType = MDISubImage if isinstance(
desc, ImageDescription) else MDISubPlot # TODO
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)
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 to 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)
@@ -178,6 +337,3 @@ class MainWindow(QMainWindow):
def make_subwin(self, MDISubType, name, *args, **kwargs):
sub = MDISubType(name, *args, **kwargs)
self.mdi.add(sub)

View File

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

View File

@@ -20,7 +20,7 @@ class MDIWindowMode(str, enum.Enum):
class MDIArea(QMdiArea):
def __init__(self, bar, window_mode=MDIWindowMode.MULTI, *args, **kwargs):
def __init__(self, bar, *args, window_mode=MDIWindowMode.MULTI, **kwargs):
super().__init__(*args, **kwargs)
self.logo = assets.logo()
self.setTabsClosable(True)
@@ -67,6 +67,7 @@ class MDIArea(QMdiArea):
self.menu.checkboxes["Multiple windows"].setChecked(True)
self.enable_subwindow_view()
for sub in self.subWindowList():
sub.restore()
sub.frame_on()
def enable_single_window_mode(self):
@@ -75,7 +76,7 @@ class MDIArea(QMdiArea):
self.closeInactiveSubWindows()
active = self.activeSubWindow()
if active:
active.showMaximized()
active.maximize()
active.frame_off()
def enable_tabbed_mode(self):
@@ -104,7 +105,7 @@ class MDIArea(QMdiArea):
def add_single(self, sub):
self.closeAllSubWindows()
self.addSubWindow(sub)
sub.showMaximized()
sub.maximize()
sub.frame_off()

97
grum/mdi/mdisubimg.py Normal file
View File

@@ -0,0 +1,97 @@
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):
def __init__(self, name, desc, *args, **kwargs):
super().__init__(name, *args, **kwargs)
self.pw = pw = pg.ImageView()
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()
y = coord.y()
x = int(x)
y = int(y)
try:
z = self.image[x, y]
except IndexError:
return
z = round(z, 3)
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

@@ -3,30 +3,48 @@ from .mdisubwin import MDISubWindow
from ..theme import pg_plot_style, pg_plot_style_cycler, pg_legend_style
class MDISubPlot(MDISubWindow):
class MDISubPlotBase(MDISubWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pw = pw = pg.PlotWidget()
self.setWidget(pw)
# connect to plot mouse-over event
pw.scene().sigMouseMoved.connect(self.on_hover)
def on_hover(self, event):
coord = self.pw.plotItem.vb.mapSceneToView(event)
x = coord.x()
y = coord.y()
x = round(x, 3)
y = round(y, 3)
self.setToolTip(f"x = {x}\ny = {y}")
class MDISubPlot(MDISubPlotBase):
def __init__(self, name, desc, *args, **kwargs):
super().__init__(name, *args, **kwargs)
pw = pg.PlotWidget()
self.setWidget(pw)
style = pg_plot_style()
plot = desc.make_plot(pw, style)
plot = desc.make_plot(self.pw, style)
self.plots = {name: plot}
class MDISubMultiPlot(MDISubWindow):
class MDISubMultiPlot(MDISubPlotBase):
def __init__(self, name, descs, *args, **kwargs):
super().__init__(name, *args, **kwargs)
pw = pg.PlotWidget()
self.setWidget(pw)
ls = pg_legend_style()
psc = pg_plot_style_cycler()
pw = self.pw
pw.addLegend(**ls)
self.plots = {name: desc.make_plot(pw, style) for (name, desc), style in zip(descs.items(), psc)}

View File

@@ -14,12 +14,27 @@ class MDISubWindow(QMdiSubWindow):
# without this, the SubWindow is not removed from the subWindowList
self.setAttribute(Qt.WA_DeleteOnClose)
self._previous_state = None
def maximize(self):
self._previous_state = state = self.windowState()
self.setWindowState(state | Qt.WindowMaximized)
def restore(self):
if self._previous_state:
self.setWindowState(self._previous_state)
def frame_on(self):
self.setWindowFlag(Qt.FramelessWindowHint, False)
self.hide()
self.setWindowFlags(Qt.SubWindow)
self.show()
def frame_off(self):
self.setWindowFlag(Qt.FramelessWindowHint, True)
self.hide()
self.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowTitleHint)
self.show()

View File

@@ -1,5 +1,5 @@
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QAction, QActionGroup
from PyQt5.QtGui import QKeySequence, QIntValidator
from PyQt5.QtWidgets import QAction, QActionGroup, QWidgetAction, QLineEdit
class MenuBase:
@@ -31,6 +31,48 @@ class MenuBase:
self.checkboxes[name] = action
return action
def addEntrybox(self, name, placeholder=None, state=False, triggered=None):
cb = self.addCheckbox(name, state=state)
edit = QLineEdit(self.qmenu)
edit.setValidator(QIntValidator()) #TODO: make optional
edit.setEnabled(state)
if placeholder:
edit.setPlaceholderText(placeholder)
edit.setContentsMargins(8, 5, 8, 5) # mimic margins of other actions TODO: this probably depends on the theme
@cb.toggled.connect
def propagate_state_and_keep_menu_open_and_focus_edit(checked):
edit.setEnabled(checked)
if checked:
self.qmenu.show()
self.qmenu.setActiveAction(cb)
edit.setFocus()
@edit.returnPressed.connect
def close_menu():
self.qmenu.close()
if triggered:
@cb.triggered.connect
def confirm_disabled(checked):
if not checked:
triggered(None)
if triggered:
@edit.returnPressed.connect
def confirm_value():
value = edit.text()
value = int(value)
triggered(value)
action = QWidgetAction(self.qmenu)
action.setDefaultWidget(edit)
self.qmenu.addAction(action)
return action
def addSeparator(self):
self.qmenu.addSeparator()

View File

@@ -0,0 +1,78 @@
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,28 +1,45 @@
from pyqtgraph import DateAxisItem
from collections import deque
class PlotDescription:
def __init__(self, name, title=None, xlabel=None, ylabel=None, xs=None, ys=None):
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 = [] if xs is None else list(xs)
self.ys = [] if ys is None else list(ys)
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):
res = plotwidget.plot(self.xs, self.ys, name=self.name, **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)
@@ -35,9 +52,15 @@ class PlotDescription:
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

@@ -16,4 +16,10 @@ class RPCClient(xrc.ServerProxy):
return head + help
def __dir__(self):
d1 = super().__dir__()
d2 = self.utils.info().keys()
return [*d1, *d2]

View File

@@ -4,7 +4,7 @@ from inspect import getdoc, signature
class RPCServer(xrs.DocXMLRPCServer):
def __init__(self, host, port, doc_title_suffix="", *args, **kwargs):
def __init__(self, host, port, *args, doc_title_suffix="", **kwargs):
addr = (host, port)
kwargs.setdefault("allow_none", True)
super().__init__(addr, *args, **kwargs)

View File

@@ -4,9 +4,9 @@ from PyQt5.QtWidgets import QShortcut
def shortcut(parent, key_sequence, triggered, **kwargs):
key_sequence = QKeySequence(key_sequence)
shortcut = QShortcut(key_sequence, parent, **kwargs)
shortcut.activated.connect(triggered)
return shortcut
sc = QShortcut(key_sequence, parent, **kwargs)
sc.activated.connect(triggered)
return sc

View File

@@ -27,7 +27,7 @@ DOT_PEN = GREY5
DOT_BRUSH = HIGHLIGHT
DOT_SIZE = 8
LINE_COLORS = [
LINE_COLORS = (
C.BLUE,
C.BASE_GREEN,
C.RED,
@@ -38,7 +38,7 @@ LINE_COLORS = [
C.COOL3,
C.BROWN1,
C.BASE_ORANGE,
]
)

BIN
mytestfile.hdf5 Normal file

Binary file not shown.

View File

@@ -6,7 +6,7 @@ long_description = file: README.md
long_description_content_type = text/markdown
url = https://gitlab.psi.ch/augustin_s/grum
project_urls =
Bug Tracker = https://gitlab.psi.ch/augustin_s/grum/issues
Bug Tracker = https://gitlab.psi.ch/augustin_s/grum/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
@@ -16,7 +16,7 @@ classifiers =
package_dir =
= .
packages = find:
python_requires = >=3.8
python_requires = >=3.6
[options.packages.find]
where = .

View File

@@ -1,8 +1,18 @@
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", "pyqtgraph", "h5py"],
install_requires=["pyqt5==5.12", "pyqtgraph", "h5py", "PyQtWebEngine==5.12", "Pillow"], #this version works for comp1 CSAXS, no later version
entry_points={"console_scripts": ["grum=grum:main"]},
)

12
tests/run.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
cd $(dirname $0)
cd ..
export QT_QPA_PLATFORM=offscreen
coverage run --source=./grum/ -m pytest ./tests/
echo
coverage report

View File

@@ -143,14 +143,14 @@ class TestMDIArea:
mdi.closeAllSubWindows = mock.MagicMock()
sine_item = self.mw.lst.lst.get("sine")
sub = MDISubPlot("sine", sine_item.value)
sub.showMaximized = mock.MagicMock()
sub.maximize = mock.MagicMock()
sub.frame_off = mock.MagicMock()
mdi.add_single(sub)
mdi.closeAllSubWindows.assert_called_once()
mdi.addSubWindow.assert_called_once_with(sub)
sub.showMaximized.assert_called_once()
sub.maximize.assert_called_once()
sub.frame_off.assert_called_once()