Skip to content
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

Allow moving aperture and scatterguard more independently #758

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions src/dodal/beamlines/i03.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@
)
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
from dodal.common.udc_directory_provider import PandASubdirectoryProvider
from dodal.devices.aperturescatterguard import (
ApertureScatterguard,
load_positions_from_beamline_parameters,
load_tolerances_from_beamline_params,
)
from dodal.devices.aperturescatterguard import ApertureConfigData, ApertureScatterguard
from dodal.devices.attenuator import Attenuator
from dodal.devices.backlight import Backlight
from dodal.devices.cryostream import CryoStream
Expand Down Expand Up @@ -64,14 +60,14 @@ def aperture_scatterguard(
object.
"""
params = get_beamline_parameters()
data = ApertureConfigData(params)
return device_instantiation(
device_factory=ApertureScatterguard,
name="aperture_scatterguard",
prefix="",
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
loaded_positions=load_positions_from_beamline_parameters(params),
tolerances=load_tolerances_from_beamline_params(params),
configuration_data=data,
)


Expand Down
9 changes: 2 additions & 7 deletions src/dodal/beamlines/i04.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from dodal.common.beamlines.beamline_parameters import get_beamline_parameters
from dodal.common.beamlines.beamline_utils import device_instantiation
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
from dodal.devices.aperturescatterguard import (
ApertureScatterguard,
load_positions_from_beamline_parameters,
load_tolerances_from_beamline_params,
)
from dodal.devices.aperturescatterguard import ApertureConfigData, ApertureScatterguard
from dodal.devices.attenuator import Attenuator
from dodal.devices.backlight import Backlight
from dodal.devices.beamstop import BeamStop
Expand Down Expand Up @@ -236,8 +232,7 @@ def aperture_scatterguard(
prefix="",
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
loaded_positions=load_positions_from_beamline_parameters(params),
tolerances=load_tolerances_from_beamline_params(params),
configuration_data=ApertureConfigData(params),
)


Expand Down
107 changes: 71 additions & 36 deletions src/dodal/devices/aperturescatterguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ class InvalidApertureMove(Exception):
)


class ApertureInOut(Enum):
OUT = 0
IN = 1


class AperturePosition(Enum):
SMALL = 1
MEDIUM = 2
LARGE = 3


@dataclass
class ApertureScatterguardTolerances:
ap_x: float
Expand Down Expand Up @@ -77,32 +88,10 @@ def position_from_params(
)


def load_tolerances_from_beamline_params(
params: GDABeamlineParameters,
) -> ApertureScatterguardTolerances:
return ApertureScatterguardTolerances(
ap_x=params["miniap_x_tolerance"],
ap_y=params["miniap_y_tolerance"],
ap_z=params["miniap_z_tolerance"],
sg_x=params["sg_x_tolerance"],
sg_y=params["sg_y_tolerance"],
)


class AperturePosition(Enum):
ROBOT_LOAD = 0
SMALL = 1
MEDIUM = 2
LARGE = 3


def load_positions_from_beamline_parameters(
params: GDABeamlineParameters,
) -> dict[AperturePosition, SingleAperturePosition]:
return {
AperturePosition.ROBOT_LOAD: position_from_params(
"Robot load", AperturePositionGDANames.ROBOT_LOAD, None, params
),
AperturePosition.SMALL: position_from_params(
"Small", AperturePositionGDANames.SMALL_APERTURE, 20, params
),
Expand All @@ -115,20 +104,52 @@ def load_positions_from_beamline_parameters(
}


def load_tolerances_from_beamline_params(
params: GDABeamlineParameters,
) -> ApertureScatterguardTolerances:
return ApertureScatterguardTolerances(
ap_x=params["miniap_x_tolerance"],
ap_y=params["miniap_y_tolerance"],
ap_z=params["miniap_z_tolerance"],
sg_x=params["sg_x_tolerance"],
sg_y=params["sg_y_tolerance"],
)


InOutAndPosition = tuple[ApertureInOut, AperturePosition | None]


@dataclass
class ApertureConfigData:
"""Data about the apertures, loaded from beamline parameters"""

tolerances: ApertureScatterguardTolerances
positions: dict[AperturePosition, SingleAperturePosition]
robot_load_position: SingleAperturePosition

def __init__(self, beamline_parameters: GDABeamlineParameters):
self.tolerances = load_tolerances_from_beamline_params(beamline_parameters)
self.positions = load_positions_from_beamline_parameters(beamline_parameters)
self.robot_load_position = position_from_params(
"Robot load", AperturePositionGDANames.ROBOT_LOAD, None, beamline_parameters
)


class ApertureScatterguard(StandardReadable, Movable):
def __init__(
self,
loaded_positions: dict[AperturePosition, SingleAperturePosition],
tolerances: ApertureScatterguardTolerances,
configuration_data: ApertureConfigData,
prefix: str = "",
name: str = "",
) -> None:
self._aperture = Aperture(prefix + "-MO-MAPT-01:")
self._scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
self._loaded_positions = loaded_positions
self._tolerances = tolerances
self._config_data = configuration_data
self._loaded_positions = configuration_data.positions
self._tolerances = configuration_data.tolerances
self._out_y = configuration_data.robot_load_position.location.aperture_y
aperture_backend = SoftSignalBackend(
SingleAperturePosition, self._loaded_positions[AperturePosition.ROBOT_LOAD]
SingleAperturePosition, configuration_data.robot_load_position
)
aperture_backend.converter = self.ApertureConverter()
self.selected_aperture = self.SelectedAperture(backend=aperture_backend)
Expand Down Expand Up @@ -179,9 +200,26 @@ def get_gda_name_for_position(self, position: AperturePosition) -> str:
return detailed_position.GDA_name

@AsyncStatus.wrap
async def set(self, value: AperturePosition):
position = self._loaded_positions[value]
await self._safe_move_within_datacollection_range(position.location)
async def set(self, value: InOutAndPosition):
in_out, position = value
if in_out == ApertureInOut.OUT:
if position:
location = self._loaded_positions[position].location
out_location = ApertureFiveDimensionalLocation(
location.aperture_x,
self._out_y,
location.aperture_z,
location.scatterguard_x,
location.scatterguard_y,
)
return await self._set_raw_unsafe(out_location)
else:
return await self._aperture.y.set(self._out_y)
elif in_out == ApertureInOut.IN:
if not position:
raise InvalidApertureMove("Cannot move in without a selected aperture")
position_detail = self._loaded_positions[position]
await self._safe_move_within_datacollection_range(position_detail.location)

def _get_motor_list(self):
return [
Expand Down Expand Up @@ -215,17 +253,14 @@ async def get_current_aperture_position(self) -> SingleAperturePosition:
If no position is found then raises InvalidApertureMove.
"""
current_ap_y = await self._aperture.y.user_readback.get_value(cached=False)
robot_load_ap_y = self._loaded_positions[
AperturePosition.ROBOT_LOAD
].location.aperture_y
if await self._aperture.large.get_value(cached=False) == 1:
return self._loaded_positions[AperturePosition.LARGE]
elif await self._aperture.medium.get_value(cached=False) == 1:
return self._loaded_positions[AperturePosition.MEDIUM]
elif await self._aperture.small.get_value(cached=False) == 1:
return self._loaded_positions[AperturePosition.SMALL]
elif current_ap_y <= robot_load_ap_y + self._tolerances.ap_y:
return self._loaded_positions[AperturePosition.ROBOT_LOAD]
elif current_ap_y <= self._out_y + self._tolerances.ap_y:
return self._config_data.robot_load_position

raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")

Expand Down Expand Up @@ -253,7 +288,7 @@ async def _safe_move_within_datacollection_range(
if diff_on_z > self._tolerances.ap_z:
raise InvalidApertureMove(
"ApertureScatterguard safe move is not yet defined for positions "
"outside of LARGE, MEDIUM, SMALL, ROBOT_LOAD. "
"outside of LARGE, MEDIUM, SMALL. "
f"Current aperture z ({current_ap_z}), outside of tolerance ({self._tolerances.ap_z}) from target ({aperture_z})."
)

Expand Down
15 changes: 2 additions & 13 deletions tests/devices/system_tests/test_aperturescatterguard_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@
from ophyd_async.core import DeviceCollector

from dodal.devices.aperturescatterguard import (
ApertureConfigData,
AperturePosition,
ApertureScatterguard,
InvalidApertureMove,
load_positions_from_beamline_parameters,
load_tolerances_from_beamline_params,
)

I03_BEAMLINE_PARAMETER_PATH = (
Expand Down Expand Up @@ -63,15 +62,12 @@ def from_file(cls, path: str):
@pytest.fixture
async def ap_sg():
params = GDABeamlineParameters.from_file(I03_BEAMLINE_PARAMETER_PATH)
positions = load_positions_from_beamline_parameters(params) # type:ignore
tolerances = load_tolerances_from_beamline_params(params) # type:ignore

async with DeviceCollector():
ap_sg = ApertureScatterguard(
prefix="BL03S",
name="ap_sg",
loaded_positions=positions,
tolerances=tolerances,
configuration_data=ApertureConfigData(params), # type:ignore
)
return ap_sg

Expand All @@ -94,12 +90,6 @@ def move_to_small(ap_sg: ApertureScatterguard):
yield from bps.abs_set(ap_sg, AperturePosition.SMALL)


@pytest.fixture
def move_to_robotload(ap_sg: ApertureScatterguard):
assert ap_sg._loaded_positions is not None
yield from bps.abs_set(ap_sg, AperturePosition.ROBOT_LOAD)


@pytest.mark.s03
async def test_aperturescatterguard_setup(ap_sg: ApertureScatterguard):
assert ap_sg._loaded_positions is not None
Expand Down Expand Up @@ -179,7 +169,6 @@ async def test_aperturescatterguard_moves_in_correct_order(
"L": ap_sg._loaded_positions[AperturePosition.LARGE],
"M": ap_sg._loaded_positions[AperturePosition.MEDIUM],
"S": ap_sg._loaded_positions[AperturePosition.SMALL],
"R": ap_sg._loaded_positions[AperturePosition.ROBOT_LOAD],
}
pos1 = positions[pos_name_1]
pos2 = positions[pos_name_2]
Expand Down
Loading
Loading