|
| 1 | +from dataclasses import dataclass |
| 2 | +from typing import Optional, Tuple |
| 3 | + |
| 4 | +from ophyd import Component as Cpt |
| 5 | +from ophyd.status import AndStatus |
| 6 | + |
| 7 | +from dodal.devices.aperture import Aperture |
| 8 | +from dodal.devices.logging_ophyd_device import InfoLoggingDevice |
| 9 | +from dodal.devices.scatterguard import Scatterguard |
| 10 | + |
| 11 | + |
| 12 | +class InvalidApertureMove(Exception): |
| 13 | + pass |
| 14 | + |
| 15 | + |
| 16 | +@dataclass |
| 17 | +class AperturePositions: |
| 18 | + """Holds tuples (miniap_x, miniap_y, miniap_z, scatterguard_x, scatterguard_y) |
| 19 | + representing the motor positions needed to select a particular aperture size. |
| 20 | + """ |
| 21 | + |
| 22 | + LARGE: Tuple[float, float, float, float, float] |
| 23 | + MEDIUM: Tuple[float, float, float, float, float] |
| 24 | + SMALL: Tuple[float, float, float, float, float] |
| 25 | + ROBOT_LOAD: Tuple[float, float, float, float, float] |
| 26 | + |
| 27 | + @classmethod |
| 28 | + def from_gda_beamline_params(cls, params): |
| 29 | + return cls( |
| 30 | + LARGE=( |
| 31 | + params["miniap_x_LARGE_APERTURE"], |
| 32 | + params["miniap_y_LARGE_APERTURE"], |
| 33 | + params["miniap_z_LARGE_APERTURE"], |
| 34 | + params["sg_x_LARGE_APERTURE"], |
| 35 | + params["sg_y_LARGE_APERTURE"], |
| 36 | + ), |
| 37 | + MEDIUM=( |
| 38 | + params["miniap_x_MEDIUM_APERTURE"], |
| 39 | + params["miniap_y_MEDIUM_APERTURE"], |
| 40 | + params["miniap_z_MEDIUM_APERTURE"], |
| 41 | + params["sg_x_MEDIUM_APERTURE"], |
| 42 | + params["sg_y_MEDIUM_APERTURE"], |
| 43 | + ), |
| 44 | + SMALL=( |
| 45 | + params["miniap_x_SMALL_APERTURE"], |
| 46 | + params["miniap_y_SMALL_APERTURE"], |
| 47 | + params["miniap_z_SMALL_APERTURE"], |
| 48 | + params["sg_x_SMALL_APERTURE"], |
| 49 | + params["sg_y_SMALL_APERTURE"], |
| 50 | + ), |
| 51 | + ROBOT_LOAD=( |
| 52 | + params["miniap_x_ROBOT_LOAD"], |
| 53 | + params["miniap_y_ROBOT_LOAD"], |
| 54 | + params["miniap_z_ROBOT_LOAD"], |
| 55 | + params["sg_x_ROBOT_LOAD"], |
| 56 | + params["sg_y_ROBOT_LOAD"], |
| 57 | + ), |
| 58 | + ) |
| 59 | + |
| 60 | + def position_valid(self, pos: Tuple[float, float, float, float, float]): |
| 61 | + """ |
| 62 | + Check if argument 'pos' is a valid position in this AperturePositions object. |
| 63 | + """ |
| 64 | + if pos not in [self.LARGE, self.MEDIUM, self.SMALL, self.ROBOT_LOAD]: |
| 65 | + return False |
| 66 | + return True |
| 67 | + |
| 68 | + |
| 69 | +class ApertureScatterguard(InfoLoggingDevice): |
| 70 | + aperture: Aperture = Cpt(Aperture, "-MO-MAPT-01:") |
| 71 | + scatterguard: Scatterguard = Cpt(Scatterguard, "-MO-SCAT-01:") |
| 72 | + aperture_positions: Optional[AperturePositions] = None |
| 73 | + |
| 74 | + def load_aperture_positions(self, positions: AperturePositions): |
| 75 | + self.aperture_positions = positions |
| 76 | + |
| 77 | + def set(self, pos: Tuple[float, float, float, float, float]) -> AndStatus: |
| 78 | + try: |
| 79 | + assert isinstance(self.aperture_positions, AperturePositions) |
| 80 | + assert self.aperture_positions.position_valid(pos) |
| 81 | + except AssertionError as e: |
| 82 | + raise InvalidApertureMove(repr(e)) |
| 83 | + return self._safe_move_within_datacollection_range(*pos) |
| 84 | + |
| 85 | + def _safe_move_within_datacollection_range( |
| 86 | + self, |
| 87 | + aperture_x: float, |
| 88 | + aperture_y: float, |
| 89 | + aperture_z: float, |
| 90 | + scatterguard_x: float, |
| 91 | + scatterguard_y: float, |
| 92 | + ) -> AndStatus: |
| 93 | + """ |
| 94 | + Move the aperture and scatterguard combo safely to a new position. |
| 95 | + See https://github.com/DiamondLightSource/python-artemis/wiki/Aperture-Scatterguard-Collisions |
| 96 | + for why this is required. |
| 97 | + """ |
| 98 | + # EpicsMotor does not have deadband/MRES field, so the way to check if we are |
| 99 | + # in a datacollection position is to see if we are "ready" (DMOV) and the target |
| 100 | + # position is correct |
| 101 | + ap_z_in_position = self.aperture.z.motor_done_move.get() |
| 102 | + if not ap_z_in_position: |
| 103 | + return |
| 104 | + current_ap_z = self.aperture.z.user_setpoint.get() |
| 105 | + if current_ap_z != aperture_z: |
| 106 | + raise InvalidApertureMove( |
| 107 | + "ApertureScatterguard safe move is not yet defined for positions " |
| 108 | + "outside of LARGE, MEDIUM, SMALL, ROBOT_LOAD." |
| 109 | + ) |
| 110 | + |
| 111 | + current_ap_y = self.aperture.y.user_readback.get() |
| 112 | + if aperture_y > current_ap_y: |
| 113 | + sg_status: AndStatus = self.scatterguard.x.set( |
| 114 | + scatterguard_x |
| 115 | + ) & self.scatterguard.y.set(scatterguard_y) |
| 116 | + sg_status.wait() |
| 117 | + final_status = ( |
| 118 | + sg_status |
| 119 | + & self.aperture.x.set(aperture_x) |
| 120 | + & self.aperture.y.set(aperture_y) |
| 121 | + & self.aperture.z.set(aperture_z) |
| 122 | + ) |
| 123 | + return final_status |
| 124 | + |
| 125 | + else: |
| 126 | + ap_status: AndStatus = ( |
| 127 | + self.aperture.x.set(aperture_x) |
| 128 | + & self.aperture.y.set(aperture_y) |
| 129 | + & self.aperture.z.set(aperture_z) |
| 130 | + ) |
| 131 | + ap_status.wait() |
| 132 | + final_status = ( |
| 133 | + ap_status |
| 134 | + & self.scatterguard.x.set(scatterguard_x) |
| 135 | + & self.scatterguard.y.set(scatterguard_y) |
| 136 | + ) |
| 137 | + return final_status |
0 commit comments