From 42c4116166c1ce8495b952413c886c1c4b392ded Mon Sep 17 00:00:00 2001 From: Khalil Ferjaoui Date: Thu, 9 Apr 2026 17:28:15 +0200 Subject: [PATCH] fix(gui/record): confirm rotation stop before blanking beam and stopping writer --- .../tem_controls/task/record_task.py | 229 +++++++++++------- 1 file changed, 145 insertions(+), 84 deletions(-) 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 dd57f97..31ecbf3 100644 --- a/jungfrau_gui/ui_components/tem_controls/task/record_task.py +++ b/jungfrau_gui/ui_components/tem_controls/task/record_task.py @@ -3,25 +3,23 @@ import h5py import logging import numpy as np -from .task import Task -from .dectris2xds import XDSparams 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 jungfrau_gui import globals +from jungfrau_gui.ui_components.tem_controls.toolbox.tool import send_with_retries +from jungfrau_gui.ui_components.tem_controls.task.task import Task +from jungfrau_gui.metadata_uploader.metadata_update_client import MetadataNotifier -from .... import globals class RecordTask(Task): reset_rotation_signal = Signal() - # def __init__(self, control_worker, end_angle = 60, log_suffix = 'RotEDlog_test', writer_event=None, standard_h5_recording=False): - def __init__(self, control_worker, end_angle = 60, log_suffix = 'RotEDlog_test', writer_event=None): + def __init__(self, control_worker, end_angle=60, log_suffix='RotEDlog_test', writer_event=None): super().__init__(control_worker, "Record") - self.phi_dot = 0 # 10 deg/s + self.phi_dot = 0 # 10 deg/s self.control = control_worker self.tem_action = self.control.tem_action self.file_operations = self.tem_action.file_operations @@ -30,10 +28,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, 3535, verbose=True) self.cfg = ConfigurationClient(redis_host(), token=auth_token()) - self.metadata_notifier = MetadataNotifier(host = "noether", port = 3463, verbose = False) - # self.standard_h5_recording = standard_h5_recording + self.metadata_notifier = MetadataNotifier(host="noether", port=3463, verbose=False) self.reset_rotation_signal.connect(self.tem_action.reset_rotation_button) @@ -42,48 +39,53 @@ def run(self): if self.writer is not None: try: - self.cfg.data_dir.mkdir(parents=True, exist_ok=True) #TODO! when do we create the data_dir? + self.cfg.data_dir.mkdir(parents=True, exist_ok=True) # TODO! when do we create the data_dir? except Exception as e: - # Handle any unexpected errors error_message = f"An unexpected error occurred: {e}" - QMessageBox.critical(self, "Error", error_message) + QMessageBox.critical(None, "Error", error_message) + + logfile = None + pos = None + stop_reason = "unknown" try: - logfile = None # Initialize logfile to None - phi0 = self.client.GetTiltXAngle() - phi1 = self.end_angle + phi_target = self.end_angle stage_rates = [10.0, 2.0, 1.0, 0.5] phi_dot_idx = self.client.Getf1OverRateTxNum() self.phi_dot = stage_rates[phi_dot_idx] - - # Attempt to open the logfile and catch potential issues + + # Parameters to make premature stop detection more robust + angle_tolerance_deg = 0.5 + max_false_stop_checks = 10 # 10 * 0.1 s ~= 1 second + false_stop_checks = 0 + try: logfile = open(self.log_suffix + '.log', 'w') logging.info("\n\n\n---------OPEN LOG-----------------\n\n\n") - + logfile.write("# TEM Record\n") logfile.write("# TIMESTAMP: " + time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()) + "\n") logfile.write(f"# Initial Angle: {phi0:6.3f} deg\n") - logfile.write(f"# Final Angle (scheduled): {phi1:6.3f} deg\n") + logfile.write(f"# Final Angle (scheduled): {phi_target:6.3f} deg\n") logfile.write(f"# angular Speed: {self.phi_dot:6.2f} deg/s\n") logfile.write(f"# magnification: {self.control.tem_status['eos.GetMagValue_MAG'][0]:<6d} x\n") logfile.write(f"# detector distance: {self.control.tem_status['eos.GetMagValue_DIFF'][0]:<6d} mm\n") - # Beam parameters + try: logfile.write(f"# spot_size: {self.client.GetSpotSize()}\n") logfile.write(f"# alpha_angle: {self.client.GetAlpha()}\n") except Exception as e: logging.error(f"Error retrieving beam parameters: {e}") - # Aperture sizes + try: logfile.write(f"# CL#: {self.client.GetAperatureSize(1)}\n") logfile.write(f"# SA#: {self.client.GetAperatureSize(4)}\n") except Exception as e: logging.error(f"Error retrieving aperture sizes: {e}") - # Lens parameters + try: logfile.write(f"# brightness: {self.client.GetCL3()}\n") logfile.write(f"# diff_focus: {self.client.GetIL1()}\n") @@ -91,17 +93,17 @@ def run(self): logfile.write(f"# PL_align: {self.client.GetPLA()}\n") except Exception as e: logging.error(f"Error retrieving lens parameters: {e}") - # Stage position + try: logfile.write(f"# stage_position: {self.client.GetStagePosition()}\n") except Exception as e: logging.error(f"Error retrieving stage position: {e}") + except FileNotFoundError as fnf_error: logging.error(f"FileNotFoundError: Directory does not exist for logfile: {fnf_error}") return except PermissionError as perm_error: logging.error(f"PermissionError: No write access to logfile: {perm_error}") - # return except Exception as e: logging.error(f"Unexpected error while opening logfile: {e}") return @@ -110,130 +112,184 @@ def run(self): # self.file_operations.update_xtalinfo_signal.emit('Measuring', 'DIALS') self.client.Setf1OverRateTxNum(phi_dot_idx) - time.sleep(1) + time.sleep(1) self.client.SetBeamBlank(0) time.sleep(0.5) - # Send SetTiltXAngle with retry mechanism try: - self.client.SetTiltXAngle(phi1) + self.client.SetTiltXAngle(phi_target) except Exception as e: logging.error(f"Unexpected error while sending SetTiltXAngle: {e}") self.client.SetBeamBlank(1) - logging.warning(f"Beam blanked to protect sample!") + logging.warning("Beam blanked to protect sample!") if not self.client.is_rotating: - # If you can verify the stage is indeed not rotating, then bail out + stop_reason = "set_tilt_failed_and_not_rotating" return else: logging.warning("Stage appears to be rotating despite the error.") self.client.SetBeamBlank(0) logging.warning("Unblanked the beam and ready to proceed...") time.sleep(0.1) - + try: - # Attempt to wait for the rotation to start logging.info("Waiting for stage rotation to start...") self.client.wait_until_rotate_starts() logging.info("Stage has initiated rotation") except Exception as rotation_error: + stop_reason = f"rotation_start_failed: {rotation_error}" logging.error(f"Stage rotation failed to start: {rotation_error}") - return + return - #If enabled we start writing files if self.writer is not None: self.writer[0]() logging.info("\033[1mAsynchronous writing of files is starting now...") t0 = time.time() try: - while self.client.is_rotating: + while True: try: if self.control.interruptRotation: logging.warning("*Interruption request*: Stopping the rotation...") send_with_retries(self.client.StopStage) + stop_reason = "user_interrupt" + break + pos = self.client.GetStagePosition() t = time.time() - # difference in timers of TEM and GUI might cause small error and should be evaluated. - if os.access(os.path.dirname(self.log_suffix), os.W_OK): - logfile.write(f"{t - t0:10.6f} {pos[3]:8.3f} deg\n") - logging.info(f"{t - t0:10.6f} {pos[3]:8.3f} deg") - self.rotations_angles.append([f'{t-t0:10.6f}', f'{pos[3]:8.3f}']) + current_angle = pos[3] + delta = phi_target - current_angle + + log_dir = os.path.dirname(self.log_suffix) + can_write_log = (log_dir == "") or os.access(log_dir, os.W_OK) + + if can_write_log: + logfile.write(f"{t - t0:10.6f} {current_angle:8.3f} deg\n") + + logging.info( + f"{t - t0:10.6f} {current_angle:8.3f} deg " + f"(target={phi_target:8.3f}, delta={delta:7.3f}, rotating={self.client.is_rotating})" + ) + self.rotations_angles.append([f"{t - t0:10.6f}", f"{current_angle:8.3f}"]) + + # Normal stop condition: stage reached target angle + if abs(delta) <= angle_tolerance_deg: + stop_reason = "target_reached" + logging.warning( + f"Stopping acquisition: reached target within {angle_tolerance_deg} deg" + ) + break + + # Suspicious early stop: confirm it for ~1 second before stopping + if not self.client.is_rotating: + false_stop_checks += 1 + logging.warning( + f"is_rotating=False before target " + f"(current={current_angle:.3f}, target={phi_target:.3f}, delta={delta:.3f}) " + f"[{false_stop_checks}/{max_false_stop_checks}]" + ) + if false_stop_checks >= max_false_stop_checks: + stop_reason = "rotation_flag_false_confirmed" + logging.warning("Rotation stop confirmed by repeated checks.") + break + else: + false_stop_checks = 0 + time.sleep(0.1) + except Exception as e: logging.error(f"Error getting stage position, skipping iteration: {e}") + false_stop_checks += 1 + if false_stop_checks >= max_false_stop_checks: + stop_reason = f"too_many_position_errors: {e}" + logging.error("Too many consecutive stage-position errors, stopping acquisition.") + break + time.sleep(0.1) continue + except TimeoutError as te: + stop_reason = f"timeout: {te}" logging.error(f"TimeoutError during rotation: {te}") except Exception as e: - logging.error(f"Unexpected error caught for TEMClient::is_rotating(): {e}") + stop_reason = f"unexpected_rotation_error: {e}" + logging.error(f"Unexpected error caught for TEMClient::is_rotating(): {e}") + + logging.warning(f"Acquisition stop reason: {stop_reason}") - # Stop the file writing if self.writer is not None: logging.info(" ******************** Stopping Data Collection...") + logging.warning("Writer stop requested.") self.writer[1]() - + time.sleep(0.01) + logging.warning("Beam blank ON.") self.client.SetBeamBlank(1) try: - phi1 = self.client.GetTiltXAngle() - if os.access(os.path.dirname(self.log_suffix), os.W_OK): - logfile.write(f"# Final Angle (measured): {phi1:.3f} deg\n") + phi_final = self.client.GetTiltXAngle() + log_dir = os.path.dirname(self.log_suffix) + can_write_log = (log_dir == "") or os.access(log_dir, os.W_OK) + if can_write_log: + logfile.write(f"# Final Angle (measured): {phi_final:.3f} deg\n") except Exception as e: - logging.error(f"Failed to get final tilt angle: {e}") + phi_final = None + logging.error(f"Failed to get final tilt angle: {e}") + + if logfile is not None: + logfile.close() + logfile = None + + if phi_final is not None: + logging.info(f"Stage rotation end at {phi_final:.1f} deg.") + else: + logging.info("Stage rotation ended, but final angle could not be read.") - if os.access(os.path.dirname(self.log_suffix), os.W_OK): logfile.close() - logging.info(f"Stage rotation end at {phi1:.1f} deg.") - - # GUI updates; should be done before Auto-Reset, which modifies the stage status try: - self.control.send_to_tem("#more", asynchronous = False) # Update tem_status map and GUI + self.control.send_to_tem("#more", asynchronous=False) logging.info('TEM-status received.') except Exception as e: - logging.error("Error updating TEM status: {e}") - - # Add H5 info and file finalization; can be launched before stage-resetting + logging.error(f"Error updating TEM status: {e}") + if self.writer is not None: time.sleep(0.1) logging.info(" ******************** Adding Info to H5 over Server...") try: beam_property = { - "beamcenter" : self.cfg.beam_center, - "sigma_width" : self.control.beam_property_fitting[:2], - "angle" : self.control.beam_property_fitting[2], - "illumination" : self.control.beam_intensity, + "beamcenter": self.cfg.beam_center, + "sigma_width": self.control.beam_property_fitting[:2], + "angle": self.control.beam_property_fitting[2], + "illumination": self.control.beam_intensity, } - send_with_retries(self.metadata_notifier.notify_metadata_update, - self.tem_action.visualization_panel.formatted_filename, - self.control.tem_status, - beam_property, - self.rotations_angles, - self.cfg.threshold, - retries=3, - delay=0.1) - + send_with_retries( + self.metadata_notifier.notify_metadata_update, + self.tem_action.visualization_panel.get_full_fname_path(), + self.control.tem_status, + beam_property, + self.rotations_angles, + self.cfg.threshold, + retries=3, + delay=0.1 + ) + self.file_operations.update_xtalinfo_signal.emit('Processing', 'XDS') # self.file_operations.update_xtalinfo_signal.emit('Processing', 'DIALS') except Exception as e: logging.error(f"Metadata Update Error: {e}") self.file_operations.update_xtalinfo_signal.emit('Metadata error', 'XDS') - # Enable auto reset of tilt - if self.tem_action.tem_tasks.autoreset_checkbox.isChecked(): + if self.tem_action.tem_tasks.autoreset_checkbox.isChecked(): logging.info("Return the stage tilt to zero.") try: self.client.Setf1OverRateTxNum(0) - time.sleep(0.1) # Wait for the command to go through + time.sleep(0.1) self.tem_action.tem_stagectrl.move0deg.clicked.emit() - time.sleep(0.5) # Wait for the command to go through before checking rotation state + time.sleep(0.5) except Exception as e: - logging.error(f"Unexpected error @ client.SetTiltXAngle(0): {e}") - pass - - # Waiting for the rotation to end + logging.error(f"Unexpected error @ client.SetTiltXAngle(0): {e}") + pass + try: while self.client.is_rotating: - time.sleep(0.2) # 0.01, but only supressing (reducing) output is necessary + time.sleep(0.2) except Exception as e: logging.error(f'Error during "Auto-Reset" rotation: {e}') @@ -242,12 +298,16 @@ def run(self): else: self.tem_action.trigger_additem.emit('green', 'recorded', pos) self.tem_action.trigger_processed_receiver.emit() + time.sleep(0.5) print("------REACHED END OF TASK----------") - # Restarting TEM polling if not self.tem_action.tem_tasks.connecttem_button.started: - QMetaObject.invokeMethod(self.tem_action.tem_tasks.connecttem_button, "click", Qt.QueuedConnection) + QMetaObject.invokeMethod( + self.tem_action.tem_tasks.connecttem_button, + "click", + Qt.QueuedConnection + ) while not self.tem_action.tem_tasks.connecttem_button.started: time.sleep(0.1) @@ -255,18 +315,19 @@ def run(self): logging.warning('Polling of TEM-info restarted.') except TimeoutError as e: - # Log the timeout error and exit early to avoid writing files logging.error(f"Stage failed to start rotation: {e}") return except Exception as e: logging.error(f"Unexpected error while waiting for rotation to start: {e}") + finally: if logfile is not None: - logfile.close() # Ensure the logfile is closed in case of any errors + logfile.close() + + logging.warning("Beam blank ON in finally cleanup.") self.client.SetBeamBlank(1) time.sleep(0.01) - # Give back control to the TEM inspector for automatic updates of the UI - self.tem_action.tem_controls.gaussian_user_forced_off = False + self.tem_action.tem_controls.gaussian_user_forced_off = False self.reset_rotation_signal.emit() \ No newline at end of file