From 0b03c984bd7e99cf83631f94842da23e46e7a17b Mon Sep 17 00:00:00 2001 From: appleb_m Date: Wed, 20 May 2026 16:11:44 +0200 Subject: [PATCH] DAQ: MAJOR: Refactored mounting service --- src/aare/daq/daq.py | 286 +++++-------------- src/aare/daq/operations/mounting/__init__.py | 8 + src/aare/daq/operations/mounting/models.py | 26 ++ src/aare/daq/operations/mounting/service.py | 183 ++++++++++++ 4 files changed, 282 insertions(+), 221 deletions(-) diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 5429def3..cb4f7f94 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -56,6 +56,7 @@ from aare.common.sample_geometry import SampleGeometryModel from aare.daq.operations.face_detection import FaceDetectionContext, FaceDetectionService, FaceDetectionResult from aare.daq.operations.loop_centering import LoopCenteringService, LoopCenteringContext from aare.daq.operations.loop_centering.models import LoopCenteringSettings +from aare.daq.operations.mounting import MountingContext, MountingService from aare.devices.area_detector import AutoEnum from aare.devices.jfjoch import JFJochWrapper @@ -123,7 +124,7 @@ class AareDAQ: self._automation_last_sample_name = "" self._automation_samples_in_queue = 0 self._last_mount_error_message = "" - self.__reset_mount_failure_counter("DAQ startup") + self._create_mounting_service().reset_mount_failure_counter("DAQ startup") def _is_hardware_failure(self, error: Exception) -> bool: return isinstance( @@ -567,52 +568,52 @@ class AareDAQ: ), logger=logger, ) - # - # def _create_mounting_service(self): - # return MountingService( - # context=MountingContext( - # cfg=self.__cfg, - # devs=self.__devs, - # mount_position=ABR_POS_MOUNT, - # ), - # logger=logger, - # ) - # - # def _create_raster_service(self): - # return RasterService( - # context=RasterContext( - # cfg=self.__cfg, - # devs=self.__devs, - # mlbox=self.__mlbox, - # jfjoch=self.__jfjoch, - # aare=self.__aare, - # sample_provider=lambda: self.sample, - # sample_geometry_provider=lambda: self.sample_geometry, - # status_provider=lambda: self.status, - # set_state=self.__set_state, - # save_screenshot_db=self.save_screenshot_db, - # upload_raster_diffraction_preview=self._upload_raster_diffraction_preview, - # auto_center_line_scan_top_left=self._auto_center_line_scan_top_left, - # ml_bounding_box=self.__ml_bounding_box, - # ), - # logger=logger, - # ) - # - # def _create_rotation_service(self): - # return RotationService( - # context=RotationContext( - # cfg=self.__cfg, - # devs=self.__devs, - # jfjoch=self.__jfjoch, - # aare=self.__aare, - # sample_provider=lambda: self.sample, - # sample_geometry_provider=lambda: self.sample_geometry, - # status_provider=lambda: self.status, - # set_state=self.__set_state, - # save_screenshot_db=self.save_screenshot_db, - # ), - # logger=logger, - # ) + + def _create_mounting_service(self): + return MountingService( + context=MountingContext( + cfg=self.__cfg, + devs=self.__devs, + mount_position=ABR_POS_MOUNT, + ), + logger=logger, + ) + + def _create_raster_service(self): + return RasterService( + context=RasterContext( + cfg=self.__cfg, + devs=self.__devs, + mlbox=self.__mlbox, + jfjoch=self.__jfjoch, + aare=self.__aare, + sample_provider=lambda: self.sample, + sample_geometry_provider=lambda: self.sample_geometry, + status_provider=lambda: self.status, + set_state=self.__set_state, + save_screenshot_db=self.save_screenshot_db, + upload_raster_diffraction_preview=self._upload_raster_diffraction_preview, + auto_center_line_scan_top_left=self._auto_center_line_scan_top_left, + ml_bounding_box=self.__ml_bounding_box, + ), + logger=logger, + ) + + def _create_rotation_service(self): + return RotationService( + context=RotationContext( + cfg=self.__cfg, + devs=self.__devs, + jfjoch=self.__jfjoch, + aare=self.__aare, + sample_provider=lambda: self.sample, + sample_geometry_provider=lambda: self.sample_geometry, + status_provider=lambda: self.status, + set_state=self.__set_state, + save_screenshot_db=self.save_screenshot_db, + ), + logger=logger, + ) #-------------------------------------------- @@ -703,18 +704,29 @@ class AareDAQ: if previous_sample is not None and previous_sample.db_id is not None: self.__aare.send_sample_event(previous_sample, SampleEventType.UNMOUNTING) + self.__set_state(BeamlineStateEnum.RobotSampleExchange) + if sample is not None and sample.db_id is not None: self.__aare.send_sample_event(sample, SampleEventType.MOUNTING) - self.__mount(sample) + + mounting_result = self._create_mounting_service().execute(target=sample) + + if not mounting_result.success: + raise mounting_result.error or MountingFailed( + mounting_result.comment or "Mount failed" + ) + if previous_sample is not None and previous_sample.db_id is not None: self.__aare.send_sample_event(previous_sample, SampleEventType.UNMOUNTED) self.__set_state(BeamlineStateEnum.SampleAlignment) + if sample is not None and sample.db_id is not None: self.__aare.send_sample_event(sample, SampleEventType.MOUNTED) self.save_screenshot_db(sample.db_id, f"{sample.db_id}_mounted") self.__set_state(BeamlineStateEnum.SampleAlignment) return True + except TransformationInvalidException as e: self._last_mount_error_message = str(e) or "Mount failed" logger.error(f"Mount failed due to invalid transformation: {e}") @@ -725,14 +737,15 @@ class AareDAQ: event_type=SampleEventType.MOUNTFAILED, ) raise + except Exception as e: self._last_mount_error_message = str(e) or "Mount failed" logger.error(f"Mount failed: {e}") previous_sample_unmounted = ( - previous_sample is not None - and previous_sample.db_id is not None - and self._was_previous_sample_unmounted_since(mount_started_at) + previous_sample is not None + and previous_sample.db_id is not None + and self._was_previous_sample_unmounted_since(mount_started_at) ) if previous_sample_unmounted: @@ -1428,20 +1441,7 @@ class AareDAQ: raise def _execute_dry(self, park=True): - self.__devs.tell.check_enable_motion() - self.__devs.tell.wait_not_busy() - self.__devs.tell.set_in_mount_position(True) - - if self.sample: - self.__devs.tell.unmount(wait=True, timeout=60.0) - self.__cfg.current_sample = None - - if park: - """set wait_cold to -1 to go to park after drying""" - self.__devs.tell.dry(wait_cold=-1, wait=True) - else: - """use default tell wait cold after drying""" - self.__devs.tell.dry(wait=True) + self._create_mounting_service().dry(park=park) def park_and_dry(self, park = True): self.__cfg.try_set_busy(timeout=360) @@ -1452,28 +1452,6 @@ class AareDAQ: self.__cfg.state_busy = False logger.error(f"Failed to park and dry: {e}") raise - - def __reset_mount_failure_counter(self, reason: str) -> None: - previous_count = self.__cfg.get_mount_fail_count() - if previous_count > 0: - logger.warning( - f"Resetting mount failure counter from {previous_count} due to: {reason}" - ) - else: - logger.debug(f"Mount failure counter already clear: {reason}") - self.__cfg.record_mount_success() - - def tell_dry(self): - self.__cfg.try_set_busy(timeout=360) - try: - self.__devs.tell.check_enable_motion() - self.__devs.tell.wait_not_busy() - self.__devs.tell.dry(wait=True) - self.__cfg.state_busy = False - except Exception as e: - self.__cfg.state_busy = False - logger.error(f"Failed to dry TELL: {e}") - raise def tell_toggle_blower(self): try: @@ -1501,140 +1479,6 @@ class AareDAQ: logger.error(f"Failed to initialise detector: {e}") raise - - def __magnet_position_sensor_check(self, timeout: float = 1.0, repeat: bool = True): - #TODO check this works, add beamstop z controls and test. - - # logger.info("checking beamstop") - # if self.__devs.beamstop_z.value < 24.0: - # raise Exception("Beamstop Z below 24.0 mm - potentially unsafe with mounting") - if self.__devs.magnet_position_sensor.value != 0: - logger.warning("Goniometer is not in position based on magnet position sensor readout") - for i in range(round(timeout * 10.0)): - if self.__devs.magnet_position_sensor.value == 0: - return - time.sleep(0.1) - logger.error("Goniometer didn't reach position based on magnet position sensor readout") - raise Exception("Goniometer is not in position based on magnet position sensor readout") - - def __mount_fail_handler( - self, - mount_error: bool = False, - dry_after_fail_count: int = 3, - end_automation_after_fail_count: int = 5 - ): - # if mount_error: - # logger.error("Mount failed") - # logger.debug("Mount error: ", extra={"mount_error": mount_error}) - # logger.debug("Currently fail handler disabled due to bugs..?") - if mount_error: - count = self.__cfg.record_mount_failure() - if count > end_automation_after_fail_count: - logger.error(f"Mounting continued to fail after stopping automation, contact your local contact") - logger.debug(f"Clearing mount fail count") - self.__cfg.record_mount_success() - raise CriticalTellException(f"Mounting continued to fail after stopping automation, contact your local contact.") - elif count == end_automation_after_fail_count: - logger.error(f"Mounting failed {count} times, stopping automation") - try: - self._execute_dry(park=True) - except Exception as e: - logger.exception(f"Failed to dry after mount failure: {e}") - raise CriticalTellException(f"Mount failed {count} times in a row, stopping automation.") - elif count == dry_after_fail_count: - #Dry after several fails - logger.warning(f"Mount failed {count} times in a row; drying") - try: - self._execute_dry(park=False) - except Exception as e: - logger.exception(f"Failed to dry after mount failure: {e}") - else: - return - else: - logger.debug("Mounting succeeded, resetting mount failure counter") - self.__cfg.record_mount_success() - - def __mount_handler(self, target : SampleShortInfo): - - value = self.__devs.tell.mount(address=target.tell_address(), force=True, auto_unmount=True, read_dm=False, - wait=True, timeout=360.0) - logger.debug(f"Mount response: {value.value}") - - if isinstance(value, str): - logger.error(f"Unexpected string response from Tell mount: {value}") - raise CriticalTellException(f"Critical error in TELL mount: unexpected response '{value}'") - - if value is None: - logger.error("Tell mount returned no response (None)") - raise CriticalTellException("Critical error in TELL mount: no response") - - safe_val = getattr(value, "value", str(value)) - logger.debug(f"Mount response: {safe_val}") - - if value == TellEventValueEnum.NO_PIN_IN_GRIPPER: - """No Pin in gripper is classed as a non critical mount fail - Unless it happens multiple times in a row, in which case the mount fail handler should dry the gripper - and if it continues stop automation as either there is several missing samples, they are stuck in the dewar - or a problem with the gripper - """ - raise MountingFailed("No Pin in Gripper") - elif value == TellEventValueEnum.PIN_IS_LOST_GRIPPER: - """Pin is lost gripper is classed as a non critical mount fail - Unless it happens multiple times in a row, in which case the mount fail handler should dry the gripper - asit may be a problem with the gripper or the type of pins. If it continues after this automation should stop. - """ - logger.error("Pin is lost") - raise MountingFailed("Pin is lost") - elif value == TellEventValueEnum.ROBOT_CLEAR_AFTER_MOUNT or value == TellEventValueEnum.DRY or value == TellEventValueEnum.COLD: - """Robot clear after mount, tell dry or tell cold are not fails, they are events telling the daq to allow - automation to continue as the robot is busy but will not crash if the end station state is changed - this allows us to save a lot of time in autoamtion""" - logger.info(f"Robot is {getattr(value, 'value', str(value))} - freeing beamline for user") - else: - """Currently there are no other fail messages from Tell""" - # Treat any unknown/unsupported event/value as critical for now - msg = f"Mount response not handled: {getattr(value, 'value', str(value))}" - logger.error(msg) - raise CriticalTellException(f"Critical error in TELL mount: {msg}") - return - - def __mount(self, target: SampleShortInfo | None): - self.__devs.smargon_move_home() - self.__devs.aerotech_pos = ABR_POS_MOUNT - #collimator should be down!!! - self.__magnet_position_sensor_check(timeout=360.0) - self.__devs.tell.check_enable_motion() - self.__devs.tell.wait_not_busy() - self.__devs.tell.set_in_mount_position(True) - previous_sample = self.__cfg.current_sample - if target is None: - if previous_sample is not None: - logger.debug(f"Unmounting sample: {previous_sample}") - try: - self.__devs.tell.unmount(wait=True, timeout=60.0) - self.__cfg.current_sample = target - except Exception as e: - raise UnmountingFailed(f"Failed to unmount: {e}") - else: - try: - #TODO: how to differentiate between mounting and unmounting fails - # during a typucal mount call as this is handled by Tell? - log_msg = f"Mounting sample: {target}" - if previous_sample is not None: - log_msg = (f"Unmounting sample: {previous_sample} \n " - f"and {log_msg}") - logger.debug(log_msg) - self.__mount_handler(target) - self.__mount_fail_handler(mount_error=False) - self.__cfg.current_sample = target - except CriticalTellException as e: - logger.error(f"Critical Tell error: {e}") - raise - except Exception as e: - logger.error(f"Mount failed: {e}") - self.__mount_fail_handler(mount_error=True) - raise - def recovery_unmount_sample(self) -> None: self.__cfg.try_set_busy(timeout=360) try: diff --git a/src/aare/daq/operations/mounting/__init__.py b/src/aare/daq/operations/mounting/__init__.py index e69de29b..ac163171 100644 --- a/src/aare/daq/operations/mounting/__init__.py +++ b/src/aare/daq/operations/mounting/__init__.py @@ -0,0 +1,8 @@ +from aare.daq.operations.mounting.models import MountingContext, MountingResult +from aare.daq.operations.mounting.service import MountingService + +__all__ = [ + "MountingContext", + "MountingResult", + "MountingService", +] \ No newline at end of file diff --git a/src/aare/daq/operations/mounting/models.py b/src/aare/daq/operations/mounting/models.py index e69de29b..ee260b68 100644 --- a/src/aare/daq/operations/mounting/models.py +++ b/src/aare/daq/operations/mounting/models.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass + +from aare.common.coordinate import AerotechCoordinate +from aare.daq.config import BeamlineConfig +from aare.daq.devices import BeamlineDevices + + +@dataclass +class MountingContext: + cfg: BeamlineConfig + devs: BeamlineDevices + mount_position: AerotechCoordinate + + +@dataclass +class MountingResult: + success: bool + mounted_sample: object | None = None + previous_sample: object | None = None + did_unmount_previous: bool = False + error: Exception | None = None + comment: str | None = None + + @property + def is_error(self) -> bool: + return self.error is not None \ No newline at end of file diff --git a/src/aare/daq/operations/mounting/service.py b/src/aare/daq/operations/mounting/service.py index e69de29b..9290ecdb 100644 --- a/src/aare/daq/operations/mounting/service.py +++ b/src/aare/daq/operations/mounting/service.py @@ -0,0 +1,183 @@ +import time + +from aare.common.exception_handler import ( + CriticalTellException, + MountingFailed, + UnmountingFailed, +) +from aare.devices.tell_client import TellEventValueEnum +from aare.daq.operations.mounting.models import MountingContext, MountingResult + + +class MountingService: + def __init__(self, *, context: MountingContext, logger): + self.ctx = context + self.logger = logger + + def reset_mount_failure_counter(self, reason: str) -> None: + previous_count = self.ctx.cfg.get_mount_fail_count() + if previous_count > 0: + self.logger.warning( + f"Resetting mount failure counter from {previous_count} due to: {reason}" + ) + else: + self.logger.debug(f"Mount failure counter already clear: {reason}") + self.ctx.cfg.record_mount_success() + + def _magnet_position_sensor_check(self, timeout: float = 1.0) -> None: + if self.ctx.devs.magnet_position_sensor.value != 0: + self.logger.warning( + "Goniometer is not in position based on magnet position sensor readout" + ) + for _ in range(round(timeout * 10.0)): + if self.ctx.devs.magnet_position_sensor.value == 0: + return + time.sleep(0.1) + + self.logger.error( + "Goniometer didn't reach position based on magnet position sensor readout" + ) + raise Exception("Goniometer is not in position based on magnet position sensor readout") + + def _mount_fail_handler( + self, + *, + mount_error: bool = False, + dry_after_fail_count: int = 3, + end_automation_after_fail_count: int = 5, + ) -> None: + if mount_error: + count = self.ctx.cfg.record_mount_failure() + + if count > end_automation_after_fail_count: + self.logger.error( + "Mounting continued to fail after stopping automation, contact your local contact" + ) + self.logger.debug("Clearing mount fail count") + self.ctx.cfg.record_mount_success() + raise CriticalTellException( + "Mounting continued to fail after stopping automation, contact your local contact." + ) + + if count == end_automation_after_fail_count: + self.logger.error(f"Mounting failed {count} times, stopping automation") + try: + self.execute(target=None) + self.dry(park=True) + except Exception as e: + self.logger.exception(f"Failed to dry after mount failure: {e}") + raise CriticalTellException(f"Mount failed {count} times in a row, stopping automation.") + + if count == dry_after_fail_count: + self.logger.warning(f"Mount failed {count} times in a row; drying") + try: + self.dry(park=False) + except Exception as e: + self.logger.exception(f"Failed to dry after mount failure: {e}") + else: + self.logger.debug("Mounting succeeded, resetting mount failure counter") + self.ctx.cfg.record_mount_success() + + def _mount_handler(self, target) -> None: + value = self.ctx.devs.tell.mount( + address=target.tell_address(), + force=True, + auto_unmount=True, + read_dm=False, + wait=True, + timeout=360.0, + ) + + if isinstance(value, str): + self.logger.error(f"Unexpected string response from Tell mount: {value}") + raise CriticalTellException(f"Critical error in TELL mount: unexpected response '{value}'") + + if value is None: + self.logger.error("Tell mount returned no response (None)") + raise CriticalTellException("Critical error in TELL mount: no response") + + safe_val = getattr(value, "value", str(value)) + self.logger.debug(f"Mount response: {safe_val}") + + if value == TellEventValueEnum.NO_PIN_IN_GRIPPER: + raise MountingFailed("No Pin in Gripper") + + if value == TellEventValueEnum.PIN_IS_LOST_GRIPPER: + self.logger.error("Pin is lost") + raise MountingFailed("Pin is lost") + + if value in ( + TellEventValueEnum.ROBOT_CLEAR_AFTER_MOUNT, + TellEventValueEnum.DRY, + TellEventValueEnum.COLD, + ): + self.logger.info(f"Robot is {safe_val} - freeing beamline for user") + return + + message = f"Mount response not handled: {safe_val}" + self.logger.error(message) + raise CriticalTellException(f"Critical error in TELL mount: {message}") + + def _prepare_mount_hardware(self) -> None: + self.ctx.devs.smargon_move_home() + self.ctx.devs.aerotech_pos = self.ctx.mount_position + self._magnet_position_sensor_check(timeout=360.0) + self.ctx.devs.tell.check_enable_motion() + self.ctx.devs.tell.wait_not_busy() + self.ctx.devs.tell.set_in_mount_position(True) + + def dry(self, *, park: bool) -> None: + self.ctx.devs.tell.check_enable_motion() + self.ctx.devs.tell.wait_not_busy() + self.ctx.devs.tell.set_in_mount_position(True) + + if self.ctx.cfg.current_sample: + self.ctx.devs.tell.unmount(wait=True, timeout=60.0) + self.ctx.cfg.current_sample = None + + if park: + self.ctx.devs.tell.dry(wait_cold=-1, wait=True) + else: + self.ctx.devs.tell.dry(wait=True) + + def execute(self, *, target) -> MountingResult: + try: + self._prepare_mount_hardware() + previous_sample = self.ctx.cfg.current_sample + + if target is None: + if previous_sample is not None: + self.logger.debug(f"Unmounting sample: {previous_sample}") + try: + self.ctx.devs.tell.unmount(wait=True, timeout=60.0) + self.ctx.cfg.current_sample = None + return MountingResult(success=True) + except Exception as e: + raise UnmountingFailed(f"Failed to unmount: {e}") from e + + log_msg = f"Mounting sample: {target}" + if previous_sample is not None: + log_msg = f"Unmounting sample: {previous_sample} \n and {log_msg}" + self.logger.debug(log_msg) + + self._mount_handler(target) + self._mount_fail_handler(mount_error=False) + self.ctx.cfg.current_sample = target + return MountingResult(success=True) + + except CriticalTellException as e: + self.logger.error(f"Critical Tell error: {e}") + return MountingResult(success=False, error=e, comment=str(e)) + + except Exception as e: + self.logger.error(f"Mount failed: {e}") + try: + self._mount_fail_handler(mount_error=True) + except Exception as fail_handler_error: + return MountingResult( + success=False, + error=fail_handler_error, + comment=str(fail_handler_error), + ) + + return MountingResult(success=False, error=e, comment=str(e)) \ No newline at end of file