Compare commits
76 Commits
tests
...
recon_plot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6ac86bfcc | ||
|
|
10c9332940 | ||
|
|
b6c47a903f | ||
|
|
b464daffca | ||
|
|
1bd1029cfd | ||
|
|
4f8d84c316 | ||
|
|
4414202f92 | ||
|
|
9228562c9a | ||
|
|
76da81b509 | ||
|
|
cac184be05 | ||
|
|
99ac3d85b9 | ||
|
|
372d3ff4c8 | ||
|
|
68abc169e0 | ||
|
|
a9abbb227c | ||
|
|
a07fad5cb7 | ||
|
|
00f06b5d85 | ||
|
|
16f149a937 | ||
|
|
221855e72c | ||
|
|
830879cbe6 | ||
|
|
61e732e941 | ||
|
|
59de72ba04 | ||
| 7470f1388b | |||
|
|
1ccde730e0 | ||
|
|
de1e1a3fec | ||
|
|
37c77f57d2 | ||
|
|
b68762550c | ||
|
|
a094567f29 | ||
|
|
facf92f498 | ||
|
|
28ebdae35b | ||
|
|
b570f0c885 | ||
|
|
b455b24c39 | ||
|
|
e75119093c | ||
| a80b568894 | |||
| 2edc68151d | |||
| aa03291e42 | |||
| 5d97dd6224 | |||
| bed086f95f | |||
| aa79f1b0a1 | |||
| bbb6d6c00c | |||
| c6f034f2d3 | |||
| a624cf37a4 | |||
| efd023ae3a | |||
| d65e97c9a7 | |||
| 4fcc10f3da | |||
| c781601246 | |||
| 831b03a744 | |||
| b94294b579 | |||
| b14f6e68b4 | |||
| 76e3a04d98 | |||
| c620f17344 | |||
| 495a9a43bc | |||
| ab1d0d524e | |||
| cf4fbad5e7 | |||
| 5ea830bf2b | |||
| 87ae26247d | |||
| 23afec2aa4 | |||
| e4ada964e3 | |||
| 9b50ade340 | |||
| 26c338c54e | |||
| 721f2a864a | |||
| 4c533489f6 | |||
| c7c17ef221 | |||
| 3ba74a1a1e | |||
| 9e6896c30f | |||
| 829c9e2ded | |||
| 9a66433a3e | |||
| 9db8d5b87e | |||
| b380d840fc | |||
| 31cbe8ac33 | |||
| b02d66b95f | |||
| 660e8208ca | |||
| f491c71462 | |||
| 16c7b4f71c | |||
| 7d169fdd53 | |||
| aeaabd028f | |||
| 4e3d8b9da6 |
@@ -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
183
grum/BECAdapter.py
Normal 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()
|
||||
13
grum/cli.py
13
grum/cli.py
@@ -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__
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class SearchBox(QWidget):
|
||||
|
||||
class SquareButton(QPushButton):
|
||||
|
||||
def resizeEvent(self, e):
|
||||
def resizeEvent(self, _event):
|
||||
height = self.height()
|
||||
self.setMinimumWidth(height)
|
||||
|
||||
|
||||
@@ -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
60
grum/imgdesc.py
Normal 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}
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
218
grum/mainwin.py
218
grum/mainwin.py
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
from .mdiarea import MDIArea, MDIWindowMode
|
||||
from .mdisubplot import MDISubPlot, MDISubMultiPlot
|
||||
from .mdisubimg import MDISubImage, MDISubMultiImage
|
||||
|
||||
|
||||
|
||||
@@ -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
97
grum/mdi/mdisubimg.py
Normal 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'))
|
||||
@@ -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)}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
78
grum/plot_multiple_phases.py
Normal file
78
grum/plot_multiple_phases.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
BIN
mytestfile.hdf5
Normal file
Binary file not shown.
@@ -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 = .
|
||||
|
||||
12
setup.py
12
setup.py
@@ -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
12
tests/run.sh
Executable 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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user