diff --git a/epoc-config.yaml b/epoc-config.yaml new file mode 100644 index 0000000..87048c4 --- /dev/null +++ b/epoc-config.yaml @@ -0,0 +1,42 @@ +# EPOC Redis Configuration + +# Experiment metadata +affiliation: "YourInstitution" +PI_name: "DefaultPI" +project_id: "ProjectID" +experiment_class: "External" +measurement_tag: "sample1" + +# Data directories +base_data_dir: "/data/jungfrau" +XDS_template: "/path/to/XDS.INP" +cal_dir: "/path/to/calibration" + +# Detector settings (e.g. of 1 Megapixel JUNGFRAU) +nrows: 1064 +ncols: 1030 +threshold: 5 + +# Viewer settings +viewer_interval: 20.0 +viewer_cmin: 0.0 +viewer_cmax: 12000.0 + +# Network endpoints +jfjoch_host: "http://jungfrau-server:5232" # Replace by the HTTP address of the Jungfraujoch web GUI +receiver_endpoint: "tcp://jungfrau-server:5000" # look at the ZMQ stream endpoint in the Jungfraujoch Web GUI +temserver: "temserver" # IP address of the TEM control machine (define in /etc/hosts) + +# Acquisition settings +rotation_speed_idx: 0 +file_id: 0 + +# Display overlays (optional) +overlays: + - {'type': 'circle', 'xy': [526, 253], 'radius': 188, 'ec': 'r', 'fill': False, 'lw': 2} + - {'type': 'circle', 'xy': [526, 253], 'radius': 5, 'ec': 'g', 'fill': False, 'lw': 2} + - {'type': 'rectangle', 'xy': [574, 52], 'width': 60, 'height': 31, 'angle': 27.5, 'ec': 'y', 'fill': False} + - {'type': 'rectangle', 'xy': [0, 0], 'width': 1000, 'height': 1, 'angle': 19.8, 'ec': 'r', 'fill': False} + +# Used affiliations list +usedAffiliations: ["YourInstitution", "External"] \ No newline at end of file diff --git a/init_redis.py b/init_redis.py new file mode 100644 index 0000000..5d0fe1b --- /dev/null +++ b/init_redis.py @@ -0,0 +1,11 @@ +from pathlib import Path +from epoc import ConfigurationClient, auth_token, redis_host + +# Connect to Redis +cfg = ConfigurationClient(redis_host(), token=auth_token()) + +# Load configuration from YAML (flush_db=True clears any existing data) +cfg.from_yaml(Path('epoc-config.yaml'), flush_db=True) + +print("Redis database initialized successfully!") +print(cfg) \ No newline at end of file diff --git a/jungfrau_gui/globals.py b/jungfrau_gui/globals.py index 1db1125..1172e91 100644 --- a/jungfrau_gui/globals.py +++ b/jungfrau_gui/globals.py @@ -1,7 +1,6 @@ import ctypes import numpy as np import multiprocessing as mp -from epoc import ConfigurationClient, auth_token, redis_host import subprocess def get_git_info(): @@ -49,46 +48,113 @@ def get_git_info(): # Git not installed or command failed return defaults -cfg = ConfigurationClient(redis_host(), token=auth_token()) +# ---------------------------- +# Runtime-configurable globals +# ---------------------------- stream = "tcp://localhost:4545" tem_mode = True -# jfj = False +tem_host = "localhost" +tem_port = 3535 -tem_host = cfg.temserver dev = False -#Configuration -nrow = cfg.nrows -ncol = cfg.ncols + +nrow = 0 +ncol = 0 dtype = np.float32 cdtype = ctypes.c_float -# fitterWorkerReady = mp.Value(ctypes.c_bool) -# fitterWorkerReady.value = False - accframes = 0 -acc_image = np.zeros((nrow,ncol), dtype = dtype) +acc_image = np.zeros((0, 0), dtype=dtype) # allocated properly in init() exit_flag = mp.Value(ctypes.c_bool) exit_flag.value = False -#Data type to write to file +# Data type to write to file file_dt = np.int32 -#Data type to receive from the stream +# Data type to receive from the stream stream_dt = np.float32 # Flags for non-updated magnification values in MAG and DIFF modes -mag_value_img = [1, 'X', 'X1'] -mag_value_diff = [1, 'mm', '1cm'] +mag_value_img = [1, "X", "X1"] +mag_value_diff = [1, "mm", "1cm"] -tag, branch, commit = get_git_info() +# Version info (safe at import; no Redis!) +tag, branch, commit = get_git_info() # constants, presets -UM_TO_NM = 1e3 -MM_TO_UM = 1e3 -KV_TO_V = 1e3 +UM_TO_NM = MM_TO_UM = MS_TO_US = S_TO_MS = KV_TO_V = 1e3 PIXEL = 0.075 # mm +# TEM control variables default_HT = 200000.00 # V +# Backlash correction (KT) backlash = [100, 80, 0, 0] +# overshoot/preload used for two-step jog moves +preload = [2000, 2000, 0, 1] # X,Y,Z in nm (2 µm), TX in deg (1°) + +min_mag_for_mag = 1500 # border between LowMag/Mag + +## safety not to hit hardware-limit +click_on_move_thresholds = {'dxy_min': 0.3, 'dxy_max': 100, + 'dz_min_mag': 1, 'dz_max_mag': 10, + 'dz_min_lmag': 3, 'absz_min': -70, 'absz_max': 20} + +## stage shift larger than these values will be hold in history +stage_relaxation_thresholds = [30, 30, 30, 0.2, 100] # nm, nm, nm, deg., deg. + +## software limit for GATAN holder. Smaller value (~65) may be necessary for complete safety (e.g. remote-operation). +max_stage_tilt = 72 +default_roation_end = 60 + +## variabls for autofocusing +IL1_0 = 21780 # 21819 +ILS_0 = [32920, 32776] # [32820, 32976] +WAIT_TIME_S = 0.25 # TODO: optimize value + + +# Frame control variables +default_polling_frequency = 1000 +min_polling_frequency = 100 # safety not to inquire TEM-values too frequently +max_polling_frequency = 10000 + +default_frame_summed = 100 +default_image_time_us = 500 +min_frame_summed = 10 # safety not to save unexpectedly large datasets +max_frame_summed = 1000 +detector_freq = 2000 +max_duration = 3600 # sec + + +# Communication variables +dataserver_host = "noether" +dataserver_port = 3463 + +max_retries_tagging = 3 +inquiry_delay = 0.1 # sec + + +def init(*, stream_, dtype_, cdtype_, tem_mode_, tem_host_, dev_, nrow_, ncol_): + """ + Initialize globals that previously depended on Redis at import-time. + + Call this exactly once in launch_gui.py *before* importing modules that use globals. + """ + global stream, dtype, cdtype, tem_mode, tem_host, dev, nrow, ncol, acc_image + + stream = stream_ + dtype = np.dtype(dtype_) + cdtype = cdtype_ + + tem_mode = bool(tem_mode_) + tem_host = str(tem_host_) + dev = bool(dev_) + + nrow = int(nrow_) + ncol = int(ncol_) + + if nrow <= 0 or ncol <= 0: + raise ValueError(f"Invalid detector geometry: nrow={nrow}, ncol={ncol}") + + acc_image = np.zeros((nrow, ncol), dtype=dtype) \ No newline at end of file diff --git a/jungfrau_gui/main_ui.py b/jungfrau_gui/main_ui.py index ac62a19..2b26deb 100755 --- a/jungfrau_gui/main_ui.py +++ b/jungfrau_gui/main_ui.py @@ -7,12 +7,10 @@ import numpy as np import time from PySide6.QtWidgets import QApplication -from PySide6.QtCore import QCoreApplication -from . import globals -from .ui_components import palette -from .zmq_receiver import ZmqReceiver -from .ui_main_window import ApplicationWindow, get_gui_info +from jungfrau_gui.ui_components import palette +from jungfrau_gui.zmq_receiver import ZmqReceiver +from jungfrau_gui.ui_main_window import ApplicationWindow, get_gui_info from pathlib import Path from epoc import ConfigurationClient, auth_token, redis_host @@ -20,6 +18,61 @@ import os import datetime +import textwrap + +ABOUT_TEXT = textwrap.dedent("""\ + ┌───────────────────────────────────────────────────────────────────┐ + │ Graphical User Interface for Electron Diffraction (JUNGFRAU GUI) │ + └───────────────────────────────────────────────────────────────────┘ + + Project: EPOC (Electrostatic Potential Of Compounds ─ DOI: 10.55776/I6546) + Years: 2024– + Version: {version} + + Repositories: + - https://github.com/epoc-ed/GUI + - https://github.com/epoc-ed + + Documentation: + - https://epoc-ed.github.io/manual/index.html + + License: + - MIT License + This project is distributed under the MIT License. + See the LICENSE file for the full text. + + Authors & Acknowledgments + Core contributors: + - Khalil Ferjaoui — PSI + - Kiyofumi Takaba — University of Vienna + - Erik Fröjd — PSI + - Tim Gruene — University of Vienna +""") + +def log_version_info(version: str) -> None: + logging.info("\n%s", ABOUT_TEXT.format(version=version)) + + +def _cfg_get(cfg, name: str, default=None): + """ + Safely read a config attribute from ConfigurationClient. + + Some properties (e.g. cfg.temserver) raise ValueError if missing. + """ + try: + return getattr(cfg, name) + except (ValueError, AttributeError): + return default + + +def _parse_dtype(dtype_str: str) -> np.dtype: + s = (dtype_str or "").strip().lower() + if s in ("float32", "f4", "np.float32"): + return np.dtype(np.float32) + if s in ("float64", "double", "f8", "np.float64", "np.double"): + return np.dtype(np.float64) + raise ValueError(f"Unknown dtype '{dtype_str}'. Use float32 or float64.") + class CustomFormatter(logging.Formatter): # Define color codes for different log levels and additional styles # Foreground (text) colors @@ -88,91 +141,107 @@ def main(): app = QApplication(sys.argv) app.setStyle("Fusion") - - cfg = ConfigurationClient(redis_host(), token=auth_token()) + # ---- CLI ---- parser = argparse.ArgumentParser() - parser.add_argument('-s', '--stream', type=str, default="tcp://noether:5501", help="zmq stream") # default="tcp://localhost:4545" - parser.add_argument("-d", "--dtype", help="Data type", type = np.dtype, default=np.float32) - parser.add_argument("-p", "--playmode", action="store_true", help="Activates simplified GUI") - parser.add_argument("-th", "--temhost", default=cfg.temserver, help="Choose host for tem-gui communication") - parser.add_argument('-l', '--log', default='INFO', help='Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)') - parser.add_argument("-f", "--logfile", action="store_true", help="File-output of logging") - parser.add_argument("-e", "--dev", action="store_true", help="Activate developing function") - parser.add_argument("-v", "--version", action="store_true", help="Detailed version description") - + parser.add_argument("-s", "--stream", type=str, default=None, + help="ZMQ stream endpoint (overrides cfg.receiver_endpoint)") + parser.add_argument("-d", "--dtype", type=str, default="float32", + help="Data type (float32 or float64)") + parser.add_argument("-p", "--playmode", action="store_true", + help="Activates simplified GUI") + parser.add_argument("-th", "--temhost", default=None, + help="Host for tem-gui communication (overrides cfg.temserver)") + parser.add_argument("--nrow", type=int, default=None, + help="Override detector rows (overrides cfg.nrows)") + parser.add_argument("--ncol", type=int, default=None, + help="Override detector cols (overrides cfg.ncols)") + parser.add_argument("-l", "--log", default="INFO", + help="Set logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)") + parser.add_argument("-f", "--logfile", action="store_true", + help="File-output of logging") + parser.add_argument("-e", "--dev", action="store_true", + help="Activate developing function") + parser.add_argument("-v", "--version", action="store_true", + help="Detailed version description") args = parser.parse_args() - # Initialize logger + # ---- Logger setup ---- logger = logging.getLogger() - - # Dynamically set the log level based on args.log - log_level = getattr(logging, args.log.upper(), None) + log_level = getattr(logging, args.log.upper(), None) if log_level is None: - raise ValueError(f"Invalid log level: {args.log}. Choose from DEBUG, INFO, WARNING, ERROR, CRITICAL.") - + raise ValueError( + f"Invalid log level: {args.log}. " + "Choose from DEBUG, INFO, WARNING, ERROR, CRITICAL." + ) logger.setLevel(log_level) - # Create the handler for console output console_handler = logging.StreamHandler() - - # Apply the custom formatter to the handler - formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s') + formatter = CustomFormatter("%(asctime)s - %(levelname)s - %(message)s") console_handler.setFormatter(formatter) - - # Add the handler to the logger logger.addHandler(console_handler) if args.logfile: - - # Determine the directory of the script being run launch_script_path = Path(sys.argv[0]).resolve().parent - log_file_path = launch_script_path / f'JFGUI{time.strftime("_%Y%m%d-%H%M%S.log", time.localtime())}' - - logging.info(f"Writing console loggings to: {log_file_path}") # Debugging line to verify file creation - + log_file_path = launch_script_path / ( + f'JFGUI{time.strftime("_%Y%m%d-%H%M%S.log", time.localtime())}' + ) + logging.info("Writing console loggings to: %s", log_file_path) file_handler = logging.FileHandler(log_file_path.as_posix()) file_handler.setLevel(log_level) - file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%H:%M:%S') - file_handler.setFormatter(file_formatter) + file_handler.setFormatter(logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s", datefmt="%H:%M:%S", + )) logger.addHandler(file_handler) - if args.dtype == np.float32: - globals.cdtype = ctypes.c_float - elif args.dtype == np.double: - cdtype = ctypes.c_double - else: - raise ValueError("unknown data type") - - # Update the type of global variables - globals.stream = args.stream - globals.dtype = args.dtype - globals.acc_image = np.zeros((globals.nrow,globals.ncol), dtype = args.dtype) - globals.tem_mode = not args.playmode - globals.tem_host = args.temhost - globals.dev = args.dev - - logging.info(f"{get_gui_info()}") + # ---- dtype ---- + dtype = _parse_dtype(args.dtype) + cdtype = ctypes.c_float if dtype == np.dtype(np.float32) else ctypes.c_double + + # ---- Redis (mandatory) ---- + try: + cfg = ConfigurationClient(redis_host(), token=auth_token()) + except Exception as e: + logging.critical("Cannot connect to Redis configuration server: %s", e) + sys.exit(1) + + logging.info("Connected to Redis configuration.") + + # ---- Resolve parameters: CLI override Redis ---- + stream = args.stream or cfg.receiver_endpoint + tem_host = args.temhost or cfg.temserver + nrow = args.nrow if args.nrow is not None else cfg.nrows + ncol = args.ncol if args.ncol is not None else cfg.ncols + + # ---- Initialize globals ---- + from jungfrau_gui import globals + + globals.init( + stream_=stream, + dtype_=dtype, + cdtype_=cdtype, + tem_mode_=not args.playmode, + tem_host_=tem_host, + dev_=args.dev, + nrow_=nrow, + ncol_=ncol, + ) + + info = get_gui_info() + logging.info("%s", info) if args.version: - logging.info(''' - **Detailed information of authors, years, project name, Github URL, license, contact address, etc.** - Graphical User Interface for Electron Diffraction with JUNGFRAU (2024-) - https://github.com/epoc-ed/GUI - EPOC Project (2024-) - https://github.com/epoc-ed - https://epoc-ed.github.io/manual/index.html - ''') + version = info.removeprefix("Jungfrau GUI ").strip() + log_version_info(version) + raise SystemExit(0) - Rcv = ZmqReceiver(endpoint=args.stream, dtype=args.dtype) + Rcv = ZmqReceiver(endpoint=stream, dtype=dtype) viewer = ApplicationWindow(Rcv, app) - app_palette = palette.get_palette("dark") - viewer.setPalette(app_palette) - + viewer.setPalette(palette.get_palette("dark")) viewer.show() - # QCoreApplication.processEvents() sys.exit(app.exec()) + if __name__ == "__main__": main() diff --git a/jungfrau_gui/metadata_uploader/metadata_update_client.py b/jungfrau_gui/metadata_uploader/metadata_update_client.py index 30b2949..b2c5f5e 100644 --- a/jungfrau_gui/metadata_uploader/metadata_update_client.py +++ b/jungfrau_gui/metadata_uploader/metadata_update_client.py @@ -8,7 +8,7 @@ from datetime import datetime import argparse from pathlib import Path -from .. import globals +from jungfrau_gui import globals # Handle imports correctly when running as a standalone script if __name__ == "__main__" and __package__ is None: @@ -33,7 +33,7 @@ def default(self, obj): return super().default(obj) class MetadataNotifier: - def __init__(self, host, port=3463, verbose = True): + def __init__(self, host, port=globals.dataserver_port, verbose = True): self.host = host self.port = port self.verbose = verbose diff --git a/jungfrau_gui/ui_components/file_operations/file_operations.py b/jungfrau_gui/ui_components/file_operations/file_operations.py index 4e0052a..4210aea 100644 --- a/jungfrau_gui/ui_components/file_operations/file_operations.py +++ b/jungfrau_gui/ui_components/file_operations/file_operations.py @@ -4,14 +4,14 @@ from PySide6.QtWidgets import (QGroupBox, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QCheckBox, QComboBox, QCompleter) -from ...ui_components.toggle_button import ToggleButton -from ...ui_components.utils import create_horizontal_line_with_margin -from ...ui_components.palette import * -from ...ui_components.tem_controls.toolbox.tool import send_with_retries -from ...metadata_uploader.metadata_update_client import MetadataNotifier +from jungfrau_gui.ui_components.toggle_button import ToggleButton +from jungfrau_gui.ui_components.utils import create_horizontal_line_with_margin +from jungfrau_gui.ui_components.palette import * +from jungfrau_gui.ui_components.tem_controls.toolbox.tool import send_with_retries +from jungfrau_gui.metadata_uploader.metadata_update_client import MetadataNotifier from epoc import ConfigurationClient, auth_token, redis_host -from ... import globals +from jungfrau_gui import globals import os import re @@ -81,7 +81,7 @@ def __init__(self, parent): self.cfg = ConfigurationClient(redis_host(), token=auth_token()) self.trigger_update_h5_index_box.connect(self.update_index_box) self.initUI() - self.metadata_notifier = MetadataNotifier(host = "noether", port = 3463, verbose = False) + self.metadata_notifier = MetadataNotifier(host = globals.dataserver_host, port = globals.dataserver_port, verbose = False) def initUI(self): @@ -421,11 +421,11 @@ def toggle_snapshot_btn(self): if globals.dev: prev_image_time_us = self.parent.visualization_panel.jfjoch_client.image_time_us # 50000 self.frame_summed_for_rotation = self.parent.visualization_panel.frame_summed.value() - frame_summed_for_snapshot = prev_image_time_us // 500 # 100 + frame_summed_for_snapshot = prev_image_time_us // globals.default_image_time_us # 100 self.parent.visualization_panel.frame_summed.setValue(frame_summed_for_snapshot) self.parent.visualization_panel.send_command_to_jfjoch('collect') - logging.info(f'Snapshot duration: {int(self.snapshot_spin.value())*1e-3} sec') + logging.info(f'Snapshot duration: {int(self.snapshot_spin.value())/globals.S_TO_MS} sec') QTimer.singleShot(self.snapshot_spin.value(), self.toggle_snapshot_btn) else: # Cancel collection @@ -487,8 +487,8 @@ def _send_metadata_with_retries(self, beam_property): beam_property, None, # self.rotations_angles, self.cfg.threshold, - retries=3, - delay=0.1 + retries=globals.max_retries_tagging, + delay=globals.inquiry_delay ) logging.info("Metadata update completed successfully") # Signal success back to the main thread @@ -523,7 +523,7 @@ def _disconnect_metadata_signals(self): def _finalize_snapshot(self): """Reset UI state after snapshot completion""" - logging.info(f'Snapshot duration end: {int(self.snapshot_spin.value())*1e-3} sec') + logging.info(f'Snapshot duration end: {int(self.snapshot_spin.value())/globals.S_TO_MS} sec') self.tag_input.setText(self.pre_text) # reset the tag to value before snapshot self.update_measurement_tag() self.snapshot_button.setText("Write Stream as a snapshot-H5") diff --git a/jungfrau_gui/ui_components/file_operations/processresult_updater.py b/jungfrau_gui/ui_components/file_operations/processresult_updater.py index 637a0a3..767fd5a 100644 --- a/jungfrau_gui/ui_components/file_operations/processresult_updater.py +++ b/jungfrau_gui/ui_components/file_operations/processresult_updater.py @@ -5,7 +5,7 @@ from datetime import datetime import argparse from pathlib import Path -# from .. import globals +from jungfrau_gui import globals from PySide6.QtCore import Signal, Slot, QObject import time @@ -26,7 +26,7 @@ class ProcessedDataReceiver(QObject): finished = Signal() - def __init__(self, parent, host, port=3463, verbose = True, mode=0): + def __init__(self, parent, host, port=globals.dataserver_port, verbose = True, mode=0): super().__init__() self.task_name = "Processed Data Receiver" self.parent = parent @@ -61,7 +61,7 @@ def run(self, timeout_ms = 5000, update_interval_ms=2000, n_retry=10, verbose = socket.send_string("Results being inquired...") result_json = socket.recv_string() if 'In processing...' in result_json: - time.sleep(update_interval_ms/1000) + time.sleep(update_interval_ms/globals.S_TO_MS) self.trial -= 1 elif 'Feedback is not activated.' in result_json: logging.info("Server does not run in the feedback mode. Inquiry cloded.") @@ -75,7 +75,7 @@ def run(self, timeout_ms = 5000, update_interval_ms=2000, n_retry=10, verbose = break except zmq.ZMQError as e: logging.error(f"Failed to receive processed data request: {e}") - time.sleep(update_interval_ms/1000) + time.sleep(update_interval_ms/globals.S_TO_MS) self.trial -= 1 # finally: # # ensure the socket is closed no matter what diff --git a/jungfrau_gui/ui_components/tem_controls/connectivity_inspector.py b/jungfrau_gui/ui_components/tem_controls/connectivity_inspector.py index 29ca6ea..748cc20 100644 --- a/jungfrau_gui/ui_components/tem_controls/connectivity_inspector.py +++ b/jungfrau_gui/ui_components/tem_controls/connectivity_inspector.py @@ -3,7 +3,7 @@ from simple_tem import TEMClient -from ... import globals +from jungfrau_gui import globals class TEM_Connector(QObject): finished = Signal(bool) @@ -11,7 +11,7 @@ class TEM_Connector(QObject): def __init__(self): super(TEM_Connector, self).__init__() self.task_name = "TEM Connector" - self.client = TEMClient(globals.tem_host, 3535, verbose=False) + self.client = TEMClient(globals.tem_host, globals.tem_port, verbose=False) @Slot() def run(self): diff --git a/jungfrau_gui/ui_components/tem_controls/gaussian_fitter.py b/jungfrau_gui/ui_components/tem_controls/gaussian_fitter.py index 90af195..d68d0ed 100644 --- a/jungfrau_gui/ui_components/tem_controls/gaussian_fitter.py +++ b/jungfrau_gui/ui_components/tem_controls/gaussian_fitter.py @@ -3,7 +3,7 @@ from PySide6.QtCore import QObject, Signal, Slot # from line_profiler import LineProfiler -from .toolbox.fit_beam_intensity import gaussian2d_rotated, super_gaussian2d_rotated, fit_2d_gaussian_roi_NaN +from jungfrau_gui.ui_components.tem_controls.toolbox.fit_beam_intensity import gaussian2d_rotated, super_gaussian2d_rotated, fit_2d_gaussian_roi_NaN class GaussianFitter(QObject): finished = Signal(object) diff --git a/jungfrau_gui/ui_components/tem_controls/gaussian_fitter_mp.py b/jungfrau_gui/ui_components/tem_controls/gaussian_fitter_mp.py index 9197c25..131eada 100644 --- a/jungfrau_gui/ui_components/tem_controls/gaussian_fitter_mp.py +++ b/jungfrau_gui/ui_components/tem_controls/gaussian_fitter_mp.py @@ -5,10 +5,10 @@ from line_profiler import LineProfiler import zmq import cbor2 -from ...decoder import tag_hook -from ... import globals +from jungfrau_gui.decoder import tag_hook +from jungfrau_gui import globals -from .toolbox.fit_beam_intensity import gaussian2d_rotated, super_gaussian2d_rotated, fit_2d_gaussian_roi_NaN_fast +from jungfrau_gui.ui_components.tem_controls.toolbox.fit_beam_intensity import gaussian2d_rotated, super_gaussian2d_rotated, fit_2d_gaussian_roi_NaN_fast from datetime import datetime # import globals diff --git a/jungfrau_gui/ui_components/tem_controls/task/beam_focus_task.py b/jungfrau_gui/ui_components/tem_controls/task/beam_focus_task.py index c181182..d9170d0 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/beam_focus_task.py +++ b/jungfrau_gui/ui_components/tem_controls/task/beam_focus_task.py @@ -2,18 +2,14 @@ import time import logging import numpy as np -from .task import Task +from jungfrau_gui.ui_components.tem_controls.task.task import Task -from .... import globals +from jungfrau_gui import globals from PySide6.QtCore import Qt, QMetaObject, Signal from datetime import datetime from simple_tem import TEMClient -IL1_0 = 21780 # 21819 -ILS_0 = [32920, 32776] # [32820, 32976] -WAIT_TIME_S = 0.25 # TODO: optimize value - class AutoFocusTask(Task): # Signal to notify the main thread that a new best result arrived newBestResult = Signal(dict) @@ -24,14 +20,14 @@ def __init__(self, control_worker): self.estimateds_duration = self.duration_s + 0.1 self.control = control_worker self.tem_action = self.control.tem_action - self.client = TEMClient(globals.tem_host, 3535) + self.client = TEMClient(globals.tem_host, globals.tem_port) # Start from a known set of lens values (but creates a freeze of ~0.1s) - self.client.SetILFocus(IL1_0) - self.client.SetILs(*ILS_0) - time.sleep(WAIT_TIME_S) + self.client.SetILFocus(globals.IL1_0) + self.client.SetILs(*globals.ILS_0) + time.sleep(globals.WAIT_TIME_S) self.lens_parameters = { - "il1": IL1_0, # an integer - "ils": ILS_0, # two integers for stigmation + "il1": globals.IL1_0, # an integer + "ils": globals.ILS_0, # two integers for stigmation } self.beam_fitter = self.control.beam_fitter self.results = [] @@ -48,7 +44,7 @@ def __init__(self, control_worker): self._best_stigmation = None self._best_combined = None - def run(self, init_IL1=IL1_0, init_stigm=ILS_0, time_budget=15): + def run(self, init_IL1=globals.IL1_0, init_stigm=globals.ILS_0, time_budget=15): try: # ---------------------- # Start parallel process @@ -84,7 +80,7 @@ def run(self, init_IL1=IL1_0, init_stigm=ILS_0, time_budget=15): ########## # METHOD A ########## - def rapid_parabolic_focus(self, init_IL1, range_width=100, num_points=5, wait_time_s=WAIT_TIME_S): + def rapid_parabolic_focus(self, init_IL1, range_width=100, num_points=5, wait_time_s=globals.WAIT_TIME_S): """ Rapidly optimize beam focus by sampling points and fitting a parabola. @@ -248,7 +244,7 @@ def rapid_parabolic_focus(self, init_IL1, range_width=100, num_points=5, wait_ti else: return None - def rapid_stigmation_optimization(self, init_stigm, deviation=100, num_points=10, wait_time_s=WAIT_TIME_S): + def rapid_stigmation_optimization(self, init_stigm, deviation=100, num_points=10, wait_time_s=globals.WAIT_TIME_S): """ Rapidly optimize beam stigmation using a simplified grid search. @@ -343,10 +339,10 @@ def rapid_autofocus(self, init_IL1=None, init_stigm=None, time_budget_seconds=15 try: # Get current lens values if not provided if init_IL1 is None: - init_IL1 = self.lens_parameters.get("il1", IL1_0) + init_IL1 = self.lens_parameters.get("il1", globals.IL1_0) if init_stigm is None: - init_stigm = self.lens_parameters.get("ils", ILS_0) + init_stigm = self.lens_parameters.get("ils", globals.ILS_0) # Reset results and best tracking self.results = [] @@ -359,7 +355,7 @@ def rapid_autofocus(self, init_IL1=None, init_stigm=None, time_budget_seconds=15 init_IL1=init_IL1, range_width=100, # Adjust based on expected focus range num_points=7, # 7 points is a good balance between speed and accuracy ? - wait_time_s=WAIT_TIME_S # Reduced wait time for speed + wait_time_s=globals.WAIT_TIME_S # Reduced wait time for speed ) if focus_result: @@ -388,7 +384,7 @@ def rapid_autofocus(self, init_IL1=None, init_stigm=None, time_budget_seconds=15 init_stigm=init_stigm, deviation=100, # Adjust based on expected stigmation range num_points=5, # number of points in each dimension - wait_time_s=WAIT_TIME_S # Reduced wait time for speed + wait_time_s=globals.WAIT_TIME_S # Reduced wait time for speed ) if stigmation_result: @@ -409,7 +405,7 @@ def rapid_autofocus(self, init_IL1=None, init_stigm=None, time_budget_seconds=15 init_IL1=optimal_il1, range_width=60, # Narrower range for refinement num_points=4, # Fewer points needed for refinement - wait_time_s=WAIT_TIME_S + wait_time_s=globals.WAIT_TIME_S ) if final_focus_result: @@ -437,7 +433,7 @@ def rapid_autofocus(self, init_IL1=None, init_stigm=None, time_budget_seconds=15 ########## # METHOD B ########## - def standard_focus(self, init_IL1=IL1_0, init_stigm=ILS_0): + def standard_focus(self, init_IL1=globals.IL1_0, init_stigm=globals.ILS_0): # Start counter autofocus_start = time.perf_counter() @@ -552,7 +548,7 @@ def standard_focus(self, init_IL1=IL1_0, init_stigm=ILS_0): # Emit final result signal (for UI update) self.newBestResult.emit(final_results) - def sweep_il1_linear(self, lower, upper, step, wait_time_s=WAIT_TIME_S): + def sweep_il1_linear(self, lower, upper, step, wait_time_s=globals.WAIT_TIME_S): """ Perform a linear sweep of IL1 TEM lens positions with Gaussian fitting at each step. @@ -595,7 +591,7 @@ def sweep_il1_linear(self, lower, upper, step, wait_time_s=WAIT_TIME_S): return True - def sweep_stig_linear(self, init_stigm, deviation, step, wait_time_s=WAIT_TIME_S): + def sweep_stig_linear(self, init_stigm, deviation, step, wait_time_s=globals.WAIT_TIME_S): """ Perform a linear sweep of stigmation parameters in X and Y directions. @@ -702,15 +698,15 @@ def goto_il1_with_hysteresis_compensation(self, target_il1, margin=20): # Go to a value well below the target self.client.SetILFocus(target_il1 - 50) - time.sleep(WAIT_TIME_S) + time.sleep(globals.WAIT_TIME_S) # Overshoot by a fixed amount self.client.SetILFocus(target_il1 + margin) - time.sleep(WAIT_TIME_S) + time.sleep(globals.WAIT_TIME_S) # Approach the final value self.client.SetILFocus(target_il1) - time.sleep(WAIT_TIME_S) + time.sleep(globals.WAIT_TIME_S) # Update stored value self.lens_parameters["il1"] = target_il1 @@ -729,19 +725,19 @@ def goto_ils_with_hysteresis_compensation(self, target_ils, margin=50): # X axis approach current_ils_y = self.lens_parameters.get("ils", [0, 0])[1] self.client.SetILs(ils_x - 100, current_ils_y) - time.sleep(WAIT_TIME_S) + time.sleep(globals.WAIT_TIME_S) self.client.SetILs(ils_x + margin, current_ils_y) - time.sleep(WAIT_TIME_S) + time.sleep(globals.WAIT_TIME_S) self.client.SetILs(ils_x, current_ils_y) - time.sleep(WAIT_TIME_S) + time.sleep(globals.WAIT_TIME_S) # Y axis approach self.client.SetILs(ils_x, ils_y - 100) - time.sleep(WAIT_TIME_S) + time.sleep(globals.WAIT_TIME_S) self.client.SetILs(ils_x, ils_y + margin) - time.sleep(WAIT_TIME_S) + time.sleep(globals.WAIT_TIME_S) self.client.SetILs(ils_x, ils_y) - time.sleep(WAIT_TIME_S) + time.sleep(globals.WAIT_TIME_S) # Update stored value self.lens_parameters["ils"] = [ils_x, ils_y] diff --git a/jungfrau_gui/ui_components/tem_controls/task/get_teminfo_task.py b/jungfrau_gui/ui_components/tem_controls/task/get_teminfo_task.py index a7ad573..ec9e8bf 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/get_teminfo_task.py +++ b/jungfrau_gui/ui_components/tem_controls/task/get_teminfo_task.py @@ -2,7 +2,7 @@ import logging import numpy as np -from .task import Task +from jungfrau_gui.ui_components.tem_controls.task.task import Task from epoc import ConfigurationClient, auth_token, redis_host class GetInfoTask(Task): diff --git a/jungfrau_gui/ui_components/tem_controls/task/record_task.py b/jungfrau_gui/ui_components/tem_controls/task/record_task.py index 27b8e7f..64fe765 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/record_task.py +++ b/jungfrau_gui/ui_components/tem_controls/task/record_task.py @@ -3,16 +3,15 @@ import h5py import logging import numpy as np -from .task import Task from PySide6.QtWidgets import QMessageBox from PySide6.QtCore import Signal, Qt, QMetaObject from simple_tem import TEMClient from epoc import ConfigurationClient, auth_token, redis_host -from ..toolbox.tool import send_with_retries -from ....metadata_uploader.metadata_update_client import MetadataNotifier - -from .... import globals +from jungfrau_gui import globals +from jungfrau_gui.ui_components.tem_controls.toolbox.tool import send_with_retries +from jungfrau_gui.ui_components.tem_controls.task.task import Task +from jungfrau_gui.metadata_uploader.metadata_update_client import MetadataNotifier class RecordTask(Task): reset_rotation_signal = Signal() @@ -28,9 +27,9 @@ def __init__(self, control_worker, end_angle = 60, log_suffix = 'RotEDlog_test', self.rotations_angles = [] self.log_suffix = log_suffix logging.info("RecordTask initialized") - self.client = TEMClient(globals.tem_host, 3535, verbose=True) + self.client = TEMClient(globals.tem_host, globals.tem_port, verbose=True) self.cfg = ConfigurationClient(redis_host(), token=auth_token()) - self.metadata_notifier = MetadataNotifier(host = "noether", port = 3463, verbose = False) + self.metadata_notifier = MetadataNotifier(host = globals.dataserver_host, port = globals.dataserver_port, verbose = False) self.reset_rotation_signal.connect(self.tem_action.reset_rotation_button) @@ -206,8 +205,8 @@ def run(self): beam_property, self.rotations_angles, self.cfg.threshold, - retries=3, - delay=0.1) + retries=globals.max_retries_tagging, + delay=globals.inquiry_delay) self.file_operations.update_xtalinfo_signal.emit('Processing', 'XDS') # self.file_operations.update_xtalinfo_signal.emit('Processing', 'DIALS') diff --git a/jungfrau_gui/ui_components/tem_controls/task/stage_centering_task.py b/jungfrau_gui/ui_components/tem_controls/task/stage_centering_task.py index 20a6917..c2b286d 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/stage_centering_task.py +++ b/jungfrau_gui/ui_components/tem_controls/task/stage_centering_task.py @@ -1,10 +1,11 @@ import time import logging import numpy as np -from .task import Task + +from jungfrau_gui import globals +from jungfrau_gui.ui_components.tem_controls.task.task import Task from simple_tem import TEMClient -from .... import globals from epoc import ConfigurationClient, auth_token, redis_host from jungfrau_gui.ui_components.tem_controls.toolbox import config as cfg_jf @@ -19,16 +20,13 @@ def __init__(self, control_worker, pixels=[10, 1]): self.control = control_worker self.pixels = pixels logging.info("CenteringTask initialized") - self.client = TEMClient(globals.tem_host, 3535, verbose=True) + self.client = TEMClient(globals.tem_host, globals.tem_port, verbose=True) self.cfg = ConfigurationClient(redis_host(), token=auth_token()) for shape in self.cfg.overlays: if shape['type'] == 'rectangle': self.lowmag_jump = shape['xy'][0]+shape['width']//2, shape['xy'][1]+shape['height']//2 break - self.thresholds = { - 'dxy_min': 0.3, 'dxy_max': 100, - 'dz_min_mag': 1, 'dz_max_mag': 10, 'dz_min_lmag': 3, 'absz_min': -70, 'absz_max': 20, - } + self.thresholds = globals.click_on_move_thresholds def rot2d(self, vector, theta):# anti-clockwise theta_r = np.radians(theta) @@ -39,7 +37,7 @@ def rot2d(self, vector, theta):# anti-clockwise def translationvector(self, pixels, magnification): calibrated_mag = cfg_jf.lut().calibrated_magnification(magnification[2]) rotation_axis = cfg_jf.lut().rotaxis_for_ht_degree(self.control.tem_status["ht.GetHtValue"], magnification=magnification[0]) - if int(magnification[0]) >= 1500 : # Mag + if int(magnification[0]) >= globals.min_mag_for_mag: # Mag logging.debug(f'Estimate with rotation') tr_vector = (pixels - [self.cfg.ncols/2, self.cfg.nrows/2]) * globals.PIXEL * globals.MM_TO_UM / calibrated_mag # in um else: # Lowmag, targeting to the rectangular overlay @@ -71,7 +69,7 @@ def run(self): if tilt_X_abs < 5: if np.abs(movexy[0]) < self.thresholds['dxy_min'] and np.abs(movexy[1]) < self.thresholds['dxy_min']: - logging.info(f'Vector already small enough (< {self.thresholds['dxy_min']} um): {movexy[0]}, {movexy[1]}') + logging.info(f"Vector already small enough (< {self.thresholds['dxy_min']} um): {movexy[0]}, {movexy[1]}") return logging.info(f'Move X: {movexy[0]} um, Y: {movexy[1]} um with MAG: {magnification[2]}') self.control.trigger_movewithbacklash.emit(np.sign(movexy[0]) > 0, -movexy[0]*globals.UM_TO_NM, globals.backlash[0], False) diff --git a/jungfrau_gui/ui_components/tem_controls/task/task_manager.py b/jungfrau_gui/ui_components/tem_controls/task/task_manager.py index 1a9b16e..0c7d178 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/task_manager.py +++ b/jungfrau_gui/ui_components/tem_controls/task/task_manager.py @@ -7,25 +7,20 @@ from PySide6.QtCore import Signal, Slot, QObject, QThread, QMetaObject, Qt, QTimer -from .task import Task -from .record_task import RecordTask - -from .beam_focus_task import AutoFocusTask - -from .get_teminfo_task import GetInfoTask -from .stage_centering_task import CenteringTask +from jungfrau_gui import globals +import jungfrau_gui.ui_threading_helpers as thread_manager +from jungfrau_gui.ui_components.tem_controls.task.task import Task +from jungfrau_gui.ui_components.tem_controls.task.record_task import RecordTask +from jungfrau_gui.ui_components.tem_controls.task.beam_focus_task import AutoFocusTask +from jungfrau_gui.ui_components.tem_controls.task.get_teminfo_task import GetInfoTask +from jungfrau_gui.ui_components.tem_controls.task.stage_centering_task import CenteringTask +from jungfrau_gui.ui_components.tem_controls.toolbox import tool as tools +from jungfrau_gui.ui_components.tem_controls.gaussian_fitter_mp import GaussianFitterMP +from jungfrau_gui.ui_components.tem_controls.task.tem_dispatcher import TEMDispatcher from simple_tem import TEMClient -from ..toolbox import tool as tools - from epoc import ConfigurationClient, auth_token, redis_host -import jungfrau_gui.ui_threading_helpers as thread_manager - -from .... import globals - -from ..gaussian_fitter_mp import GaussianFitterMP - def on_new_best_result_in_main_thread(result_dict): # This runs in the main thread. We can safely update GUI elements, logs, etc. print("New best result =>", result_dict) @@ -56,6 +51,7 @@ class ControlWorker(QObject): trigger_getteminfo = Signal(str) trigger_centering = Signal(bool, str) trigger_movewithbacklash = Signal(int, float, float, bool) + trigger_move_parking = Signal(int, float, float, bool) # always preload actionFit_Beam = Signal() # originally defined with QuGui # actionAdjustZ = Signal() @@ -63,7 +59,8 @@ class ControlWorker(QObject): def __init__(self, tem_action): #, timeout:int=10, buffer=1024): super().__init__() self.cfg = ConfigurationClient(redis_host(), token=auth_token()) - self.client = TEMClient(globals.tem_host, 3535, verbose=False) + self.client = TEMClient(globals.tem_host, globals.tem_port, verbose=False) + self.tem = TEMDispatcher(self.client) self.task = Task(self, "Dummy") self.task_thread = QThread() @@ -85,6 +82,10 @@ def __init__(self, tem_action): #, timeout:int=10, buffer=1024): self.trigger_getteminfo.connect(self.getteminfo) self.trigger_centering.connect(self.centering) self.trigger_movewithbacklash.connect(self.move_with_backlash) + self.trigger_move_parking.connect(self.move_parking_with_preload) + + self._net_away = {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0} # X,Y,Z,TX signed net move + # self.actionAdjustZ.connect(self.start_adjustZ) self.beam_fitter = None @@ -421,7 +422,7 @@ def update_tem_status(self, response): position = np.array(pos_list) position_prev = np.array(pos_prev_list) diff_pos = position - position_prev - threshold = np.array([30, 30, 30, 0.2, 100]) # nm, nm, nm, deg., deg. + threshold = np.array(globals.stage_relaxation_thresholds) update_mask = np.abs(diff_pos) > threshold # Update diff using vectorized operations @@ -749,6 +750,10 @@ def stop_task(self): def shutdown(self): logging.info("Shutting down control") try: + try: + self.tem.shutdown() + except Exception: + pass # self.client.exit_server() # logging.warning("TEM server is OFF") # time.sleep(0.12) @@ -758,6 +763,8 @@ def shutdown(self): logging.error(f'Shutdown of Task Manager triggered error: {e}') pass + # KT's implementation of backlash corrected fast movements + # The routine moves the stage in one direction, but with a reduced/corrected value @Slot(int, float, float, bool) def move_with_backlash(self, moverid=0, value=10, backlash=0, button=False, scale=1): """ @@ -786,7 +793,7 @@ def move_with_backlash(self, moverid=0, value=10, backlash=0, button=False, scal elif direction == 1 and np.sign(self.tem_status["stage.GetPos_diff"][axis]) < 0: backlash = 0 - logging.debug(f"xyz0, dxyz0 : {list(map(lambda x, y: f'{x/globals.UM_TO_NM:8.3f}{y/globals.UM_TO_NM:8.3f}', self.tem_status['stage.GetPos'][:3], self.tem_status['stage.GetPos_diff'][:3]))}, " + logging.debug(f"xyz0, dxyz0 : {list(map(lambda x, y: f'{x/1e3:8.3f}{y/1e3:8.3f}', self.tem_status['stage.GetPos'][:3], self.tem_status['stage.GetPos_diff'][:3]))}, " f"{self.tem_status['stage.GetPos'][3]:6.2f} {self.tem_status['stage.GetPos_diff'][3]:6.2f}, {backlash}" ) @@ -819,10 +826,173 @@ def move_with_backlash(self, moverid=0, value=10, backlash=0, button=False, scal return if moverid < 2 and button: # display the previous move to user + # logging.info(f"Moved stage {value*scale/1e3:.1f} um in X-direction") if moverid == 0: self.tem_action.tem_stagectrl.movex10ump.setStyleSheet('background-color: rgb(53, 53, 53); color: rgb(128, 128, 255);') self.tem_action.tem_stagectrl.movex10umn.setStyleSheet('background-color: rgb(53, 53, 53); color: white;') else: self.tem_action.tem_stagectrl.movex10ump.setStyleSheet('background-color: rgb(53, 53, 53); color: white;') self.tem_action.tem_stagectrl.movex10umn.setStyleSheet('background-color: rgb(53, 53, 53); color: rgb(128, 128, 255);') - logging.debug(f"xyz1, dxyz1 : {list(map(lambda x, y: f'{x/globals.UM_TO_NM:8.3f}{y/globals.UM_TO_NM:8.3f}', self.tem_status['stage.GetPos'][:3], self.tem_status['stage.GetPos_diff'][:3]))}, {self.tem_status['stage.GetPos'][3]:6.2f} {self.tem_status['stage.GetPos_diff'][3]:6.2f}, {backlash}") + logging.debug(f"xyz1, dxyz1 : {list(map(lambda x, y: f'{x/1e3:8.3f}{y/1e3:8.3f}', self.tem_status['stage.GetPos'][:3], self.tem_status['stage.GetPos_diff'][:3]))}, {self.tem_status['stage.GetPos'][3]:6.2f} {self.tem_status['stage.GetPos_diff'][3]:6.2f}, {backlash}") + + + # ************************************************ + # Routines for lag-corrected fast movement actions + # ************************************************ + + def _SetTXRel_timeout(self, val: float, timeout_ms: int): + # Calls underlying TEMClient with custom timeout, without modifying TEMClient. + return self.client._send_message("SetTXRel", val, timeout_ms=timeout_ms) + + def _SetXRel_timeout(self, val: float, timeout_ms: int): + return self.client._send_message("SetXRel", val, timeout_ms=timeout_ms) + + def _SetYRel_timeout(self, val: float, timeout_ms: int): + return self.client._send_message("SetYRel", val, timeout_ms=timeout_ms) + + def _SetZRel_timeout(self, val: float, timeout_ms: int): + return self.client._send_message("SetZRel", val, timeout_ms=timeout_ms) + + + def _two_step_sequence(self, axis_fn, value, preload, settle_s=0.1): + """ + Build an atomic sequence for preload-compensated moves: + value > 0: (value+preload), wait, (-preload) + value < 0: (value-preload), wait, (+preload) + """ + if value == 0: + return [] + if preload == 0: + return [(axis_fn, (value,), {})] + + if value > 0: + return [ + (axis_fn, (value + preload,), {}), + (time.sleep, (settle_s,), {}), + (axis_fn, (-preload,), {}), + ] + else: + return [ + (axis_fn, (value - preload,), {}), + (time.sleep, (settle_s,), {}), + (axis_fn, (preload,), {}), + ] + + + @Slot(int, float, float, bool) + def move_parking_with_preload(self, moverid=0, value=10.0, preload=0.0, button=False, scale=1.0): + """ + Relative stage jog with optional backlash (preload) compensation. + + If a direction change is detected, applies a two-step move to take up slack: + +dir: (value + preload) then (-preload) + -dir: (value - preload) then (+preload) + Otherwise, sends a single relative move. + + Args: + moverid (int): 0/1:+X/-X, 2/3:+Y/-Y, 4/5:+Z/-Z, 6/7:+TX/-TX. + value (float): Relative move (nm for X/Y/Z, deg for TX); sign matches moverid. + preload (float): Overshoot amount (same units as value); 0 disables. + button (bool): If True, updates X jog button highlight. + scale (float): Multiplier applied to `value` before sending. + """ + # Ask for fresh status (coalesced, won't spam) + QTimer.singleShot(0, lambda: self.send_to_tem("#info", asynchronous=True)) + + axis = moverid // 2 + movement_value = value * scale + effective_preload = float(preload) #if (apply_preload and preload != 0) else 0.0 + + # set longer timeout for slow rotation speeds + MOVE_TIMEOUT_MS = 30000 if axis == 3 else 5000 + + # pick axis function + axis_fn = self._axis_fn_for_axis(axis, MOVE_TIMEOUT_MS) + + # Build atomic job sequence and enqueue on single TEM lane + jobs = self._two_step_sequence(axis_fn, movement_value, effective_preload, settle_s=0.05) + if jobs: + self.tem.post_sequence(jobs) + + self._record_away_and_enable_back(axis, movement_value) + + # UI styling for X buttons + if moverid < 2 and button: + if moverid == 0: + self.tem_action.tem_stagectrl.movex10ump.setStyleSheet( + "background-color: rgb(53, 53, 53); color: rgb(128, 128, 255);" + ) + self.tem_action.tem_stagectrl.movex10umn.setStyleSheet( + "background-color: rgb(53, 53, 53); color: white;" + ) + else: + self.tem_action.tem_stagectrl.movex10ump.setStyleSheet( + "background-color: rgb(53, 53, 53); color: white;" + ) + self.tem_action.tem_stagectrl.movex10umn.setStyleSheet( + "background-color: rgb(53, 53, 53); color: rgb(128, 128, 255);" + ) + + + @Slot(int) + def move_back_no_preload(self, axis=0): + """ + Move back by the last recorded away move on this axis. + axis: 0=X,1=Y,2=Z,3=TX + """ + away_accumulated = float(self._net_away.get(axis, 0.0)) + if away_accumulated == 0.0: + logging.warning(f"No recorded away move for axis {axis}") + self._clear_away_and_disable(axis) + return + + back_value = -away_accumulated + MOVE_TIMEOUT_MS = 30000 if axis == 3 else 5000 + axis_fn = self._axis_fn_for_axis(axis, MOVE_TIMEOUT_MS) + + self.tem.post_sequence([(axis_fn, (back_value,), {})]) + + self._clear_away_and_disable(axis) + + def _axis_fn_for_axis(self, axis: int, timeout_ms: int): + # Helper to choose corresponding TEMClient routine as fct of the set axis + if axis == 0: + return lambda v: self._SetXRel_timeout(v, timeout_ms) + if axis == 1: + return lambda v: self._SetYRel_timeout(v, timeout_ms) + if axis == 2: + return lambda v: self._SetZRel_timeout(v, timeout_ms) + if axis == 3: + return lambda v: self._SetTXRel_timeout(v, timeout_ms) + raise ValueError(f"Invalid axis {axis}") + + + def _record_away_and_enable_back(self, axis, movement_value): + # record the net “away” move so 'Back' buttons know what to do + self._net_away[axis] += movement_value + + # enable back button corresponding to the latest preloaded fast movement + if axis == 0: # translation (X) + QTimer.singleShot(0, lambda: self.tem_action.tem_stagectrl.back_x.setEnabled(True)) + if axis == 3: # rotation (TX) + QTimer.singleShot(0, lambda: self.tem_action.tem_stagectrl.back_tx.setEnabled(True)) + + + def _clear_away_and_disable(self, axis): + # Clear 'away' as compensation has been completed + self._net_away[axis] = 0.0 + + # Disable back buttons + if axis == 0: # translation (X) + QTimer.singleShot(0, lambda: self.tem_action.tem_stagectrl.back_x.setEnabled(False)) + if axis == 3: # rotation (TX) + QTimer.singleShot(0, lambda: self.tem_action.tem_stagectrl.back_tx.setEnabled(False)) + + # Uncolor translation buttons + if axis == 0: + self.tem_action.tem_stagectrl.movex10ump.setStyleSheet( + "background-color: rgb(53, 53, 53); color: white;" + ) + self.tem_action.tem_stagectrl.movex10umn.setStyleSheet( + "background-color: rgb(53, 53, 53); color: white;" + ) diff --git a/jungfrau_gui/ui_components/tem_controls/task/tem_dispatcher.py b/jungfrau_gui/ui_components/tem_controls/task/tem_dispatcher.py new file mode 100644 index 0000000..b0ae5a7 --- /dev/null +++ b/jungfrau_gui/ui_components/tem_controls/task/tem_dispatcher.py @@ -0,0 +1,77 @@ +import queue +import threading +import time +import logging + +class TEMDispatcher: + """ + Single-threaded command lane for TEM calls. + - post(fn, ...): fire-and-forget + - call(fn, ...): wait for result + - post_sequence([...]): run multiple commands atomically (no interleave) + - post_latest(key, fn, ...): keep only the newest request for a key (ideal for polling) + """ + def __init__(self, client): + self.client = client + self._q = queue.Queue() + self._stop = threading.Event() + self._latest_token = {} # key -> token + self._thread = threading.Thread(target=self._loop, name="TEM-IO", daemon=True) + self._thread.start() + + def shutdown(self): + self._stop.set() + self._q.put(None) + + def _loop(self): + while not self._stop.is_set(): + item = self._q.get() + if item is None: + break + + kind = item[0] + try: + if kind == "call": + _, fn, args, kwargs, done, out = item + out["result"] = fn(*args, **kwargs) + done.set() + + elif kind == "post": + _, fn, args, kwargs = item + fn(*args, **kwargs) + + elif kind == "sequence": + _, fns = item + for fn, args, kwargs in fns: + fn(*args, **kwargs) + + elif kind == "latest": + _, key, token, fn, args, kwargs = item + # skip stale polls + if self._latest_token.get(key) == token: + fn(*args, **kwargs) + + except Exception as e: + logging.warning(f"TEMDispatcher error in {kind}: {type(e).__name__}: {e}") + + def post(self, fn, *args, **kwargs): + self._q.put(("post", fn, args, kwargs)) + + def call(self, fn, *args, timeout=None, **kwargs): + done = threading.Event() + out = {} + self._q.put(("call", fn, args, kwargs, done, out)) + ok = done.wait(timeout) + return out.get("result") if ok else None + + def post_sequence(self, fns): + """ + fns = [(fn, args_tuple, kwargs_dict), ...] + executed back-to-back in the TEM thread. + """ + self._q.put(("sequence", fns)) + + def post_latest(self, key, fn, *args, **kwargs): + token = object() + self._latest_token[key] = token + self._q.put(("latest", key, token, fn, args, kwargs)) diff --git a/jungfrau_gui/ui_components/tem_controls/tem_action.py b/jungfrau_gui/ui_components/tem_controls/tem_action.py index 9ee91cd..d4685a7 100644 --- a/jungfrau_gui/ui_components/tem_controls/tem_action.py +++ b/jungfrau_gui/ui_components/tem_controls/tem_action.py @@ -5,18 +5,15 @@ from PySide6.QtCore import QRectF, QObject, QTimer, Qt, QMetaObject, Signal, Slot from PySide6.QtGui import QFont, QTransform -from .toolbox.tool import * -from .toolbox import config as cfg_jf - -from .task.task_manager import * +import jungfrau_gui.ui_threading_helpers as thread_manager +from jungfrau_gui.ui_components.tem_controls.toolbox.tool import * +from jungfrau_gui.ui_components.tem_controls.toolbox import config as cfg_jf +from jungfrau_gui.ui_components.tem_controls.task.task_manager import * +from jungfrau_gui.ui_components.tem_controls.connectivity_inspector import TEM_Connector +from jungfrau_gui.ui_components.file_operations.processresult_updater import ProcessedDataReceiver +from jungfrau_gui.ui_components.tem_controls.tem_status_updater import TemUpdateWorker from epoc import ConfigurationClient, auth_token, redis_host - -from .connectivity_inspector import TEM_Connector -from ..file_operations.processresult_updater import ProcessedDataReceiver -from .tem_status_updater import TemUpdateWorker - -import jungfrau_gui.ui_threading_helpers as thread_manager import time from jungfrau_gui import globals @@ -88,17 +85,36 @@ def __init__(self, parent, grandparent): self.control.updated.connect(self.on_tem_update) - # Move X positive 10 micrometers - self.tem_stagectrl.movex10ump.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(0, 10000, globals.backlash[0], True)) - # Move X negative 10 micrometers - self.tem_stagectrl.movex10umn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(1, -10000, globals.backlash[0], True)) - # Move TX positive 10 degrees - self.tem_stagectrl.move10degp.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(6, 10, globals.backlash[3], False)) - # Move TX negative 10 degrees - self.tem_stagectrl.move10degn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(7, -10, globals.backlash[3], False)) + # Away X: +10 um, always preload (e.g. +12 then -2) + self.tem_stagectrl.movex10ump.clicked.connect( + lambda: self.control.trigger_move_parking.emit(0, 10000, globals.preload[0], True) + ) + self.tem_stagectrl.movex10umn.clicked.connect( + lambda: self.control.trigger_move_parking.emit(1, -10000, globals.preload[0], True) + ) + + # Away TX: +10 deg, preload (e.g. +11 then -1) + self.tem_stagectrl.move10degp.clicked.connect( + lambda: self.control.trigger_move_parking.emit(6, 10, globals.preload[3], False) + ) + self.tem_stagectrl.move10degn.clicked.connect( + lambda: self.control.trigger_move_parking.emit(7, -10, globals.preload[3], False) + ) + + # Stage translation move back (X=0) with no preload + self.tem_stagectrl.back_x.clicked.connect( + lambda: self.control.move_back_no_preload(0) + ) + + # Stage rotation move back (TX=3) with no preload + self.tem_stagectrl.back_tx.clicked.connect( + lambda: self.control.move_back_no_preload(3) + ) + # Set Tilt X Angle to 0 degrees self.tem_stagectrl.move0deg.clicked.connect( lambda: threading.Thread(target=self.control.client.SetTiltXAngle, args=(0,)).start()) + self.tem_stagectrl.go_button.clicked.connect(self.go_listedposition) self.tem_stagectrl.addpos_button.clicked.connect(lambda: self.add_listedposition()) self.trigger_additem.connect(self.add_listedposition) @@ -948,7 +964,7 @@ def plot_currentposition(self, color='yellow'): @Slot() def inquire_processed_data(self): if self.dataReceiverReady: - self.process_receiver = ProcessedDataReceiver(self, host = "noether") + self.process_receiver = ProcessedDataReceiver(self, host = globals.dataserver_host) self.datareceiver_thread = QThread() self.datareceiver_thread.setObjectName("Data_Receiver Thread") self.parent.threadWorkerPairs.append((self.datareceiver_thread, self.process_receiver)) @@ -974,7 +990,7 @@ def update_ecount(self, cutoff=400, bins_set=20): if Mag_idx == 4: logging.warning("Brightness should be calculated in imaging mode") return - frame = self.visualization_panel.jfjoch_client._lots_of_images / 3600 # usually 20, with 100 frame-sum + frame = self.visualization_panel.jfjoch_client._lots_of_images / globals.max_duration # usually 20, with 100 frame-sum image = self.parent.imageItem.image data_flat = image.flatten() image_deloverflow = image[np.where(image < np.iinfo('int32').max-1)] @@ -1026,7 +1042,7 @@ def take_snapshot(self, max_list=50): data_sampled = image_deloverflow[np.where((image_deloverflow < high_thresh)&(image_deloverflow > low_thresh))] uniqs, counts = np.unique(data_sampled//10, return_counts=True) approximate_average_count = uniqs[np.argmax(counts)].max() * 10 - low_thresh, high_thresh = approximate_average_count*(1-margin), approximate_average_count*(1+margin) + low_thresh, high_thresh = approximate_average_count*(1-subiman), approximate_average_count*(1+margin) logging.info(f"Snapshot displayed in enhanced contrast ({low_thresh}-{high_thresh})") # downsizing snapshot_image = pg.ImageItem(np.clip((np.nan_to_num(image) - low_thresh) / (high_thresh - low_thresh) * 255, 0, 255).astype(np.uint8)) @@ -1035,7 +1051,7 @@ def take_snapshot(self, max_list=50): scale = globals.PIXEL*globals.MM_TO_UM/calibrated_mag tr.scale(scale, scale) tr.rotate(180 + self.lut.rotaxis_for_ht_degree(self.control.tem_status["ht.GetHtValue"], magnification=magnification[0])) - if int(magnification[0]) >= 1500 : # Mag + if int(magnification[0]) >= globals.min_mag_for_mag: # Mag tr.translate(-image.shape[0]/2, -image.shape[1]/2) else: tr.translate(-self.lowmag_jump[0], -self.lowmag_jump[1]) @@ -1097,12 +1113,12 @@ def synchronize_xtallist(self): return # load mode if self.tem_stagectrl.position_list.count() == self.gui_id_offset + 1: - self.process_receiver = ProcessedDataReceiver(self, host = "noether", mode=1) + self.process_receiver = ProcessedDataReceiver(self, host = globals.dataserver_host, mode=1) logging.info("Start session-metadata loading") self.control.tem_status["gui_id"] = self.tem_stagectrl.position_list.count() - self.gui_id_offset # save mode elif len(self.xtallist) != 1: - self.process_receiver = ProcessedDataReceiver(self, host = "noether", mode=2) + self.process_receiver = ProcessedDataReceiver(self, host = globals.dataserver_host, mode=2) logging.info("Start session-metadata saving") else: logging.warning("No data available") diff --git a/jungfrau_gui/ui_components/tem_controls/tem_controls.py b/jungfrau_gui/ui_components/tem_controls/tem_controls.py index e202576..9171f65 100644 --- a/jungfrau_gui/ui_components/tem_controls/tem_controls.py +++ b/jungfrau_gui/ui_components/tem_controls/tem_controls.py @@ -1,7 +1,8 @@ import math import logging +import threading import numpy as np -from ... import globals +from jungfrau_gui import globals import pyqtgraph as pg from datetime import datetime from PySide6.QtCore import QThread, Qt, QRectF, QMetaObject, Slot, Signal, QTimer @@ -9,18 +10,17 @@ from PySide6.QtWidgets import (QGroupBox, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QDoubleSpinBox, QCheckBox, QGraphicsEllipseItem, QGraphicsRectItem) -from .toolbox.plot_dialog import PlotDialog -from .gaussian_fitter import GaussianFitter +from jungfrau_gui.ui_components.tem_controls.toolbox.plot_dialog import PlotDialog +from jungfrau_gui.ui_components.tem_controls.gaussian_fitter import GaussianFitter -from ...ui_components.toggle_button import ToggleButton -from .ui_tem_specific import TEMStageCtrl, TEMTasks #, XtalInfo -from .tem_action import TEMAction +from jungfrau_gui.ui_components.toggle_button import ToggleButton +from jungfrau_gui.ui_components.tem_controls.ui_tem_specific import TEMStageCtrl, TEMTasks #, XtalInfo +from jungfrau_gui.ui_components.tem_controls.tem_action import TEMAction import jungfrau_gui.ui_threading_helpers as thread_manager from epoc import ConfigurationClient, auth_token, redis_host -from ...ui_components.palette import * -import threading +from jungfrau_gui.ui_components.palette import * from PySide6.QtWidgets import QApplication class TemControls(QGroupBox): diff --git a/jungfrau_gui/ui_components/tem_controls/toolbox/config.py b/jungfrau_gui/ui_components/tem_controls/toolbox/config.py index 67c03b5..a942855 100644 --- a/jungfrau_gui/ui_components/tem_controls/toolbox/config.py +++ b/jungfrau_gui/ui_components/tem_controls/toolbox/config.py @@ -12,11 +12,10 @@ from PySide6.QtCore import QRectF from epoc import ConfigurationClient, auth_token, redis_host -from .... import globals +from jungfrau_gui import globals f = files('jungfrau_gui').joinpath('ui_components/tem_controls/toolbox/jfgui2_config.json') parser = json.loads(f.read_text()) -cfg = ConfigurationClient(redis_host(), token=auth_token()) class lut: distance = parser['distances'] @@ -31,6 +30,7 @@ def __init__(self): self.array_data = np.array([list(d.values()) for d in self.distance]) self.raw_grid = np.delete(self.array_data, [2, 4, 5, 6], -1)[:-3,:] # remove date, unit, mag, and brightness at the moment self.data_grid = np.array([[int(nominal[:-2])*10, int(ht_value), float(calibrated)] for nominal, calibrated, ht_value in self.raw_grid]) + self.cfg = ConfigurationClient(redis_host(), token=auth_token()) def _lookup(self, dic, key, label_search, label_get, index=0): df_lut = pd.json_normalize(dic) @@ -68,13 +68,13 @@ def sa_size(self, key_search): return self._lookup(self.sa, key_search, 'ID', 'size') def shiftoverlay_for_ht(self, ht_in_V, magnification=1200): - if magnification > 1500: # mag + if magnification >= globals.min_mag_for_mag: # mag return self._lookup(self.ht_mag_specific, ht_in_V, 'ht_voltage', 'overlay_xy', index=0) else: return self._lookup(self.ht_mag_specific, ht_in_V, 'ht_voltage', 'overlay_xy', index=-1) def rotaxis_for_ht(self, ht_in_V, magnification=20000): - if magnification > 1500: # mag + if magnification >= globals.min_mag_for_mag: # mag return self._lookup(self.ht_mag_specific, ht_in_V, 'ht_voltage', 'axis_xds', index=0) else: return self._lookup(self.ht_mag_specific, ht_in_V, 'ht_voltage', 'axis_xds', index=-1) @@ -89,7 +89,7 @@ def overlays_for_ht(self, ht_in_V): item_circle = QGraphicsEllipseItem(QRectF(x-r, y-r, 2*r, 2*r)) item_circle.setPen(pg.mkPen('r', width=2)) - r = cfg.overlays[0]['radius'] + r = self.cfg.overlays[0]['radius'] item_common = QGraphicsEllipseItem(QRectF(x-r, y-r, 2*r, 2*r)) item_common.setPen(pg.mkPen('r', width=2)) diff --git a/jungfrau_gui/ui_components/tem_controls/toolbox/fit_beam_intensity.py b/jungfrau_gui/ui_components/tem_controls/toolbox/fit_beam_intensity.py index c5e5b03..17f38a1 100644 --- a/jungfrau_gui/ui_components/tem_controls/toolbox/fit_beam_intensity.py +++ b/jungfrau_gui/ui_components/tem_controls/toolbox/fit_beam_intensity.py @@ -7,7 +7,7 @@ from scipy.interpolate import griddata from line_profiler import LineProfiler -from .... import globals +from jungfrau_gui import globals def filter_outliers(im_roi, lower_percentile=1, upper_percentile=99.99): """ diff --git a/jungfrau_gui/ui_components/tem_controls/toolbox/plot_dialog.py b/jungfrau_gui/ui_components/tem_controls/toolbox/plot_dialog.py index 1cde965..724fc5e 100644 --- a/jungfrau_gui/ui_components/tem_controls/toolbox/plot_dialog.py +++ b/jungfrau_gui/ui_components/tem_controls/toolbox/plot_dialog.py @@ -4,7 +4,7 @@ from PySide6.QtWidgets import QPushButton, QVBoxLayout, QDialog from PySide6.QtCore import QTime -from ... import palette +from jungfrau_gui.ui_components import palette class PlotDialog(QDialog): diff --git a/jungfrau_gui/ui_components/tem_controls/toolbox/tool.py b/jungfrau_gui/ui_components/tem_controls/toolbox/tool.py index 0f41f05..088dec8 100644 --- a/jungfrau_gui/ui_components/tem_controls/toolbox/tool.py +++ b/jungfrau_gui/ui_components/tem_controls/toolbox/tool.py @@ -3,7 +3,7 @@ import logging import zmq -from .... import globals +from jungfrau_gui import globals def create_full_mapping(info_queries, more_queries, init_queries, info_queries_client, more_queries_client, init_queries_client): """ diff --git a/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py b/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py index b52af45..3ecbae9 100644 --- a/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py +++ b/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py @@ -2,12 +2,12 @@ QRadioButton, QPushButton, QCheckBox, QDoubleSpinBox, QSizePolicy, QComboBox, QSpinBox, QWidget, QGridLayout) from PySide6.QtGui import QFont -from ..toggle_button import ToggleButton -from ..utils import create_horizontal_line_with_margin +from jungfrau_gui.ui_components.toggle_button import ToggleButton +from jungfrau_gui.ui_components.utils import create_horizontal_line_with_margin +from jungfrau_gui import globals from epoc import ConfigurationClient, auth_token, redis_host -from ... import globals import pyqtgraph as pg import numpy as np @@ -70,6 +70,7 @@ def initUI(self): stage_ctrl_label.setFont(font_big) stage_ctrl_section.addWidget(stage_ctrl_label) + # Speed radio buttons self.hbox_rot = QHBoxLayout() rot_label = QLabel("Rotation Speed:", self) self.rb_speeds = QButtonGroup() @@ -86,6 +87,7 @@ def initUI(self): stage_ctrl_section.addSpacing(10) stage_ctrl_section.addLayout(self.hbox_rot) + # Fast movement buttons self.hbox_move = QHBoxLayout() move_label = QLabel("Fast movement:", self) self.movestages = QButtonGroup() @@ -102,6 +104,15 @@ def initUI(self): self.hbox_move.addWidget(move_label, 1) stage_ctrl_section.addLayout(self.hbox_move) + # Fast movement back buttons (typically going back to crystal before data collection) + self.hbox_back = QHBoxLayout() + self.back_x = QPushButton('Back (X)', self) + self.back_tx = QPushButton('Back (TiltX)', self) + self.hbox_back.addWidget(self.back_x) + self.hbox_back.addWidget(self.back_tx) + stage_ctrl_section.addLayout(self.hbox_back) + + for i in self.rb_speeds.buttons(): self.hbox_rot.addWidget(i, 1) i.setEnabled(False) @@ -110,6 +121,9 @@ def initUI(self): self.hbox_move.addWidget(i, 1) i.setEnabled(False) + self.back_x.setEnabled(False) + self.back_tx.setEnabled(False) + self.hbox_magmode = QHBoxLayout() mode_label = QLabel("Magnification Mode:", self) self.mag_modes = QButtonGroup() @@ -223,9 +237,9 @@ def initUI(self): self.connecttem_button = ToggleButton('Check TEM Connection', self) self.connecttem_button.setEnabled(True) self.polling_frequency = QSpinBox(self) - self.polling_frequency.setMinimum(100) - self.polling_frequency.setMaximum(10000) - self.polling_frequency.setValue(1000) + self.polling_frequency.setMinimum(globals.min_polling_frequency) + self.polling_frequency.setMaximum(globals.max_polling_frequency) + self.polling_frequency.setValue(globals.default_polling_frequency) self.polling_frequency.setSingleStep(100) self.polling_frequency.setPrefix("Polling Freq: ") self.polling_frequency.setSuffix("ms") @@ -265,8 +279,8 @@ def initUI(self): INPUT_layout = QHBoxLayout() input_start_angle_lb = QLabel("Start angle:", self) # current value self.input_start_angle = QDoubleSpinBox(self) - self.input_start_angle.setMaximum(72) - self.input_start_angle.setMinimum(-72) + self.input_start_angle.setMaximum(globals.max_stage_tilt) + self.input_start_angle.setMinimum(-globals.max_stage_tilt) self.input_start_angle.setSuffix('°') self.input_start_angle.setDecimals(1) # self.input_start_angle.setValue("") @@ -279,11 +293,11 @@ def initUI(self): END_layout = QHBoxLayout() end_angle = QLabel("Target angle:", self) self.update_end_angle = QDoubleSpinBox(self) - self.update_end_angle.setMaximum(72) # should be checked with the holder's threshold - self.update_end_angle.setMinimum(-72) + self.update_end_angle.setMaximum(globals.max_stage_tilt) + self.update_end_angle.setMinimum(-globals.max_stage_tilt) self.update_end_angle.setSuffix('°') self.update_end_angle.setDecimals(1) - self.update_end_angle.setValue(60) # will be replaced with configuration file + self.update_end_angle.setValue(globals.default_roation_end) if globals.dev: self.mirror_angles_checkbox = QCheckBox("mirror", self) self.mirror_angles_checkbox.setChecked(False) diff --git a/jungfrau_gui/ui_components/visualization_panel/reader.py b/jungfrau_gui/ui_components/visualization_panel/reader.py index 5b852f6..d1885c6 100644 --- a/jungfrau_gui/ui_components/visualization_panel/reader.py +++ b/jungfrau_gui/ui_components/visualization_panel/reader.py @@ -1,6 +1,6 @@ import logging import numpy as np -from ... import globals +from jungfrau_gui import globals from PySide6.QtCore import QObject, Signal, Slot diff --git a/jungfrau_gui/ui_components/visualization_panel/visualization_panel.py b/jungfrau_gui/ui_components/visualization_panel/visualization_panel.py index f7a2120..c55ca5e 100644 --- a/jungfrau_gui/ui_components/visualization_panel/visualization_panel.py +++ b/jungfrau_gui/ui_components/visualization_panel/visualization_panel.py @@ -9,22 +9,18 @@ QLabel, QPushButton, QSpinBox, QCheckBox, QGridLayout, QSizePolicy, QSpacerItem, QMessageBox) -from epoc import ConfigurationClient, auth_token, redis_host - -from .reader import Reader - -from ... import globals -from ...ui_components.toggle_button import ToggleButton -from ..tem_controls.ui_tem_specific import TEMDetector -from ...ui_components.utils import create_horizontal_line_with_margin - +from jungfrau_gui import globals import jungfrau_gui.ui_threading_helpers as thread_manager +from jungfrau_gui.ui_components.palette import * +from jungfrau_gui.ui_components.toggle_button import ToggleButton +from jungfrau_gui.ui_components.utils import create_horizontal_line_with_margin +from jungfrau_gui.ui_components.tem_controls.toolbox import config as cfg_jf +from jungfrau_gui.ui_components.tem_controls.ui_tem_specific import TEMDetector +from jungfrau_gui.ui_components.tem_controls.toolbox.progress_pop_up import ProgressPopup +from jungfrau_gui.ui_components.visualization_panel.reader import Reader from epoc import JungfraujochWrapper, ConfigurationClient, auth_token, redis_host -from ...ui_components.palette import * from rich import print -from ..tem_controls.toolbox.progress_pop_up import ProgressPopup -from jungfrau_gui.ui_components.tem_controls.toolbox import config as cfg_jf font_big = QFont("Arial", 11) font_big.setBold(True) @@ -138,9 +134,9 @@ def initUI(self): if globals.dev: frame_sum = QLabel("Frames summed:", self) self.frame_summed = QSpinBox(self) - self.frame_summed.setRange(10, 1000) + self.frame_summed.setRange(globals.min_frame_summed, globals.max_frame_summed) self.frame_summed.setSingleStep(10) - self.frame_summed.setValue(100) + self.frame_summed.setValue(globals.default_frame_summed) frame_sum_layout = QHBoxLayout() frame_sum_layout.addWidget(frame_sum) frame_sum_layout.addWidget(self.frame_summed) @@ -484,10 +480,10 @@ def send_command_to_jfjoch(self, command): # Cancel current task self.send_command_to_jfjoch("cancel") self.jfjoch_client.wait_until_idle() - self.jfjoch_client._lots_of_images = 72000 # 2000 hz x 3600 sec / 100 frame-summation - self.jfjoch_client.image_time_us = 50000 # 500 us/frame * 100 frame-summation - logging.info(f"{self.jfjoch_client.image_time_us*1e-3:.2f} ms per image acquisition") - logging.info(f"Nb of frames per trigger: {self.jfjoch_client._lots_of_images}") # 72000 + self.jfjoch_client._lots_of_images = globals.detector_freq * globals.max_duration // globals.default_frame_summed + self.jfjoch_client.image_time_us = globals.default_image_time_us * globals.default_frame_summed + logging.info(f"{self.jfjoch_client.image_time_us/globals.MS_TO_US:.2f} ms per image acquisition") + logging.info(f"Nb of frames per trigger: {self.jfjoch_client._lots_of_images}") logging.info(f"Threshold (in keV) set to: {self.thresholdBox.value()}") self.jfjoch_client.start(n_images = self.jfjoch_client._lots_of_images, fname = "", @@ -531,10 +527,10 @@ def send_command_to_jfjoch(self, command): # self.full_fname.setText(self._full_fpath.as_posix()) # update the GUI widget if globals.dev: - self.jfjoch_client.image_time_us = self.frame_summed.value() * 500 # i.e. 500 us per image for a 2kHz frame rate - self.jfjoch_client._lots_of_images = 2000 * 3600 // self.frame_summed.value() # -> 72000 summed images per hour for a summing factor of 100 at 2kHz frame rate - logging.info(f"Nb of frames per trigger for measurement: {self.jfjoch_client._lots_of_images}") # 72000 - logging.info(f"{self.jfjoch_client.image_time_us*1e-3:.2f} ms per (summed) image acquisition") + self.jfjoch_client.image_time_us = self.frame_summed.value() * globals.default_image_time_us + self.jfjoch_client._lots_of_images = globals.detector_freq * globals.max_duration // self.frame_summed.value() + logging.info(f"Nb of frames per trigger for measurement: {self.jfjoch_client._lots_of_images}") + logging.info(f"{self.jfjoch_client.image_time_us/globals.MS_TO_US:.2f} ms per (summed) image acquisition") prev_contrast = self.parent.histogram.getLevels() self.parent.histogram.setLevels(prev_contrast[0] * self.frame_summed.value() / 100, prev_contrast[1] * self.frame_summed.value() / 100) diff --git a/jungfrau_gui/ui_main_window.py b/jungfrau_gui/ui_main_window.py index fec1eee..17801c9 100644 --- a/jungfrau_gui/ui_main_window.py +++ b/jungfrau_gui/ui_main_window.py @@ -1,19 +1,19 @@ import logging -from . import globals +from jungfrau_gui import globals import numpy as np import pyqtgraph as pg -from .ui_components.overlay import draw_overlay +from jungfrau_gui.ui_components.overlay import draw_overlay from pyqtgraph.dockarea import Dock from PySide6.QtWidgets import (QMainWindow, QVBoxLayout, QWidget, QHBoxLayout, QPushButton, QGridLayout, QMessageBox, QTabWidget, QLabel) from PySide6.QtCore import Qt, QObject, QEvent, QTimer from PySide6.QtGui import QShortcut, QKeySequence -from .ui_components.visualization_panel.visualization_panel import VisualizationPanel -from .ui_components.tem_controls.tem_controls import TemControls -from .ui_components.file_operations.file_operations import FileOperations -from .ui_components.utils import create_gaussian -from .ui_components.toggle_button import ToggleButton +from jungfrau_gui.ui_components.visualization_panel.visualization_panel import VisualizationPanel +from jungfrau_gui.ui_components.tem_controls.tem_controls import TemControls +from jungfrau_gui.ui_components.file_operations.file_operations import FileOperations +from jungfrau_gui.ui_components.utils import create_gaussian +from jungfrau_gui.ui_components.toggle_button import ToggleButton import jungfrau_gui.ui_threading_helpers as thread_manager diff --git a/jungfrau_gui/zmq_receiver.py b/jungfrau_gui/zmq_receiver.py index 019f8b2..7d6feee 100644 --- a/jungfrau_gui/zmq_receiver.py +++ b/jungfrau_gui/zmq_receiver.py @@ -2,9 +2,9 @@ import time import logging import numpy as np -from . import globals +from jungfrau_gui import globals import cbor2 -from .decoder import tag_hook +from jungfrau_gui.decoder import tag_hook # Receiver of the ZMQ stream