From 0504276ed44226600367d59c3e229af0da9acb29 Mon Sep 17 00:00:00 2001 From: KiyofumiTakaba Date: Wed, 10 Dec 2025 20:55:28 +0100 Subject: [PATCH 01/13] global constant control --- jungfrau_gui/globals.py | 44 +++++++++++++++ .../metadata_update_client.py | 2 +- .../file_operations/file_operations.py | 12 ++-- .../file_operations/processresult_updater.py | 8 +-- .../tem_controls/connectivity_inspector.py | 2 +- .../tem_controls/task/beam_focus_task.py | 56 +++++++++---------- .../tem_controls/task/record_task.py | 8 +-- .../tem_controls/task/stage_centering_task.py | 9 +-- .../tem_controls/task/task_manager.py | 4 +- .../ui_components/tem_controls/tem_action.py | 12 ++-- .../tem_controls/toolbox/config.py | 4 +- .../tem_controls/ui_tem_specific.py | 16 +++--- .../visualization_panel.py | 20 +++---- 13 files changed, 117 insertions(+), 80 deletions(-) diff --git a/jungfrau_gui/globals.py b/jungfrau_gui/globals.py index 1db1125a..e868b378 100644 --- a/jungfrau_gui/globals.py +++ b/jungfrau_gui/globals.py @@ -55,6 +55,7 @@ def get_git_info(): # jfj = False tem_host = cfg.temserver +tem_port = 3535 dev = False #Configuration nrow = cfg.nrows @@ -87,8 +88,51 @@ def get_git_info(): # constants, presets UM_TO_NM = 1e3 MM_TO_UM = 1e3 +MS_TO_US = 1e3 +S_TO_MS = 1e3 KV_TO_V = 1e3 PIXEL = 0.075 # mm +# TEM control variables default_HT = 200000.00 # V backlash = [100, 80, 0, 0] + +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 diff --git a/jungfrau_gui/metadata_uploader/metadata_update_client.py b/jungfrau_gui/metadata_uploader/metadata_update_client.py index 30b29494..799db72a 100644 --- a/jungfrau_gui/metadata_uploader/metadata_update_client.py +++ b/jungfrau_gui/metadata_uploader/metadata_update_client.py @@ -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 fe43e3a4..e81c0e13 100644 --- a/jungfrau_gui/ui_components/file_operations/file_operations.py +++ b/jungfrau_gui/ui_components/file_operations/file_operations.py @@ -92,7 +92,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): @@ -432,11 +432,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 @@ -498,8 +498,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 @@ -534,7 +534,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 637a0a34..4c1b0423 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 ... 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 29ca6ea1..13c6727f 100644 --- a/jungfrau_gui/ui_components/tem_controls/connectivity_inspector.py +++ b/jungfrau_gui/ui_components/tem_controls/connectivity_inspector.py @@ -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/task/beam_focus_task.py b/jungfrau_gui/ui_components/tem_controls/task/beam_focus_task.py index c1811829..56b28596 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 @@ -10,10 +10,6 @@ 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/record_task.py b/jungfrau_gui/ui_components/tem_controls/task/record_task.py index 55da6274..346b0ad1 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/record_task.py +++ b/jungfrau_gui/ui_components/tem_controls/task/record_task.py @@ -30,9 +30,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.standard_h5_recording = standard_h5_recording self.reset_rotation_signal.connect(self.tem_action.reset_rotation_button) @@ -209,8 +209,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 fa723ee0..9efdd28c 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 @@ -21,16 +21,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) @@ -41,7 +38,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 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 50471f6b..a97b7ae9 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/task_manager.py +++ b/jungfrau_gui/ui_components/tem_controls/task/task_manager.py @@ -64,7 +64,7 @@ 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.task = Task(self, "Dummy") self.task_thread = QThread() @@ -422,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 diff --git a/jungfrau_gui/ui_components/tem_controls/tem_action.py b/jungfrau_gui/ui_components/tem_controls/tem_action.py index 9dc032b1..df461a98 100644 --- a/jungfrau_gui/ui_components/tem_controls/tem_action.py +++ b/jungfrau_gui/ui_components/tem_controls/tem_action.py @@ -985,7 +985,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)) @@ -1011,7 +1011,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)] @@ -1072,7 +1072,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)) @@ -1081,7 +1081,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.rotate(180+cfg_jf.others.rotation_axis_theta) tr.translate(-image.shape[0]/2, -image.shape[1]/2) else: @@ -1145,12 +1145,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/toolbox/config.py b/jungfrau_gui/ui_components/tem_controls/toolbox/config.py index 67c03b58..67a8782f 100644 --- a/jungfrau_gui/ui_components/tem_controls/toolbox/config.py +++ b/jungfrau_gui/ui_components/tem_controls/toolbox/config.py @@ -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) 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 ced729b5..248e46cb 100644 --- a/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py +++ b/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py @@ -223,9 +223,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") @@ -272,8 +272,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("") @@ -286,11 +286,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/visualization_panel.py b/jungfrau_gui/ui_components/visualization_panel/visualization_panel.py index f7a2120d..e4f8f9a7 100644 --- a/jungfrau_gui/ui_components/visualization_panel/visualization_panel.py +++ b/jungfrau_gui/ui_components/visualization_panel/visualization_panel.py @@ -138,9 +138,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 +484,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 +531,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) From 4f6e25e16e5e047cb136f63b8d4757ea393ffb6b Mon Sep 17 00:00:00 2001 From: kferjaoui Date: Sat, 13 Dec 2025 16:42:16 +0100 Subject: [PATCH 02/13] fix: avoid import-time cfg lookups; init globals after CLI parse - Remove Redis-backed cfg access from globals.py module scope - Avoid cfg.temserver as argparse default (prevents ValueError at startup) - Initialize globals via globals.init() after parsing args and resolving cfg --- jungfrau_gui/globals.py | 62 +++++++++++++------- jungfrau_gui/main_ui.py | 125 ++++++++++++++++++++++++++-------------- 2 files changed, 125 insertions(+), 62 deletions(-) diff --git a/jungfrau_gui/globals.py b/jungfrau_gui/globals.py index 1db1125a..70f4b755 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,69 @@ 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 = cfg.temserver +tem_host = "localhost" 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 -PIXEL = 0.075 # mm +KV_TO_V = 1e3 +PIXEL = 0.075 # mm -default_HT = 200000.00 # V +default_HT = 200000.00 # V backlash = [100, 80, 0, 0] + + +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 ac62a19b..6b1f34a6 100755 --- a/jungfrau_gui/main_ui.py +++ b/jungfrau_gui/main_ui.py @@ -20,6 +20,26 @@ import os import datetime +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 @@ -85,73 +105,94 @@ def format(self, record): def main(): os.environ["QT_LOGGING_RULES"] = "qt.core.qobject.connect=false" - + app = QApplication(sys.argv) app.setStyle("Fusion") - cfg = ConfigurationClient(redis_host(), token=auth_token()) - + # ---- Command-Line Interface FIRST (no Redis-backed defaults!) ---- 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="tcp://noether:5501", help="ZMQ stream 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 (defaults to cfg.temserver if set)",) + parser.add_argument("--nrow", type=int, default=None, help="Override detector rows (defaults to cfg.nrows)",) + parser.add_argument("--ncol", type=int, default=None, help="Override detector cols (defaults to 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}") - logging.info(f"Writing console loggings to: {log_file_path}") # Debugging line to verify file creation - 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_formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s", + datefmt="%H:%M:%S", + ) file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) - if args.dtype == np.float32: - globals.cdtype = ctypes.c_float - elif args.dtype == np.double: - cdtype = ctypes.c_double + # ---- Resolve dtype ---- + dtype = _parse_dtype(args.dtype) + if dtype == np.dtype(np.float32): + cdtype = ctypes.c_float 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 - + cdtype = ctypes.c_double + + # ---- Connect to config (Redis) AFTER parsing ---- + cfg = ConfigurationClient(redis_host(), token=auth_token()) + + # Resolve TEM host safely (missing key should NOT crash) + tem_host = args.temhost or _cfg_get(cfg, "temserver", default=None) + if tem_host is None: + # pick a sensible fallback; you can change this + tem_host = "localhost" + logging.warning("cfg.temserver not set; defaulting tem_host to 'localhost'.") + + # Resolve detector geometry safely + nrow = args.nrow if args.nrow is not None else _cfg_get(cfg, "nrows", default=None) + ncol = args.ncol if args.ncol is not None else _cfg_get(cfg, "ncols", default=None) + + if nrow is None or ncol is None: + raise RuntimeError( + "Detector geometry missing (nrows/ncols). " + "Set cfg.nrows/cfg.ncols in Redis or pass --nrow/--ncol." + ) + + # ---- Initialize globals explicitly (NO import-time Redis reads) ---- + from jungfrau_gui import globals + + globals.init( + stream_=args.stream, + dtype_=dtype, + cdtype_=cdtype, + tem_mode_=not args.playmode, + tem_host_=tem_host, + dev_=args.dev, + nrow_=nrow, + ncol_=ncol, + ) + logging.info(f"{get_gui_info()}") if args.version: From 5bf0870a8ca3e59ebe83c610e229715cd05432c8 Mon Sep 17 00:00:00 2001 From: kferjaoui Date: Sat, 13 Dec 2025 20:08:51 +0100 Subject: [PATCH 03/13] refactor: normalize imports to jungfrau_gui.* absolute paths --- jungfrau_gui/main_ui.py | 8 +++---- .../metadata_update_client.py | 2 +- .../file_operations/file_operations.py | 12 +++++----- .../file_operations/processresult_updater.py | 1 - .../tem_controls/connectivity_inspector.py | 2 +- .../tem_controls/gaussian_fitter.py | 2 +- .../tem_controls/gaussian_fitter_mp.py | 6 ++--- .../tem_controls/task/beam_focus_task.py | 4 ++-- .../tem_controls/task/get_teminfo_task.py | 2 +- .../tem_controls/task/record_task.py | 9 ++++--- .../tem_controls/task/stage_centering_task.py | 5 ++-- .../tem_controls/task/task_manager.py | 24 +++++++------------ .../ui_components/tem_controls/tem_action.py | 17 ++++++------- .../tem_controls/tem_controls.py | 16 ++++++------- .../tem_controls/toolbox/config.py | 2 +- .../toolbox/fit_beam_intensity.py | 2 +- .../tem_controls/toolbox/plot_dialog.py | 2 +- .../tem_controls/toolbox/tool.py | 2 +- .../tem_controls/ui_tem_specific.py | 6 ++--- .../visualization_panel/reader.py | 2 +- .../visualization_panel.py | 20 +++++++--------- jungfrau_gui/ui_main_window.py | 14 +++++------ jungfrau_gui/zmq_receiver.py | 4 ++-- 23 files changed, 75 insertions(+), 89 deletions(-) diff --git a/jungfrau_gui/main_ui.py b/jungfrau_gui/main_ui.py index 6b1f34a6..2c24e242 100755 --- a/jungfrau_gui/main_ui.py +++ b/jungfrau_gui/main_ui.py @@ -9,10 +9,10 @@ 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 import globals +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 diff --git a/jungfrau_gui/metadata_uploader/metadata_update_client.py b/jungfrau_gui/metadata_uploader/metadata_update_client.py index 30b29494..958b4fea 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: diff --git a/jungfrau_gui/ui_components/file_operations/file_operations.py b/jungfrau_gui/ui_components/file_operations/file_operations.py index 4e0052aa..931bf2cd 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 diff --git a/jungfrau_gui/ui_components/file_operations/processresult_updater.py b/jungfrau_gui/ui_components/file_operations/processresult_updater.py index 637a0a34..9355ddde 100644 --- a/jungfrau_gui/ui_components/file_operations/processresult_updater.py +++ b/jungfrau_gui/ui_components/file_operations/processresult_updater.py @@ -5,7 +5,6 @@ from datetime import datetime import argparse from pathlib import Path -# from .. import globals from PySide6.QtCore import Signal, Slot, QObject import time diff --git a/jungfrau_gui/ui_components/tem_controls/connectivity_inspector.py b/jungfrau_gui/ui_components/tem_controls/connectivity_inspector.py index 29ca6ea1..1f813e91 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) diff --git a/jungfrau_gui/ui_components/tem_controls/gaussian_fitter.py b/jungfrau_gui/ui_components/tem_controls/gaussian_fitter.py index 90af1957..d68d0ed9 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 9197c25f..46c140b3 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 c1811829..5064f356 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,9 +2,9 @@ import time import logging import numpy as np -from .task import Task +from jungfrau_gui.ui_components.tem_controls.task import Task -from .... import globals +from jungfrau_gui import globals from PySide6.QtCore import Qt, QMetaObject, Signal from datetime import datetime 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 a7ad573b..ec9694dc 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 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 27b8e7f9..1b5c0bea 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 import Task +from jungfrau_gui.metadata_uploader.metadata_update_client import MetadataNotifier class RecordTask(Task): reset_rotation_signal = Signal() 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 20a6917b..da25d24d 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 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 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 1a9b16e7..1f4520c9 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,19 @@ 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 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 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) diff --git a/jungfrau_gui/ui_components/tem_controls/tem_action.py b/jungfrau_gui/ui_components/tem_controls/tem_action.py index 9ee91cdd..f97b330a 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 diff --git a/jungfrau_gui/ui_components/tem_controls/tem_controls.py b/jungfrau_gui/ui_components/tem_controls/tem_controls.py index e2025768..9171f654 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 67c03b58..4bead7fc 100644 --- a/jungfrau_gui/ui_components/tem_controls/toolbox/config.py +++ b/jungfrau_gui/ui_components/tem_controls/toolbox/config.py @@ -12,7 +12,7 @@ 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()) 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 c5e5b039..17f38a17 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 1cde965b..724fc5eb 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 0f41f058..088dec88 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 b52af456..556c8c74 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 diff --git a/jungfrau_gui/ui_components/visualization_panel/reader.py b/jungfrau_gui/ui_components/visualization_panel/reader.py index 5b852f64..d1885c64 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 f7a2120d..93644b48 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) diff --git a/jungfrau_gui/ui_main_window.py b/jungfrau_gui/ui_main_window.py index fec1eeee..17801c92 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 019f8b2f..7d6feee8 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 From b874e6990bd48d5e50487404799eee335b4987c4 Mon Sep 17 00:00:00 2001 From: kferjaoui Date: Sat, 13 Dec 2025 20:58:49 +0100 Subject: [PATCH 04/13] fix: correct global imports --- jungfrau_gui/ui_components/tem_controls/gaussian_fitter_mp.py | 2 +- jungfrau_gui/ui_components/tem_controls/task/beam_focus_task.py | 2 +- .../ui_components/tem_controls/task/get_teminfo_task.py | 2 +- jungfrau_gui/ui_components/tem_controls/task/record_task.py | 2 +- .../ui_components/tem_controls/task/stage_centering_task.py | 2 +- jungfrau_gui/ui_components/tem_controls/task/task_manager.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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 46c140b3..131eadae 100644 --- a/jungfrau_gui/ui_components/tem_controls/gaussian_fitter_mp.py +++ b/jungfrau_gui/ui_components/tem_controls/gaussian_fitter_mp.py @@ -6,7 +6,7 @@ import zmq import cbor2 from jungfrau_gui.decoder import tag_hook -from jungfrau_gui. import globals +from jungfrau_gui import globals 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 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 5064f356..b6e8bdb1 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,7 +2,7 @@ import time import logging import numpy as np -from jungfrau_gui.ui_components.tem_controls.task import Task +from jungfrau_gui.ui_components.tem_controls.task.task import Task from jungfrau_gui import globals from PySide6.QtCore import Qt, QMetaObject, Signal 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 ec9694dc..ec9e8bff 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 jungfrau_gui.ui_components.tem_controls.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 1b5c0bea..7c4b462d 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/record_task.py +++ b/jungfrau_gui/ui_components/tem_controls/task/record_task.py @@ -10,7 +10,7 @@ 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 import Task +from jungfrau_gui.ui_components.tem_controls.task.task import Task from jungfrau_gui.metadata_uploader.metadata_update_client import MetadataNotifier class RecordTask(Task): 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 da25d24d..9e7c3031 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 @@ -3,7 +3,7 @@ import numpy as np from jungfrau_gui import globals -from jungfrau_gui.ui_components.tem_controls.task import Task +from jungfrau_gui.ui_components.tem_controls.task.task import Task from simple_tem import TEMClient from epoc import ConfigurationClient, auth_token, redis_host 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 1f4520c9..d40d7fef 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/task_manager.py +++ b/jungfrau_gui/ui_components/tem_controls/task/task_manager.py @@ -9,7 +9,7 @@ from jungfrau_gui import globals import jungfrau_gui.ui_threading_helpers as thread_manager -from jungfrau_gui.ui_components.tem_controls.task import Task +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 d0cd89a3db08ee337f3ffa071fbd956e00efa1e9 Mon Sep 17 00:00:00 2001 From: kferjaoui Date: Sun, 14 Dec 2025 11:59:59 +0100 Subject: [PATCH 05/13] fix f-string --- .../ui_components/tem_controls/task/stage_centering_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9e7c3031..b61a7e4f 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 @@ -72,7 +72,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) From a76bb52a421a76c377ac40c5d2b27b2d806e3b70 Mon Sep 17 00:00:00 2001 From: kferjaoui Date: Sun, 14 Dec 2025 14:47:58 +0100 Subject: [PATCH 06/13] feat: serialize TEM commands for backlash-safe fast moves - Add single TEM I/O dispatcher and route fast stage moves through it - Execute preload/overshoot + correction as an atomic sequence (no interleaving with other TEM calls) - Keep existing UI signal wiring and TEMClient dependency unchanged --- .../tem_controls/task/task_manager.py | 143 ++++++++++-------- .../tem_controls/task/tem_dispatcher.py | 77 ++++++++++ 2 files changed, 161 insertions(+), 59 deletions(-) create mode 100644 jungfrau_gui/ui_components/tem_controls/task/tem_dispatcher.py 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 1a9b16e7..8bd62a6a 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/task_manager.py +++ b/jungfrau_gui/ui_components/tem_controls/task/task_manager.py @@ -26,6 +26,8 @@ from ..gaussian_fitter_mp import GaussianFitterMP +from jungfrau_gui.ui_components.tem_controls.task.tem_dispatcher import TEMDispatcher + 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) @@ -64,6 +66,7 @@ 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.tem = TEMDispatcher(self.client) self.task = Task(self, "Dummy") self.task_thread = QThread() @@ -749,6 +752,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,71 +765,89 @@ def shutdown(self): logging.error(f'Shutdown of Task Manager triggered error: {e}') pass + def _two_step_sequence(self, axis_fn, value, preload, settle_s=0.05): + """ + 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_with_backlash(self, moverid=0, value=10, backlash=0, button=False, scale=1): + def move_with_backlash(self, moverid=0, value=10.0, preload=0.0, button=False, scale=1.0): """ - Move the stage with backlash correction. - - Args: - moverid: Direction identifier (0-7) - 0,1: +X, -X - 2,3: +Y, -Y - 4,5: +Z, -Z - 6,7: +TX, -TX (tilt) - value: Movement amount - backlash: Backlash correction amount - scale: Scaling factor for movement value + Robust backlash/preload move: + - Computes whether preload should be applied (only on direction change) + - Executes the move as an atomic sequence in the TEMDispatcher thread + - Keeps GUI responsive (no blocking in main thread) """ - # Request current status information + # Ask for fresh status (coalesced, won't spam) QTimer.singleShot(0, lambda: self.send_to_tem("#info", asynchronous=True)) - - # Backlash correction logic - only apply when changing direction - axis = moverid // 2 # Determine which axis (X=0, Y=1, Z=2, TX=3) - direction = moverid % 2 # Determine direction (even=positive, odd=negative) - - # Check if we're continuing in the same direction (no backlash needed) - if direction == 0 and np.sign(self.tem_status["stage.GetPos_diff"][axis]) >= 0: - backlash = 0 - 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]))}, " - f"{self.tem_status['stage.GetPos'][3]:6.2f} {self.tem_status['stage.GetPos_diff'][3]:6.2f}, {backlash}" - ) - - # Get client reference - client = self.client - # Calculate final movement value with backlash compensation + axis = moverid // 2 # X=0, Y=1, Z=2, TX=3 + direction = moverid % 2 # 0=positive, 1=negative + movement_value = value * scale - - # Execute movement in a separate thread based on direction - match moverid: - case 0: # +X - threading.Thread(target=client.SetXRel, args=(movement_value - backlash,)).start() - case 1: # -X - threading.Thread(target=client.SetXRel, args=(movement_value + backlash,)).start() - case 2: # +Y - threading.Thread(target=client.SetYRel, args=(movement_value - backlash,)).start() - case 3: # -Y - threading.Thread(target=client.SetYRel, args=(movement_value + backlash,)).start() - case 4: # +Z - threading.Thread(target=client.SetZRel, args=(movement_value + backlash,)).start() - case 5: # -Z - threading.Thread(target=client.SetZRel, args=(movement_value - backlash,)).start() - case 6: # +TX (tilt) - threading.Thread(target=client.SetTXRel, args=(movement_value + backlash,)).start() - case 7: # -TX (tilt) - threading.Thread(target=client.SetTXRel, args=(movement_value - backlash,)).start() - case _: - logging.warning(f"Undefined moverid {moverid}") - return + last_diff_sign = np.sign(self.tem_status["stage.GetPos_diff"][axis]) + + apply_preload = True + if direction == 0 and last_diff_sign >= 0: + apply_preload = False + elif direction == 1 and last_diff_sign < 0: + apply_preload = False + + effective_preload = float(preload) if (apply_preload and preload != 0) else 0.0 + + client = self.client + + # pick axis function (same as before) + if moverid in (0, 1): + axis_fn = client.SetXRel + elif moverid in (2, 3): + axis_fn = client.SetYRel + elif moverid in (4, 5): + axis_fn = client.SetZRel + elif moverid in (6, 7): + axis_fn = client.SetTXRel + else: + logging.warning(f"Undefined moverid {moverid}") + return + + # 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) - if moverid < 2 and button: # display the previous move to user + # 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;') + 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}") + 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);" + ) 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 00000000..b0ae5a71 --- /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)) From 79552c5a1796a7f80cf26dc99392bc028c74b70f Mon Sep 17 00:00:00 2001 From: kferjaoui Date: Sun, 14 Dec 2025 14:54:12 +0100 Subject: [PATCH 07/13] fix: update docstring for move_with_backlash() --- .../tem_controls/task/task_manager.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 8bd62a6a..6e1bfd7b 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/task_manager.py +++ b/jungfrau_gui/ui_components/tem_controls/task/task_manager.py @@ -793,10 +793,19 @@ def _two_step_sequence(self, axis_fn, value, preload, settle_s=0.05): @Slot(int, float, float, bool) def move_with_backlash(self, moverid=0, value=10.0, preload=0.0, button=False, scale=1.0): """ - Robust backlash/preload move: - - Computes whether preload should be applied (only on direction change) - - Executes the move as an atomic sequence in the TEMDispatcher thread - - Keeps GUI responsive (no blocking in main thread) + 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)) From 567b6b0889af85a33155e0b5d51adbba12b019cf Mon Sep 17 00:00:00 2001 From: kferjaoui Date: Sun, 14 Dec 2025 15:10:39 +0100 Subject: [PATCH 08/13] fix f-string --- .../ui_components/tem_controls/task/stage_centering_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 20a6917b..9b642b94 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 @@ -71,7 +71,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) From e27e2e5ba355d8b2f1f37837d8a8b5cd2df724be Mon Sep 17 00:00:00 2001 From: kferjaoui Date: Sun, 14 Dec 2025 15:44:18 +0100 Subject: [PATCH 09/13] fix: add preload overshoot for fast moves and keep backlash for fine correction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Define globals.preload (e.g. +1 µm / +1°) for two-step overshoot+return moves - Wire stage jog buttons and move_with_backlash() calls to use preload consistently - Note: current backlash/overshoot behavior is only applied on negative moves --- jungfrau_gui/globals.py | 3 ++ .../tem_controls/task/stage_centering_task.py | 6 ++-- .../ui_components/tem_controls/tem_action.py | 32 ++++++++++++++----- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/jungfrau_gui/globals.py b/jungfrau_gui/globals.py index 1db1125a..23e52339 100644 --- a/jungfrau_gui/globals.py +++ b/jungfrau_gui/globals.py @@ -92,3 +92,6 @@ def get_git_info(): default_HT = 200000.00 # V backlash = [100, 80, 0, 0] + +# overshoot/preload used for two-step jog moves +preload = [1000, 1000, 0, 1] # X,Y,Z in nm (1 µm), TX in deg (1°) \ No newline at end of file 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 9b642b94..18f6afeb 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 @@ -74,9 +74,11 @@ def run(self): 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) + # self.control.trigger_movewithbacklash.emit(np.sign(movexy[0]) > 0, -movexy[0]*globals.UM_TO_NM, globals.backlash[0], False) + self.control.trigger_movewithbacklash.emit(np.sign(movexy[0]) > 0, -movexy[0]*globals.UM_TO_NM, globals.preload[0], False) time.sleep(0.5) - self.control.trigger_movewithbacklash.emit((np.sign(movexy[1]) > 0)+2, -movexy[1]*globals.UM_TO_NM, globals.backlash[1], False) + # self.control.trigger_movewithbacklash.emit((np.sign(movexy[1]) > 0)+2, -movexy[1]*globals.UM_TO_NM, globals.backlash[1], False) + self.control.trigger_movewithbacklash.emit((np.sign(movexy[1]) > 0)+2, -movexy[1]*globals.UM_TO_NM, globals.preload[1], False) time.sleep(0.5) else: if tilt_X_abs < 11: diff --git a/jungfrau_gui/ui_components/tem_controls/tem_action.py b/jungfrau_gui/ui_components/tem_controls/tem_action.py index 9ee91cdd..7bf0cd54 100644 --- a/jungfrau_gui/ui_components/tem_controls/tem_action.py +++ b/jungfrau_gui/ui_components/tem_controls/tem_action.py @@ -89,16 +89,22 @@ 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)) + # self.tem_stagectrl.movex10ump.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(0, 10000, globals.backlash[0], True)) + self.tem_stagectrl.movex10ump.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(0, 10000, globals.preload[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)) + # self.tem_stagectrl.movex10umn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(1, -10000, globals.backlash[0], True)) + self.tem_stagectrl.movex10umn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(1, -10000, globals.preload[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)) + # self.tem_stagectrl.move10degp.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(6, 10, globals.backlash[3], False)) + self.tem_stagectrl.move10degp.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(6, 10, globals.preload[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)) + # self.tem_stagectrl.move10degn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(7, -10, globals.backlash[3], False)) + self.tem_stagectrl.move10degn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(7, -10, globals.preload[3], False)) + # 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) @@ -1079,15 +1085,25 @@ def subimageMouseClickEvent(self, event): logging.info("Large movement (> 300 um) is not yet permitted for safety.") return + # if dx >= 0: + # self.control.trigger_movewithbacklash.emit(0, dx, globals.backlash[0], False) + # else: + # self.control.trigger_movewithbacklash.emit(1, dx, globals.backlash[0], False) + # time.sleep(np.abs(dx)/5e4) # assumes speed of movement as > 50 um/s + # if dy >= 0: + # self.control.trigger_movewithbacklash.emit(2, dy, globals.backlash[1], False) + # else: + # self.control.trigger_movewithbacklash.emit(3, dy, globals.backlash[1], False) + if dx >= 0: - self.control.trigger_movewithbacklash.emit(0, dx, globals.backlash[0], False) + self.control.trigger_movewithbacklash.emit(0, dx, globals.preload[0], False) else: - self.control.trigger_movewithbacklash.emit(1, dx, globals.backlash[0], False) + self.control.trigger_movewithbacklash.emit(1, dx, globals.preload[0], False) time.sleep(np.abs(dx)/5e4) # assumes speed of movement as > 50 um/s if dy >= 0: - self.control.trigger_movewithbacklash.emit(2, dy, globals.backlash[1], False) + self.control.trigger_movewithbacklash.emit(2, dy, globals.preload[1], False) else: - self.control.trigger_movewithbacklash.emit(3, dy, globals.backlash[1], False) + self.control.trigger_movewithbacklash.emit(3, dy, globals.preload[1], False) logging.info(f'Move X: {dx/globals.UM_TO_NM:.1f} um, Y: {dy/globals.UM_TO_NM:.1f} um') From 108d9acf621fcbf6c693d646778c592961c514da Mon Sep 17 00:00:00 2001 From: kferjaoui Date: Sun, 14 Dec 2025 21:08:27 +0100 Subject: [PATCH 10/13] fix: remove unused/redundant imports --- jungfrau_gui/main_ui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jungfrau_gui/main_ui.py b/jungfrau_gui/main_ui.py index 2c24e242..fb940a66 100755 --- a/jungfrau_gui/main_ui.py +++ b/jungfrau_gui/main_ui.py @@ -7,9 +7,7 @@ import numpy as np import time from PySide6.QtWidgets import QApplication -from PySide6.QtCore import QCoreApplication -from jungfrau_gui import globals 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 dda021cac67b2e6b17efcb5dbe9c459e70f643d7 Mon Sep 17 00:00:00 2001 From: Khalil Daniel Ferjaoui Date: Tue, 20 Jan 2026 15:45:27 +0100 Subject: [PATCH 11/13] Add preload-based parking + stackable Back for fast stage moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - implement “parking” movements to reduce backlash: move past target by a preload margin, then step back by preload so gears are loaded in the return direction. For e.g; pushing [+10µm] moves +12µm then −2µm; [−10µm] -> −12µm then +2µm; same idea for ±TX degrees - track a per-axis net “away” displacement (accumulator) so repeated parking movements stack; Back returns in one straight move by −net_away (no preload) and then resets the accumulator. - serialize all TEM ZMQ commands through a single dispatcher lane to avoid overlapping REQ/REP calls and reduce timeouts under polling + user actions TODO: tie "back buttons" status control (enable/disable) + accumulator reset to dispatcher success via Qt queued signal/callback (currently toggled from main thread) --- jungfrau_gui/globals.py | 2 +- .../tem_controls/task/stage_centering_task.py | 6 +- .../tem_controls/task/task_manager.py | 199 +++++++++++++++--- .../ui_components/tem_controls/tem_action.py | 55 ++--- .../tem_controls/ui_tem_specific.py | 14 ++ 5 files changed, 217 insertions(+), 59 deletions(-) diff --git a/jungfrau_gui/globals.py b/jungfrau_gui/globals.py index 23e52339..8786a79f 100644 --- a/jungfrau_gui/globals.py +++ b/jungfrau_gui/globals.py @@ -94,4 +94,4 @@ def get_git_info(): backlash = [100, 80, 0, 0] # overshoot/preload used for two-step jog moves -preload = [1000, 1000, 0, 1] # X,Y,Z in nm (1 µm), TX in deg (1°) \ No newline at end of file +preload = [2000, 2000, 0, 1] # X,Y,Z in nm (2 µm), TX in deg (1°) \ No newline at end of file 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 18f6afeb..9b642b94 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 @@ -74,11 +74,9 @@ def run(self): 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) - self.control.trigger_movewithbacklash.emit(np.sign(movexy[0]) > 0, -movexy[0]*globals.UM_TO_NM, globals.preload[0], False) + self.control.trigger_movewithbacklash.emit(np.sign(movexy[0]) > 0, -movexy[0]*globals.UM_TO_NM, globals.backlash[0], False) time.sleep(0.5) - # self.control.trigger_movewithbacklash.emit((np.sign(movexy[1]) > 0)+2, -movexy[1]*globals.UM_TO_NM, globals.backlash[1], False) - self.control.trigger_movewithbacklash.emit((np.sign(movexy[1]) > 0)+2, -movexy[1]*globals.UM_TO_NM, globals.preload[1], False) + self.control.trigger_movewithbacklash.emit((np.sign(movexy[1]) > 0)+2, -movexy[1]*globals.UM_TO_NM, globals.backlash[1], False) time.sleep(0.5) else: if tilt_X_abs < 11: 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 6e1bfd7b..f92744aa 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/task_manager.py +++ b/jungfrau_gui/ui_components/tem_controls/task/task_manager.py @@ -58,6 +58,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() @@ -88,6 +89,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 @@ -765,7 +770,98 @@ def shutdown(self): logging.error(f'Shutdown of Task Manager triggered error: {e}') pass - def _two_step_sequence(self, axis_fn, value, preload, settle_s=0.05): + # 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): + """ + Move the stage with backlash correction. + + Args: + moverid: Direction identifier (0-7) + 0,1: +X, -X + 2,3: +Y, -Y + 4,5: +Z, -Z + 6,7: +TX, -TX (tilt) + value: Movement amount + backlash: Backlash correction amount + scale: Scaling factor for movement value + """ + # Request current status information + QTimer.singleShot(0, lambda: self.send_to_tem("#info", asynchronous=True)) + + # Backlash correction logic - only apply when changing direction + axis = moverid // 2 # Determine which axis (X=0, Y=1, Z=2, TX=3) + direction = moverid % 2 # Determine direction (even=positive, odd=negative) + + # Check if we're continuing in the same direction (no backlash needed) + if direction == 0 and np.sign(self.tem_status["stage.GetPos_diff"][axis]) >= 0: + backlash = 0 + 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/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}" + ) + + # Get client reference + client = self.client + + # Calculate final movement value with backlash compensation + movement_value = value * scale + + # Execute movement in a separate thread based on direction + match moverid: + case 0: # +X + threading.Thread(target=client.SetXRel, args=(movement_value - backlash,)).start() + case 1: # -X + threading.Thread(target=client.SetXRel, args=(movement_value + backlash,)).start() + case 2: # +Y + threading.Thread(target=client.SetYRel, args=(movement_value - backlash,)).start() + case 3: # -Y + threading.Thread(target=client.SetYRel, args=(movement_value + backlash,)).start() + case 4: # +Z + threading.Thread(target=client.SetZRel, args=(movement_value + backlash,)).start() + case 5: # -Z + threading.Thread(target=client.SetZRel, args=(movement_value - backlash,)).start() + case 6: # +TX (tilt) + threading.Thread(target=client.SetTXRel, args=(movement_value + backlash,)).start() + case 7: # -TX (tilt) + threading.Thread(target=client.SetTXRel, args=(movement_value - backlash,)).start() + case _: + logging.warning(f"Undefined moverid {moverid}") + 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/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) @@ -791,7 +887,7 @@ def _two_step_sequence(self, axis_fn, value, preload, settle_s=0.05): @Slot(int, float, float, bool) - def move_with_backlash(self, moverid=0, value=10.0, preload=0.0, button=False, scale=1.0): + 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. @@ -809,40 +905,23 @@ def move_with_backlash(self, moverid=0, value=10.0, preload=0.0, button=False, s """ # Ask for fresh status (coalesced, won't spam) QTimer.singleShot(0, lambda: self.send_to_tem("#info", asynchronous=True)) - - axis = moverid // 2 # X=0, Y=1, Z=2, TX=3 - direction = moverid % 2 # 0=positive, 1=negative - + + axis = moverid // 2 movement_value = value * scale - last_diff_sign = np.sign(self.tem_status["stage.GetPos_diff"][axis]) - - apply_preload = True - if direction == 0 and last_diff_sign >= 0: - apply_preload = False - elif direction == 1 and last_diff_sign < 0: - apply_preload = False + effective_preload = float(preload) #if (apply_preload and preload != 0) else 0.0 - effective_preload = float(preload) if (apply_preload and preload != 0) else 0.0 - - client = self.client + # set longer timeout for slow rotation speeds + MOVE_TIMEOUT_MS = 30000 if axis == 3 else 5000 - # pick axis function (same as before) - if moverid in (0, 1): - axis_fn = client.SetXRel - elif moverid in (2, 3): - axis_fn = client.SetYRel - elif moverid in (4, 5): - axis_fn = client.SetZRel - elif moverid in (6, 7): - axis_fn = client.SetTXRel - else: - logging.warning(f"Undefined moverid {moverid}") - return + # 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: @@ -860,3 +939,67 @@ def move_with_backlash(self, moverid=0, value=10.0, preload=0.0, button=False, s 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/tem_action.py b/jungfrau_gui/ui_components/tem_controls/tem_action.py index 7bf0cd54..e4a16724 100644 --- a/jungfrau_gui/ui_components/tem_controls/tem_action.py +++ b/jungfrau_gui/ui_components/tem_controls/tem_action.py @@ -88,18 +88,31 @@ 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)) - self.tem_stagectrl.movex10ump.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(0, 10000, globals.preload[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)) - self.tem_stagectrl.movex10umn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(1, -10000, globals.preload[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)) - self.tem_stagectrl.move10degp.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(6, 10, globals.preload[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)) - self.tem_stagectrl.move10degn.clicked.connect(lambda: self.control.trigger_movewithbacklash.emit(7, -10, globals.preload[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( @@ -1085,25 +1098,15 @@ def subimageMouseClickEvent(self, event): logging.info("Large movement (> 300 um) is not yet permitted for safety.") return - # if dx >= 0: - # self.control.trigger_movewithbacklash.emit(0, dx, globals.backlash[0], False) - # else: - # self.control.trigger_movewithbacklash.emit(1, dx, globals.backlash[0], False) - # time.sleep(np.abs(dx)/5e4) # assumes speed of movement as > 50 um/s - # if dy >= 0: - # self.control.trigger_movewithbacklash.emit(2, dy, globals.backlash[1], False) - # else: - # self.control.trigger_movewithbacklash.emit(3, dy, globals.backlash[1], False) - if dx >= 0: - self.control.trigger_movewithbacklash.emit(0, dx, globals.preload[0], False) + self.control.trigger_movewithbacklash.emit(0, dx, globals.backlash[0], False) else: - self.control.trigger_movewithbacklash.emit(1, dx, globals.preload[0], False) + self.control.trigger_movewithbacklash.emit(1, dx, globals.backlash[0], False) time.sleep(np.abs(dx)/5e4) # assumes speed of movement as > 50 um/s if dy >= 0: - self.control.trigger_movewithbacklash.emit(2, dy, globals.preload[1], False) + self.control.trigger_movewithbacklash.emit(2, dy, globals.backlash[1], False) else: - self.control.trigger_movewithbacklash.emit(3, dy, globals.preload[1], False) + self.control.trigger_movewithbacklash.emit(3, dy, globals.backlash[1], False) logging.info(f'Move X: {dx/globals.UM_TO_NM:.1f} um, Y: {dy/globals.UM_TO_NM:.1f} um') 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 b52af456..0c41c89e 100644 --- a/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py +++ b/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py @@ -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 TX', 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() From 13b135e480f6ce36c86bf254860cc28eecec1258 Mon Sep 17 00:00:00 2001 From: Khalil Daniel Ferjaoui Date: Tue, 20 Jan 2026 17:29:06 +0100 Subject: [PATCH 12/13] ui(tem): rename Back buttons to 'Back (X)' and 'Back (TX)' for clarity --- jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0c41c89e..d004444f 100644 --- a/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py +++ b/jungfrau_gui/ui_components/tem_controls/ui_tem_specific.py @@ -106,8 +106,8 @@ def initUI(self): # 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 TX', self) + 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) From 4ce77e8d8d9efd3bd230e53a4ea680d16e1e5e3f Mon Sep 17 00:00:00 2001 From: Khalil Daniel Ferjaoui Date: Fri, 13 Feb 2026 10:20:48 +0100 Subject: [PATCH 13/13] refactor: make Redis mandatory at startup, add config bootstrap files Entry point (main_ui.py): - Remove graceful fallback for missing Redis connection; fail fast with logging.critical + sys.exit(1) instead - Remove _cfg_get helper - Simplify parameter resolution to: CLI override -> cfg attribute - Update ABOUT_TEXT version description New files: - epoc-config.yaml: default Redis configuration template with documented fields for detector geometry, network endpoints, acquisition settings, and display overlays - init_redis.py: bootstrap script to populate Redis from YAML (flush_db=True) for fresh deployments --- epoc-config.yaml | 42 +++++ init_redis.py | 11 ++ jungfrau_gui/main_ui.py | 158 +++++++++++------- .../tem_controls/toolbox/config.py | 4 +- 4 files changed, 149 insertions(+), 66 deletions(-) create mode 100644 epoc-config.yaml create mode 100644 init_redis.py diff --git a/epoc-config.yaml b/epoc-config.yaml new file mode 100644 index 00000000..87048c4f --- /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 00000000..5d0fe1bc --- /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/main_ui.py b/jungfrau_gui/main_ui.py index fb940a66..2b26debd 100755 --- a/jungfrau_gui/main_ui.py +++ b/jungfrau_gui/main_ui.py @@ -18,6 +18,41 @@ 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. @@ -103,23 +138,32 @@ def format(self, record): def main(): os.environ["QT_LOGGING_RULES"] = "qt.core.qobject.connect=false" - + app = QApplication(sys.argv) app.setStyle("Fusion") - - # ---- Command-Line Interface FIRST (no Redis-backed defaults!) ---- - parser = argparse.ArgumentParser() - parser.add_argument("-s", "--stream", type=str, default="tcp://noether:5501", help="ZMQ stream 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 (defaults to cfg.temserver if set)",) - parser.add_argument("--nrow", type=int, default=None, help="Override detector rows (defaults to cfg.nrows)",) - parser.add_argument("--ncol", type=int, default=None, help="Override detector cols (defaults to 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",) + # ---- CLI ---- + parser = argparse.ArgumentParser() + 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() # ---- Logger setup ---- @@ -127,7 +171,8 @@ def main(): 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." + f"Invalid log level: {args.log}. " + "Choose from DEBUG, INFO, WARNING, ERROR, CRITICAL." ) logger.setLevel(log_level) @@ -138,50 +183,41 @@ def main(): if args.logfile: 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}") - + 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) - # ---- Resolve dtype ---- + # ---- dtype ---- dtype = _parse_dtype(args.dtype) - if dtype == np.dtype(np.float32): - cdtype = ctypes.c_float - else: - cdtype = ctypes.c_double - - # ---- Connect to config (Redis) AFTER parsing ---- - cfg = ConfigurationClient(redis_host(), token=auth_token()) - - # Resolve TEM host safely (missing key should NOT crash) - tem_host = args.temhost or _cfg_get(cfg, "temserver", default=None) - if tem_host is None: - # pick a sensible fallback; you can change this - tem_host = "localhost" - logging.warning("cfg.temserver not set; defaulting tem_host to 'localhost'.") - - # Resolve detector geometry safely - nrow = args.nrow if args.nrow is not None else _cfg_get(cfg, "nrows", default=None) - ncol = args.ncol if args.ncol is not None else _cfg_get(cfg, "ncols", default=None) - - if nrow is None or ncol is None: - raise RuntimeError( - "Detector geometry missing (nrows/ncols). " - "Set cfg.nrows/cfg.ncols in Redis or pass --nrow/--ncol." - ) + cdtype = ctypes.c_float if dtype == np.dtype(np.float32) else ctypes.c_double - # ---- Initialize globals explicitly (NO import-time Redis reads) ---- + # ---- 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_=args.stream, + stream_=stream, dtype_=dtype, cdtype_=cdtype, tem_mode_=not args.playmode, @@ -191,27 +227,21 @@ def main(): ncol_=ncol, ) - logging.info(f"{get_gui_info()}") + 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/ui_components/tem_controls/toolbox/config.py b/jungfrau_gui/ui_components/tem_controls/toolbox/config.py index 4bead7fc..7677bb75 100644 --- a/jungfrau_gui/ui_components/tem_controls/toolbox/config.py +++ b/jungfrau_gui/ui_components/tem_controls/toolbox/config.py @@ -16,7 +16,6 @@ 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) @@ -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))