-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(#391) Create an ophyd-async ApertureScatterguard device #419
Changes from 21 commits
e4e5122
adaaf77
7b376be
241414b
2309790
661ec59
7d0f304
ed2b34b
9650bea
41ca74f
41436a3
32be1b5
1b94847
19a03bd
ad5ba13
2de7fa1
b6f7373
418de3d
49fa716
7325b3b
bf1de01
cedcfd2
5c5c97e
9652718
211703f
852adb6
991aaf0
73c1361
a991d2f
3848acb
e173bbe
1142f8b
b2ef997
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,15 @@ | ||
from ophyd import Component, Device, EpicsSignalRO | ||
from ophyd_async.core import StandardReadable | ||
from ophyd_async.epics.signal import epics_signal_r | ||
|
||
from dodal.devices.util.motor_utils import ExtendedEpicsMotor | ||
from dodal.devices.util.motor_utils import ExtendedMotor | ||
|
||
|
||
class Aperture(Device): | ||
x = Component(ExtendedEpicsMotor, "X") | ||
y = Component(ExtendedEpicsMotor, "Y") | ||
z = Component(ExtendedEpicsMotor, "Z") | ||
small = Component(EpicsSignalRO, "Y:SMALL_CALC") | ||
medium = Component(EpicsSignalRO, "Y:MEDIUM_CALC") | ||
large = Component(EpicsSignalRO, "Y:LARGE_CALC") | ||
class Aperture(StandardReadable): | ||
def __init__(self, prefix: str, name: str = "") -> None: | ||
self.x = ExtendedMotor(prefix + "X") | ||
self.y = ExtendedMotor(prefix + "Y") | ||
self.z = ExtendedMotor(prefix + "Z") | ||
self.small = epics_signal_r(int, prefix + "Y:SMALL_CALC") | ||
self.medium = epics_signal_r(int, prefix + "Y:MEDIUM_CALC") | ||
self.large = epics_signal_r(int, prefix + "Y:LARGE_CALC") | ||
super().__init__(name) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,12 @@ | ||
import operator | ||
import asyncio | ||
from collections import namedtuple | ||
from dataclasses import dataclass | ||
from functools import reduce | ||
from typing import List, Sequence | ||
|
||
from ophyd import Component as Cpt | ||
from ophyd import SignalRO | ||
from ophyd.epics_motor import EpicsMotor | ||
from ophyd.status import AndStatus, Status, StatusBase | ||
from bluesky.protocols import Movable | ||
from ophyd_async.core import AsyncStatus, SignalR, StandardReadable | ||
from ophyd_async.core.sim_signal_backend import SimSignalBackend | ||
|
||
from dodal.devices.aperture import Aperture | ||
from dodal.devices.logging_ophyd_device import InfoLoggingDevice | ||
from dodal.devices.scatterguard import Scatterguard | ||
from dodal.log import LOGGER | ||
|
||
|
@@ -33,10 +29,12 @@ class InvalidApertureMove(Exception): | |
|
||
@dataclass | ||
class SingleAperturePosition: | ||
name: str | ||
GDA_name: str | ||
radius_microns: float | None | ||
location: ApertureFiveDimensionalLocation | ||
name: str = "" | ||
GDA_name: str = "" | ||
radius_microns: float | None = 0 | ||
location: ApertureFiveDimensionalLocation = ApertureFiveDimensionalLocation( | ||
0, 0, 0, 0, 0 | ||
) | ||
|
||
|
||
def position_from_params( | ||
|
@@ -74,7 +72,7 @@ def from_gda_beamline_params(cls, params): | |
ROBOT_LOAD=position_from_params("Robot load", "ROBOT_LOAD", None, params), | ||
) | ||
|
||
def as_list(self) -> List[SingleAperturePosition]: | ||
def as_list(self) -> list[SingleAperturePosition]: | ||
return [ | ||
self.LARGE, | ||
self.MEDIUM, | ||
|
@@ -83,29 +81,38 @@ def as_list(self) -> List[SingleAperturePosition]: | |
] | ||
|
||
|
||
class ApertureScatterguard(InfoLoggingDevice): | ||
aperture = Cpt(Aperture, "-MO-MAPT-01:") | ||
scatterguard = Cpt(Scatterguard, "-MO-SCAT-01:") | ||
aperture_positions: AperturePositions | None = None | ||
TOLERANCE_STEPS = 3 # Number of MRES steps | ||
class ApertureScatterguard(StandardReadable, Movable): | ||
def __init__(self, prefix: str = "", name: str = "") -> None: | ||
self.aperture = Aperture(prefix="-MO-MAPT-01:") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
self.scatterguard = Scatterguard(prefix="-MO-SCAT-01:") | ||
self.aperture_positions: AperturePositions | None = None | ||
self.TOLERANCE_STEPS = 3 # Number of MRES steps | ||
self.selected_aperture = self.SelectedAperture( | ||
backend=SimSignalBackend(datatype=SingleAperturePosition, source="") | ||
) | ||
self.set_readable_signals( | ||
read=[ | ||
self.selected_aperture, | ||
] | ||
) | ||
super().__init__(name) | ||
|
||
class SelectedAperture(SignalRO): | ||
def get(self): | ||
class SelectedAperture(SignalR): | ||
async def read(self, *args, **kwargs): | ||
assert isinstance(self.parent, ApertureScatterguard) | ||
return self.parent._get_current_aperture_position() | ||
|
||
selected_aperture = Cpt(SelectedAperture) | ||
await self._backend.put(await self.parent._get_current_aperture_position()) | ||
return {self.name: await self._backend.get_reading()} | ||
|
||
def load_aperture_positions(self, positions: AperturePositions): | ||
LOGGER.info(f"{self.name} loaded in {positions}") | ||
self.aperture_positions = positions | ||
|
||
def set(self, pos: SingleAperturePosition) -> StatusBase: | ||
def set(self, pos: SingleAperturePosition) -> AsyncStatus: | ||
assert isinstance(self.aperture_positions, AperturePositions) | ||
if pos not in self.aperture_positions.as_list(): | ||
raise InvalidApertureMove(f"Unknown aperture: {pos}") | ||
|
||
return self._safe_move_within_datacollection_range(pos.location) | ||
return AsyncStatus(self._safe_move_within_datacollection_range(pos.location)) | ||
|
||
def _get_motor_list(self): | ||
return [ | ||
|
@@ -116,37 +123,49 @@ def _get_motor_list(self): | |
self.scatterguard.y, | ||
] | ||
|
||
def _set_raw_unsafe(self, positions: ApertureFiveDimensionalLocation) -> AndStatus: | ||
motors: Sequence[EpicsMotor] = self._get_motor_list() | ||
return reduce( | ||
operator.and_, [motor.set(pos) for motor, pos in zip(motors, positions)] | ||
async def _set_raw_unsafe(self, positions: ApertureFiveDimensionalLocation): | ||
"""Accept the risks and move in an unsafe way. Collisions possible.""" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. love this docstring There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed! |
||
# unpacking the position | ||
aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = positions | ||
|
||
await asyncio.gather( | ||
self.aperture.x._move(aperture_x), | ||
self.aperture.y._move(aperture_y), | ||
self.aperture.z._move(aperture_z), | ||
self.scatterguard.x._move(scatterguard_x), | ||
self.scatterguard.y._move(scatterguard_y), | ||
) | ||
|
||
def _get_current_aperture_position(self) -> SingleAperturePosition: | ||
async def _get_current_aperture_position(self) -> SingleAperturePosition: | ||
""" | ||
Returns the current aperture position using readback values | ||
for SMALL, MEDIUM, LARGE. ROBOT_LOAD position defined when | ||
mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance. | ||
If no position is found then raises InvalidApertureMove. | ||
""" | ||
assert isinstance(self.aperture_positions, AperturePositions) | ||
current_ap_y = float(self.aperture.y.user_readback.get()) | ||
current_ap_y = await self.aperture.y.readback.get_value(cached=False) | ||
robot_load_ap_y = self.aperture_positions.ROBOT_LOAD.location.aperture_y | ||
tolerance = self.TOLERANCE_STEPS * self.aperture.y.motor_resolution.get() | ||
if int(self.aperture.large.get()) == 1: | ||
tolerance = ( | ||
self.TOLERANCE_STEPS * await self.aperture.y.motor_resolution.get_value() | ||
) | ||
# extendedepicsmotor class has tolerance fields added | ||
# ophyd-async epics motor may need to do the same thing - epics motor | ||
if await self.aperture.large.get_value(cached=False) == 1: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should: I think we now have these fields on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment is now removed |
||
return self.aperture_positions.LARGE | ||
elif int(self.aperture.medium.get()) == 1: | ||
elif await self.aperture.medium.get_value(cached=False) == 1: | ||
return self.aperture_positions.MEDIUM | ||
elif int(self.aperture.small.get()) == 1: | ||
elif await self.aperture.small.get_value(cached=False) == 1: | ||
return self.aperture_positions.SMALL | ||
elif current_ap_y <= robot_load_ap_y + tolerance: | ||
return self.aperture_positions.ROBOT_LOAD | ||
|
||
raise InvalidApertureMove("Current aperture/scatterguard state unrecognised") | ||
|
||
def _safe_move_within_datacollection_range( | ||
async def _safe_move_within_datacollection_range( | ||
self, pos: ApertureFiveDimensionalLocation | ||
) -> StatusBase: | ||
): | ||
""" | ||
Move the aperture and scatterguard combo safely to a new position. | ||
See https://github.com/DiamondLightSource/hyperion/wiki/Aperture-Scatterguard-Collisions | ||
|
@@ -159,19 +178,17 @@ def _safe_move_within_datacollection_range( | |
# unpacking the position | ||
aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = pos | ||
|
||
ap_z_in_position = self.aperture.z.motor_done_move.get() | ||
ap_z_in_position = await self.aperture.z.motor_done_move.get_value() | ||
if not ap_z_in_position: | ||
status: Status = Status(obj=self) | ||
status.set_exception( | ||
InvalidApertureMove( | ||
"ApertureScatterguard z is still moving. Wait for it to finish " | ||
"before triggering another move." | ||
) | ||
raise InvalidApertureMove( | ||
"ApertureScatterguard z is still moving. Wait for it to finish " | ||
"before triggering another move." | ||
) | ||
return status | ||
|
||
current_ap_z = self.aperture.z.user_setpoint.get() | ||
tolerance = self.TOLERANCE_STEPS * self.aperture.z.motor_resolution.get() | ||
current_ap_z = await self.aperture.z.readback.get_value() | ||
tolerance = ( | ||
self.TOLERANCE_STEPS * await self.aperture.z.motor_resolution.get_value() | ||
) | ||
diff_on_z = abs(current_ap_z - aperture_z) | ||
if diff_on_z > tolerance: | ||
raise InvalidApertureMove( | ||
|
@@ -180,28 +197,26 @@ def _safe_move_within_datacollection_range( | |
f"Current aperture z ({current_ap_z}), outside of tolerance ({tolerance}) from target ({aperture_z})." | ||
) | ||
|
||
current_ap_y = self.aperture.y.user_readback.get() | ||
current_ap_y = await self.aperture.y.readback.get_value() | ||
|
||
if aperture_y > current_ap_y: | ||
sg_status: AndStatus = self.scatterguard.x.set( | ||
scatterguard_x | ||
) & self.scatterguard.y.set(scatterguard_y) | ||
sg_status.wait() | ||
return ( | ||
sg_status | ||
& self.aperture.x.set(aperture_x) | ||
& self.aperture.y.set(aperture_y) | ||
& self.aperture.z.set(aperture_z) | ||
await asyncio.gather( | ||
self.scatterguard.x._move(scatterguard_x), | ||
self.scatterguard.y._move(scatterguard_y), | ||
) | ||
|
||
ap_status: AndStatus = ( | ||
self.aperture.x.set(aperture_x) | ||
& self.aperture.y.set(aperture_y) | ||
& self.aperture.z.set(aperture_z) | ||
await asyncio.gather( | ||
self.aperture.x._move(aperture_x), | ||
self.aperture.y._move(aperture_y), | ||
self.aperture.z._move(aperture_z), | ||
) | ||
return | ||
await asyncio.gather( | ||
self.aperture.x._move(aperture_x), | ||
self.aperture.y._move(aperture_y), | ||
self.aperture.z._move(aperture_z), | ||
) | ||
ap_status.wait() | ||
final_status: AndStatus = ( | ||
ap_status | ||
& self.scatterguard.x.set(scatterguard_x) | ||
& self.scatterguard.y.set(scatterguard_y) | ||
|
||
await asyncio.gather( | ||
self.scatterguard.x._move(scatterguard_x), | ||
self.scatterguard.y._move(scatterguard_y), | ||
) | ||
return final_status |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,12 @@ | ||
from ophyd import Component as Cpt | ||
from ophyd import Device | ||
from ophyd_async.core import StandardReadable | ||
|
||
from dodal.devices.util.motor_utils import ExtendedEpicsMotor | ||
from dodal.devices.util.motor_utils import ExtendedMotor | ||
|
||
|
||
class Scatterguard(Device): | ||
x = Cpt(ExtendedEpicsMotor, "X") | ||
y = Cpt(ExtendedEpicsMotor, "Y") | ||
class Scatterguard(StandardReadable): | ||
"""The scatterguard device.""" | ||
|
||
def __init__(self, prefix: str, name: str = "") -> None: | ||
self.x = ExtendedMotor(prefix + "X") | ||
self.y = ExtendedMotor(prefix + "Y") | ||
super().__init__(name) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,16 @@ | ||
from ophyd import Component, EpicsMotor, EpicsSignalRO | ||
from ophyd_async.epics.motion import Motor | ||
from ophyd_async.epics.signal import epics_signal_r | ||
|
||
|
||
class ExtendedEpicsMotor(EpicsMotor): | ||
motor_resolution: Component[EpicsSignalRO] = Component(EpicsSignalRO, ".MRES") | ||
max_velocity: Component[EpicsSignalRO] = Component(EpicsSignalRO, ".VMAX") | ||
|
||
|
||
class ExtendedMotor(Motor): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I forgot that I had done some work to get these into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still trying to get this into |
||
def __init__(self, prefix: str, name: str = ""): | ||
self.motor_resolution = epics_signal_r(float, prefix + ".MRES") | ||
self.max_velocity = epics_signal_r(float, prefix + ".VMAX") | ||
self.motor_done_move = epics_signal_r(float, prefix + ".DMOV") | ||
super().__init__(name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you will want to
from bluesky.protocols import Movable