diff --git a/src/mx_bluesky/hyperion/device_setup_plans/utils.py b/src/mx_bluesky/hyperion/device_setup_plans/utils.py index 481190178..f597af2bb 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/utils.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/utils.py @@ -3,6 +3,10 @@ from bluesky import plan_stubs as bps from bluesky import preprocessors as bpp from bluesky.utils import Msg +from dodal.devices.dcm import DCM +from dodal.devices.detector import ( + DetectorParams, +) from dodal.devices.detector.detector_motion import DetectorMotion, ShutterState from dodal.devices.eiger import EigerDetector @@ -12,6 +16,13 @@ ) +def fill_in_energy_if_not_supplied(dcm: DCM, detector_params: DetectorParams): + if not detector_params.expected_energy_ev: + actual_energy_ev = 1000 * (yield from bps.rd(dcm.energy_in_kev)) + detector_params.expected_energy_ev = actual_energy_ev + return detector_params + + def start_preparing_data_collection_then_do_plan( eiger: EigerDetector, detector_motion: DetectorMotion, diff --git a/src/mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py b/src/mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py new file mode 100644 index 000000000..2462a3863 --- /dev/null +++ b/src/mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +import dataclasses +from collections.abc import Generator +from datetime import datetime +from pathlib import Path +from typing import cast + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +from blueapi.core import BlueskyContext +from bluesky.utils import Msg +from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue +from dodal.devices.attenuator import Attenuator +from dodal.devices.dcm import DCM +from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, VFMMirrorVoltages +from dodal.devices.motors import XYZPositioner +from dodal.devices.oav.oav_detector import OAV +from dodal.devices.robot import BartRobot, SampleLocation +from dodal.devices.smargon import Smargon, StubPosition +from dodal.devices.thawer import Thawer +from dodal.devices.undulator_dcm import UndulatorDCM +from dodal.devices.webcam import Webcam +from dodal.devices.xbpm_feedback import XBPMFeedback +from dodal.plans.motor_util_plans import MoveTooLarge, home_and_reset_wrapper + +from mx_bluesky.hyperion.experiment_plans.set_energy_plan import ( + SetEnergyComposite, + set_energy_plan, +) +from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.hyperion.parameters.constants import CONST +from mx_bluesky.hyperion.parameters.robot_load import RobotLoadAndEnergyChange + + +@dataclasses.dataclass +class RobotLoadAndEnergyChangeComposite: + # SetEnergyComposite fields + vfm: FocusingMirrorWithStripes + vfm_mirror_voltages: VFMMirrorVoltages + dcm: DCM + undulator_dcm: UndulatorDCM + xbpm_feedback: XBPMFeedback + attenuator: Attenuator + + # RobotLoad fields + robot: BartRobot + webcam: Webcam + lower_gonio: XYZPositioner + thawer: Thawer + oav: OAV + smargon: Smargon + aperture_scatterguard: ApertureScatterguard + + +def create_devices(context: BlueskyContext) -> RobotLoadAndEnergyChangeComposite: + from mx_bluesky.hyperion.utils.context import device_composite_from_context + + return device_composite_from_context(context, RobotLoadAndEnergyChangeComposite) + + +def wait_for_smargon_not_disabled(smargon: Smargon, timeout=60): + """Waits for the smargon disabled flag to go low. The robot hardware is responsible + for setting this to low when it is safe to move. It does this through a physical + connection between the robot and the smargon. + """ + LOGGER.info("Waiting for smargon enabled") + SLEEP_PER_CHECK = 0.1 + times_to_check = int(timeout / SLEEP_PER_CHECK) + for _ in range(times_to_check): + smargon_disabled = yield from bps.rd(smargon.disabled) + if not smargon_disabled: + LOGGER.info("Smargon now enabled") + return + yield from bps.sleep(SLEEP_PER_CHECK) + raise TimeoutError( + "Timed out waiting for smargon to become enabled after robot load" + ) + + +def take_robot_snapshots(oav: OAV, webcam: Webcam, directory: Path): + time_now = datetime.now() + snapshot_format = f"{time_now.strftime('%H%M%S')}_{{device}}_after_load" + for device in [oav.snapshot, webcam]: + yield from bps.abs_set( + device.filename, snapshot_format.format(device=device.name) + ) + yield from bps.abs_set(device.directory, str(directory)) + # Note: should be able to use `wait=True` after https://github.com/bluesky/bluesky/issues/1795 + yield from bps.trigger(device, group="snapshots") + yield from bps.wait("snapshots") + + +def prepare_for_robot_load( + aperture_scatterguard: ApertureScatterguard, smargon: Smargon +): + yield from bps.abs_set( + aperture_scatterguard, + ApertureValue.ROBOT_LOAD, + group="prepare_robot_load", + ) + + yield from bps.mv(smargon.stub_offsets, StubPosition.RESET_TO_ROBOT_LOAD) + + # fmt: off + yield from bps.mv(smargon.x, 0, + smargon.y, 0, + smargon.z, 0, + smargon.omega, 0, + smargon.chi, 0, + smargon.phi, 0) + # fmt: on + + yield from bps.wait("prepare_robot_load") + + +def do_robot_load( + composite: RobotLoadAndEnergyChangeComposite, + sample_location: SampleLocation, + demand_energy_ev: float | None, + thawing_time: float, +): + yield from bps.abs_set( + composite.robot, + sample_location, + group="robot_load", + ) + + if demand_energy_ev: + yield from set_energy_plan( + demand_energy_ev / 1000, + cast(SetEnergyComposite, composite), + ) + + yield from bps.wait("robot_load") + + yield from bps.abs_set( + composite.thawer.thaw_for_time_s, thawing_time, group="thawing_finished" + ) + yield from wait_for_smargon_not_disabled(composite.smargon) + + +def raise_exception_if_moved_out_of_cryojet(exception): + yield from bps.null() + if isinstance(exception, MoveTooLarge): + raise Exception( + f"Moving {exception.axis} back to {exception.position} after \ + robot load would move it out of the cryojet. The max safe \ + distance is {exception.maximum_move}" + ) + + +def pin_already_loaded( + robot: BartRobot, sample_location: SampleLocation +) -> Generator[Msg, None, bool]: + current_puck = yield from bps.rd(robot.current_puck) + current_pin = yield from bps.rd(robot.current_pin) + return ( + int(current_puck) == sample_location.puck + and int(current_pin) == sample_location.pin + ) + + +def robot_load_and_snapshots( + composite: RobotLoadAndEnergyChangeComposite, + location: SampleLocation, + snapshot_directory: Path, + thawing_time: float, + demand_energy_ev: float | None, +): + robot_load_plan = do_robot_load( + composite, + location, + demand_energy_ev, + thawing_time, + ) + + # The lower gonio must be in the correct position for the robot load and we + # want to put it back afterwards. Note we don't wait the robot is interlocked + # to the lower gonio and the move is quicker than the robot takes to get to the + # load position. + yield from bpp.contingency_wrapper( + home_and_reset_wrapper( + robot_load_plan, + composite.lower_gonio, + BartRobot.LOAD_TOLERANCE_MM, + CONST.HARDWARE.CRYOJET_MARGIN_MM, + "lower_gonio", + wait_for_all=False, + ), + except_plan=raise_exception_if_moved_out_of_cryojet, + ) + + yield from take_robot_snapshots(composite.oav, composite.webcam, snapshot_directory) + + yield from bps.create(name=CONST.DESCRIPTORS.ROBOT_LOAD) + yield from bps.read(composite.robot.barcode) + yield from bps.read(composite.oav.snapshot) + yield from bps.read(composite.webcam) + yield from bps.save() + + yield from bps.wait("reset-lower_gonio") + + +def robot_load_and_change_energy_plan( + composite: RobotLoadAndEnergyChangeComposite, + params: RobotLoadAndEnergyChange, +): + assert params.sample_puck is not None + assert params.sample_pin is not None + + sample_location = SampleLocation(params.sample_puck, params.sample_pin) + + yield from prepare_for_robot_load( + composite.aperture_scatterguard, composite.smargon + ) + yield from bpp.run_wrapper( + robot_load_and_snapshots( + composite, + sample_location, + params.snapshot_directory, + params.thawing_time, + params.demand_energy_ev, + ), + md={ + "subplan_name": CONST.PLAN.ROBOT_LOAD, + "metadata": { + "visit": params.visit, + "sample_id": params.sample_id, + "sample_puck": sample_location.puck, + "sample_pin": sample_location.pin, + }, + "activate_callbacks": [ + "RobotLoadISPyBCallback", + ], + }, + ) diff --git a/src/mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py b/src/mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py index d63a4fa80..59ac56946 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py @@ -1,16 +1,10 @@ from __future__ import annotations import dataclasses -from collections.abc import Generator -from datetime import datetime -from pathlib import Path from typing import cast -import bluesky.plan_stubs as bps -import bluesky.preprocessors as bpp from blueapi.core import BlueskyContext, MsgGenerator -from bluesky.utils import Msg -from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue +from dodal.devices.aperturescatterguard import ApertureScatterguard from dodal.devices.attenuator import Attenuator from dodal.devices.backlight import Backlight from dodal.devices.dcm import DCM @@ -24,7 +18,7 @@ from dodal.devices.oav.pin_image_recognition import PinTipDetection from dodal.devices.robot import BartRobot, SampleLocation from dodal.devices.s4_slit_gaps import S4SlitGaps -from dodal.devices.smargon import Smargon, StubPosition +from dodal.devices.smargon import Smargon from dodal.devices.synchrotron import Synchrotron from dodal.devices.thawer import Thawer from dodal.devices.undulator import Undulator @@ -34,10 +28,11 @@ from dodal.devices.zebra import Zebra from dodal.devices.zebra_controlled_shutter import ZebraShutter from dodal.devices.zocalo import ZocaloResults -from dodal.plans.motor_util_plans import MoveTooLarge, home_and_reset_wrapper +from dodal.log import LOGGER from ophyd_async.fastcs.panda import HDFPanda from mx_bluesky.hyperion.device_setup_plans.utils import ( + fill_in_energy_if_not_supplied, start_preparing_data_collection_then_do_plan, ) from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import ( @@ -46,12 +41,11 @@ from mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan import ( pin_centre_then_xray_centre_plan, ) -from mx_bluesky.hyperion.experiment_plans.set_energy_plan import ( - SetEnergyComposite, - read_energy, - set_energy_plan, +from mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy import ( + RobotLoadAndEnergyChangeComposite, + pin_already_loaded, + robot_load_and_change_energy_plan, ) -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import RobotLoadThenCentre @@ -100,144 +94,6 @@ def create_devices(context: BlueskyContext) -> RobotLoadThenCentreComposite: return device_composite_from_context(context, RobotLoadThenCentreComposite) -def wait_for_smargon_not_disabled(smargon: Smargon, timeout=60): - """Waits for the smargon disabled flag to go low. The robot hardware is responsible - for setting this to low when it is safe to move. It does this through a physical - connection between the robot and the smargon. - """ - LOGGER.info("Waiting for smargon enabled") - SLEEP_PER_CHECK = 0.1 - times_to_check = int(timeout / SLEEP_PER_CHECK) - for _ in range(times_to_check): - smargon_disabled = yield from bps.rd(smargon.disabled) - if not smargon_disabled: - LOGGER.info("Smargon now enabled") - return - yield from bps.sleep(SLEEP_PER_CHECK) - raise TimeoutError( - "Timed out waiting for smargon to become enabled after robot load" - ) - - -def take_robot_snapshots(oav: OAV, webcam: Webcam, directory: Path): - time_now = datetime.now() - snapshot_format = f"{time_now.strftime('%H%M%S')}_{{device}}_after_load" - for device in [oav.snapshot, webcam]: - yield from bps.abs_set( - device.filename, snapshot_format.format(device=device.name) - ) - yield from bps.abs_set(device.directory, str(directory)) - # Note: should be able to use `wait=True` after https://github.com/bluesky/bluesky/issues/1795 - yield from bps.trigger(device, group="snapshots") - yield from bps.wait("snapshots") - - -def prepare_for_robot_load(composite: RobotLoadThenCentreComposite): - yield from bps.abs_set( - composite.aperture_scatterguard, - ApertureValue.ROBOT_LOAD, - group="prepare_robot_load", - ) - - yield from bps.mv(composite.smargon.stub_offsets, StubPosition.RESET_TO_ROBOT_LOAD) - - # fmt: off - yield from bps.mv(composite.smargon.x, 0, - composite.smargon.y, 0, - composite.smargon.z, 0, - composite.smargon.omega, 0, - composite.smargon.chi, 0, - composite.smargon.phi, 0) - # fmt: on - - yield from bps.wait("prepare_robot_load") - - -def do_robot_load( - composite: RobotLoadThenCentreComposite, - sample_location: SampleLocation, - demand_energy_ev: float | None, - thawing_time: float, -): - yield from bps.abs_set( - composite.robot, - sample_location, - group="robot_load", - ) - - if demand_energy_ev: - yield from set_energy_plan( - demand_energy_ev / 1000, - cast(SetEnergyComposite, composite), - ) - - yield from bps.wait("robot_load") - - yield from bps.abs_set( - composite.thawer.thaw_for_time_s, thawing_time, group="thawing_finished" - ) - yield from wait_for_smargon_not_disabled(composite.smargon) - - -def raise_exception_if_moved_out_of_cryojet(exception): - yield from bps.null() - if isinstance(exception, MoveTooLarge): - raise Exception( - f"Moving {exception.axis} back to {exception.position} after \ - robot load would move it out of the cryojet. The max safe \ - distance is {exception.maximum_move}" - ) - - -def _pin_already_loaded( - robot: BartRobot, pin_to_load: int, puck_to_load: int -) -> Generator[Msg, None, bool]: - current_puck = yield from bps.rd(robot.current_puck) - current_pin = yield from bps.rd(robot.current_pin) - return int(current_puck) == puck_to_load and int(current_pin) == pin_to_load - - -def robot_load_and_snapshots( - composite: RobotLoadThenCentreComposite, - params: RobotLoadThenCentre, - location: SampleLocation, -): - robot_load_plan = do_robot_load( - composite, - location, - params.demand_energy_ev, - params.thawing_time, - ) - - # The lower gonio must be in the correct position for the robot load and we - # want to put it back afterwards. Note we don't wait the robot is interlocked - # to the lower gonio and the move is quicker than the robot takes to get to the - # load position. - yield from bpp.contingency_wrapper( - home_and_reset_wrapper( - robot_load_plan, - composite.lower_gonio, - BartRobot.LOAD_TOLERANCE_MM, - CONST.HARDWARE.CRYOJET_MARGIN_MM, - "lower_gonio", - wait_for_all=False, - ), - except_plan=raise_exception_if_moved_out_of_cryojet, - ) - - yield from take_robot_snapshots( - composite.oav, composite.webcam, params.snapshot_directory - ) - - yield from bps.create(name=CONST.DESCRIPTORS.ROBOT_LOAD) - yield from bps.read(composite.robot.barcode) - yield from bps.read(composite.oav.snapshot) - yield from bps.read(composite.webcam) - yield from bps.save() - - yield from bps.wait("reset-lower_gonio") - - def centring_plan_from_robot_load_params( composite: RobotLoadThenCentreComposite, params: RobotLoadThenCentre, @@ -251,23 +107,10 @@ def centring_plan_from_robot_load_params( def robot_load_then_centre_plan( composite: RobotLoadThenCentreComposite, params: RobotLoadThenCentre, - sample_location: SampleLocation, ): - yield from prepare_for_robot_load(composite) - yield from bpp.run_wrapper( - robot_load_and_snapshots(composite, params, sample_location), - md={ - "subplan_name": CONST.PLAN.ROBOT_LOAD, - "metadata": { - "visit_path": str(params.visit_directory), - "sample_id": params.sample_id, - "sample_puck": params.sample_puck, - "sample_pin": params.sample_pin, - }, - "activate_callbacks": [ - "RobotLoadISPyBCallback", - ], - }, + yield from robot_load_and_change_energy_plan( + cast(RobotLoadAndEnergyChangeComposite, composite), + params.robot_load_params(), ) yield from centring_plan_from_robot_load_params(composite, params) @@ -283,10 +126,10 @@ def robot_load_then_centre( assert parameters.sample_puck is not None assert parameters.sample_pin is not None + sample_location = SampleLocation(parameters.sample_puck, parameters.sample_pin) + doing_sample_load = not ( - yield from _pin_already_loaded( - composite.robot, parameters.sample_pin, parameters.sample_puck - ) + yield from pin_already_loaded(composite.robot, sample_location) ) doing_chi_change = parameters.chi_start_deg is not None @@ -295,7 +138,6 @@ def robot_load_then_centre( plan = robot_load_then_centre_plan( composite, parameters, - SampleLocation(parameters.sample_puck, parameters.sample_pin), ) LOGGER.info("Pin not loaded, loading and centring") elif doing_chi_change: @@ -305,12 +147,10 @@ def robot_load_then_centre( LOGGER.info("Pin already loaded and chi not changed so doing nothing") return - detector_params = parameters.detector_params - if not detector_params.expected_energy_ev: - actual_energy_ev = 1000 * ( - yield from read_energy(cast(SetEnergyComposite, composite)) - ) - detector_params.expected_energy_ev = actual_energy_ev + detector_params = yield from fill_in_energy_if_not_supplied( + composite.dcm, parameters.detector_params + ) + eiger.set_detector_parameters(detector_params) yield from start_preparing_data_collection_then_do_plan( diff --git a/src/mx_bluesky/hyperion/experiment_plans/set_energy_plan.py b/src/mx_bluesky/hyperion/experiment_plans/set_energy_plan.py index 7cdaeaf8f..fef5e54e1 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/set_energy_plan.py @@ -6,11 +6,8 @@ """ import dataclasses -from collections.abc import Generator -from typing import Any from bluesky import plan_stubs as bps -from bluesky.utils import Msg from dodal.devices.attenuator import Attenuator from dodal.devices.dcm import DCM from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, VFMMirrorVoltages @@ -51,11 +48,6 @@ def _set_energy_plan( yield from bps.wait(group=UNDULATOR_GROUP) -def read_energy(composite: SetEnergyComposite) -> Generator[Msg, Any, float]: - """Obtain the energy in kev""" - return (yield from bps.rd(composite.dcm.energy_in_kev)) # type: ignore - - def set_energy_plan( energy_kev, composite: SetEnergyComposite, diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py index feb2087d3..a52c2dd0c 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py @@ -1,7 +1,5 @@ from __future__ import annotations -import re - from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( DataCollectionGroupInfo, DataCollectionInfo, @@ -11,7 +9,6 @@ I03_EIGER_DETECTOR, ) from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import ( - VISIT_PATH_REGEX, get_current_time_string, ) from mx_bluesky.hyperion.parameters.components import DiffractionExperimentWithSample @@ -63,8 +60,3 @@ def get_proposal_and_session_from_visit_string(visit_string: str) -> tuple[str, visit_parts = visit_string.split("-") assert len(visit_parts) == 2, f"Unexpected visit string {visit_string}" return visit_parts[0], int(visit_parts[1]) - - -def get_visit_string_from_path(path: str | None) -> str | None: - match = re.search(VISIT_PATH_REGEX, path) if path else None - return str(match.group(1)) if match else None diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py index dd2e79e3b..e2cc8866a 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py @@ -6,7 +6,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import ( get_proposal_and_session_from_visit_string, - get_visit_string_from_path, ) from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import ( PlanReactiveCallback, @@ -37,10 +36,9 @@ def activity_gated_start(self, doc: RunStart): ISPYB_LOGGER.debug(f"ISPyB robot load callback received: {doc}") self.run_uid = doc.get("uid") assert isinstance(metadata := doc.get("metadata"), dict) - assert isinstance( - visit := get_visit_string_from_path(metadata["visit_path"]), str + proposal, session = get_proposal_and_session_from_visit_string( + metadata["visit"] ) - proposal, session = get_proposal_and_session_from_visit_string(visit) self.action_id = self.expeye.start_load( proposal, session, diff --git a/src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py b/src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py index f7dd09af7..b523ffcda 100644 --- a/src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py +++ b/src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py @@ -9,8 +9,6 @@ from mx_bluesky.hyperion.parameters.constants import CONST -VISIT_PATH_REGEX = r".+/([a-zA-Z]{2}\d{4,5}-\d{1,3})(/?$)" - def get_ispyb_config(): return os.environ.get("ISPYB_CONFIG_PATH", CONST.SIM.ISPYB_CONFIG) diff --git a/src/mx_bluesky/hyperion/parameters/components.py b/src/mx_bluesky/hyperion/parameters/components.py index b8cdaed43..2ecbe6d13 100644 --- a/src/mx_bluesky/hyperion/parameters/components.py +++ b/src/mx_bluesky/hyperion/parameters/components.py @@ -1,6 +1,5 @@ from __future__ import annotations -import datetime import json from abc import abstractmethod from collections.abc import Sequence @@ -142,10 +141,19 @@ def take_snapshots(self) -> bool: return bool(self.snapshot_omegas_deg) -class DiffractionExperiment(HyperionParameters, WithSnapshot): - """For all experiments which use beam""" +class WithOptionalEnergyChange(BaseModel): + demand_energy_ev: float | None = Field(default=None, gt=0) + +class WithVisit(BaseModel): visit: str = Field(min_length=1) + + +class DiffractionExperiment( + HyperionParameters, WithSnapshot, WithOptionalEnergyChange, WithVisit +): + """For all experiments which use beam""" + file_name: str exposure_time_s: float = Field(gt=0) comment: str = Field(default="") @@ -159,7 +167,6 @@ class DiffractionExperiment(HyperionParameters, WithSnapshot): zocalo_environment: str = Field(default=CONST.ZOCALO_ENV) trigger_mode: TriggerMode = Field(default=TriggerMode.FREE_RUN) detector_distance_mm: float | None = Field(default=None, gt=0) - demand_energy_ev: float | None = Field(default=None, gt=0) run_number: int | None = Field(default=None, ge=0) selected_aperture: ApertureValue | None = Field(default=None) transmission_frac: float = Field(default=0.1) @@ -177,12 +184,6 @@ def validate_snapshot_directory(cls, values): ) return values - @property - def visit_directory(self) -> Path: - return ( - Path(CONST.I03.BASE_DATA_DIR) / str(datetime.date.today().year) / self.visit - ) - @property def num_images(self) -> int: return 0 diff --git a/src/mx_bluesky/hyperion/parameters/gridscan.py b/src/mx_bluesky/hyperion/parameters/gridscan.py index 4edc73548..b9f675163 100644 --- a/src/mx_bluesky/hyperion/parameters/gridscan.py +++ b/src/mx_bluesky/hyperion/parameters/gridscan.py @@ -20,10 +20,12 @@ OptionalGonioAngleStarts, SplitScan, WithOavCentring, + WithOptionalEnergyChange, WithScan, XyzStarts, ) from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants +from mx_bluesky.hyperion.parameters.robot_load import RobotLoadAndEnergyChange class GridCommon( @@ -85,6 +87,10 @@ class PinTipCentreThenXrayCentre(GridCommon): class RobotLoadThenCentre(GridCommon): thawing_time: float = Field(default=CONST.I03.THAWING_TIME) + def robot_load_params(self): + my_params = self.model_dump() + return RobotLoadAndEnergyChange(**my_params) + def pin_centre_then_xray_centre_params(self): my_params = self.model_dump() del my_params["thawing_time"] @@ -99,11 +105,10 @@ class SpecifiedGridScan(GridCommon, XyzStarts, WithScan): ... -class ThreeDGridScan(SpecifiedGridScan, SplitScan): +class ThreeDGridScan(SpecifiedGridScan, SplitScan, WithOptionalEnergyChange): """Parameters representing a so-called 3D grid scan, which consists of doing a gridscan in X and Y, followed by one in X and Z.""" - demand_energy_ev: float | None = Field(default=None) grid1_omega_deg: float = Field(default=CONST.PARAM.GRIDSCAN.OMEGA_1) # type: ignore grid2_omega_deg: float = Field(default=CONST.PARAM.GRIDSCAN.OMEGA_2) x_step_size_um: float = Field(default=CONST.PARAM.GRIDSCAN.BOX_WIDTH_UM) diff --git a/src/mx_bluesky/hyperion/parameters/robot_load.py b/src/mx_bluesky/hyperion/parameters/robot_load.py new file mode 100644 index 000000000..fbdffd7d7 --- /dev/null +++ b/src/mx_bluesky/hyperion/parameters/robot_load.py @@ -0,0 +1,16 @@ +from pydantic import Field + +from mx_bluesky.hyperion.parameters.components import ( + HyperionParameters, + WithOptionalEnergyChange, + WithSample, + WithSnapshot, + WithVisit, +) +from mx_bluesky.hyperion.parameters.constants import CONST + + +class RobotLoadAndEnergyChange( + HyperionParameters, WithSample, WithSnapshot, WithOptionalEnergyChange, WithVisit +): + thawing_time: float = Field(default=CONST.I03.THAWING_TIME) diff --git a/tests/test_data/parameter_json_files/good_test_robot_load_and_centre_params.json b/tests/test_data/parameter_json_files/good_test_robot_load_and_centre_params.json new file mode 100644 index 000000000..c5e63125a --- /dev/null +++ b/tests/test_data/parameter_json_files/good_test_robot_load_and_centre_params.json @@ -0,0 +1,21 @@ +{ + "parameter_model_version": "5.0.0", + "zocalo_environment": "dev_artemis", + "beamline": "BL03S", + "insertion_prefix": "SR03S", + "storage_directory": "/tmp/", + "visit": "cm31105-4", + "file_name": "file_name", + "run_number": 0, + "use_roi_mode": false, + "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", + "omega_start_deg": 0, + "transmission_frac": 1.0, + "exposure_time_s": 0.004, + "detector_distance_mm": 255, + "demand_energy_ev": 11100, + "sample_id": 12345, + "sample_puck": 40, + "sample_pin": 3, + "comment": "Descriptive comment." +} diff --git a/tests/test_data/parameter_json_files/good_test_robot_load_params.json b/tests/test_data/parameter_json_files/good_test_robot_load_params.json index c5e63125a..d57b2890a 100644 --- a/tests/test_data/parameter_json_files/good_test_robot_load_params.json +++ b/tests/test_data/parameter_json_files/good_test_robot_load_params.json @@ -3,19 +3,10 @@ "zocalo_environment": "dev_artemis", "beamline": "BL03S", "insertion_prefix": "SR03S", - "storage_directory": "/tmp/", + "snapshot_directory": "/tmp/", "visit": "cm31105-4", - "file_name": "file_name", - "run_number": 0, - "use_roi_mode": false, - "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", - "omega_start_deg": 0, - "transmission_frac": 1.0, - "exposure_time_s": 0.004, - "detector_distance_mm": 255, "demand_energy_ev": 11100, "sample_id": 12345, "sample_puck": 40, - "sample_pin": 3, - "comment": "Descriptive comment." + "sample_pin": 3 } diff --git a/tests/unit_tests/hyperion/device_setup_plans/test_setup_oav.py b/tests/unit_tests/hyperion/device_setup_plans/test_setup_oav.py index 61c866d0e..9e6aa1ffa 100644 --- a/tests/unit_tests/hyperion/device_setup_plans/test_setup_oav.py +++ b/tests/unit_tests/hyperion/device_setup_plans/test_setup_oav.py @@ -21,27 +21,6 @@ def mock_parameters(): return OAVParameters("loopCentring", OAV_CENTRING_JSON) -@pytest.mark.parametrize( - "zoom, expected_plugin", - [ - ("1.0", "proc"), - ("7.0", "CAM"), - ], -) -def test_when_set_up_oav_with_different_zoom_levels_then_flat_field_applied_correctly( - zoom, - expected_plugin, - mock_parameters: OAVParameters, - oav: OAV, - ophyd_pin_tip_detection: PinTipDetection, -): - mock_parameters.zoom = zoom - - RE = RunEngine() - RE(pre_centring_setup_oav(oav, mock_parameters, ophyd_pin_tip_detection)) - assert oav.grid_snapshot.input_plugin.get() == expected_plugin - - def test_when_set_up_oav_then_only_waits_on_oav_to_finish( mock_parameters: OAVParameters, oav: OAV, ophyd_pin_tip_detection: PinTipDetection ): diff --git a/tests/unit_tests/hyperion/experiment_plans/test_robot_load_and_change_energy.py b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_and_change_energy.py new file mode 100644 index 000000000..a43722b1b --- /dev/null +++ b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_and_change_energy.py @@ -0,0 +1,397 @@ +from functools import partial +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from bluesky.run_engine import RunEngine +from bluesky.simulators import RunEngineSimulator, assert_message_and_return_remaining +from bluesky.utils import Msg +from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue +from dodal.devices.oav.oav_detector import OAV +from dodal.devices.smargon import Smargon, StubPosition +from dodal.devices.webcam import Webcam +from ophyd.sim import NullStatus +from ophyd_async.core import set_mock_value + +from mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy import ( + RobotLoadAndEnergyChangeComposite, + prepare_for_robot_load, + robot_load_and_change_energy_plan, + take_robot_snapshots, +) +from mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback import ( + RobotLoadISPyBCallback, +) +from mx_bluesky.hyperion.parameters.robot_load import RobotLoadAndEnergyChange + +from ....conftest import raw_params_from_file + + +@pytest.fixture +def robot_load_composite( + smargon, dcm, robot, aperture_scatterguard, oav, webcam, thawer, lower_gonio, eiger +) -> RobotLoadAndEnergyChangeComposite: + composite: RobotLoadAndEnergyChangeComposite = MagicMock() + composite.smargon = smargon + composite.dcm = dcm + set_mock_value(composite.dcm.energy_in_kev.user_readback, 11.105) + composite.robot = robot + composite.aperture_scatterguard = aperture_scatterguard + composite.smargon.stub_offsets.set = MagicMock(return_value=NullStatus()) + composite.aperture_scatterguard.set = MagicMock(return_value=NullStatus()) + composite.oav = oav + composite.webcam = webcam + composite.lower_gonio = lower_gonio + composite.thawer = thawer + composite.eiger = eiger + return composite + + +@pytest.fixture +def robot_load_and_energy_change_params(): + params = raw_params_from_file( + "tests/test_data/parameter_json_files/good_test_robot_load_params.json" + ) + return RobotLoadAndEnergyChange(**params) + + +@pytest.fixture +def robot_load_and_energy_change_params_no_energy(robot_load_and_energy_change_params): + robot_load_and_energy_change_params.demand_energy_ev = None + return robot_load_and_energy_change_params + + +def dummy_set_energy_plan(energy, composite): + return (yield Msg("set_energy_plan")) + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.set_energy_plan", + MagicMock(side_effect=dummy_set_energy_plan), +) +def test_when_plan_run_with_requested_energy_specified_energy_change_executes( + robot_load_composite: RobotLoadAndEnergyChangeComposite, + robot_load_and_energy_change_params: RobotLoadAndEnergyChange, + sim_run_engine: RunEngineSimulator, +): + sim_run_engine.add_handler( + "read", + lambda msg: {"dcm-energy_in_kev": {"value": 11.105}}, + "dcm-energy_in_kev", + ) + messages = sim_run_engine.simulate_plan( + robot_load_and_change_energy_plan( + robot_load_composite, robot_load_and_energy_change_params + ) + ) + assert_message_and_return_remaining( + messages, lambda msg: msg.command == "set_energy_plan" + ) + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.set_energy_plan", + MagicMock(return_value=iter([Msg("set_energy_plan")])), +) +def test_robot_load_and_energy_change_doesnt_set_energy_if_not_specified( + robot_load_composite: RobotLoadAndEnergyChangeComposite, + robot_load_and_energy_change_params_no_energy: RobotLoadAndEnergyChange, + sim_run_engine: RunEngineSimulator, +): + sim_run_engine.add_handler( + "locate", + lambda msg: {"readback": 11.105}, + "dcm-energy_in_kev", + ) + messages = sim_run_engine.simulate_plan( + robot_load_and_change_energy_plan( + robot_load_composite, + robot_load_and_energy_change_params_no_energy, + ) + ) + assert not any(msg for msg in messages if msg.command == "set_energy_plan") + + +def run_simulating_smargon_wait( + robot_load_then_centre_params, + robot_load_composite, + total_disabled_reads, + sim_run_engine: RunEngineSimulator, +): + num_of_reads = 0 + + def return_not_disabled_after_reads(_): + nonlocal num_of_reads + num_of_reads += 1 + return {"values": {"value": int(num_of_reads < total_disabled_reads)}} + + sim_run_engine.add_handler( + "locate", + lambda msg: {"readback": 11.105}, + "dcm-energy_in_kev", + ) + sim_run_engine.add_handler( + "read", return_not_disabled_after_reads, "smargon-disabled" + ) + + return sim_run_engine.simulate_plan( + robot_load_and_change_energy_plan( + robot_load_composite, robot_load_then_centre_params + ) + ) + + +@pytest.mark.parametrize("total_disabled_reads", [5, 3, 14]) +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.set_energy_plan", + MagicMock(return_value=iter([])), +) +def test_given_smargon_disabled_when_plan_run_then_waits_on_smargon( + robot_load_composite: RobotLoadAndEnergyChangeComposite, + robot_load_and_energy_change_params: RobotLoadAndEnergyChange, + total_disabled_reads: int, + sim_run_engine, +): + messages = run_simulating_smargon_wait( + robot_load_and_energy_change_params, + robot_load_composite, + total_disabled_reads, + sim_run_engine, + ) + + sleep_messages = filter(lambda msg: msg.command == "sleep", messages) + read_disabled_messages = filter( + lambda msg: msg.command == "read" and msg.obj.name == "smargon-disabled", + messages, + ) + + assert len(list(sleep_messages)) == total_disabled_reads - 1 + assert len(list(read_disabled_messages)) == total_disabled_reads + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.set_energy_plan", + MagicMock(return_value=iter([])), +) +def test_given_smargon_disabled_for_longer_than_timeout_when_plan_run_then_throws_exception( + robot_load_composite: RobotLoadAndEnergyChangeComposite, + robot_load_and_energy_change_params: RobotLoadAndEnergyChange, + sim_run_engine, +): + with pytest.raises(TimeoutError): + run_simulating_smargon_wait( + robot_load_and_energy_change_params, + robot_load_composite, + 1000, + sim_run_engine, + ) + + +async def test_when_prepare_for_robot_load_called_then_moves_as_expected( + aperture_scatterguard: ApertureScatterguard, smargon: Smargon, done_status +): + smargon.stub_offsets.set = MagicMock(return_value=done_status) + aperture_scatterguard.set = MagicMock(return_value=done_status) + + set_mock_value(smargon.x.user_readback, 10) + set_mock_value(smargon.z.user_readback, 5) + set_mock_value(smargon.omega.user_readback, 90) + + RE = RunEngine() + RE(prepare_for_robot_load(aperture_scatterguard, smargon)) + + assert await smargon.x.user_readback.get_value() == 0 + assert await smargon.z.user_readback.get_value() == 0 + assert await smargon.omega.user_readback.get_value() == 0 + + smargon.stub_offsets.set.assert_called_once_with(StubPosition.RESET_TO_ROBOT_LOAD) # type: ignore + aperture_scatterguard.set.assert_called_once_with(ApertureValue.ROBOT_LOAD) # type: ignore + + +@patch( + "mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback.ExpeyeInteraction.end_load" +) +@patch( + "mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback.ExpeyeInteraction.update_barcode_and_snapshots" +) +@patch( + "mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback.ExpeyeInteraction.start_load" +) +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.set_energy_plan", + MagicMock(return_value=iter([])), +) +def test_given_ispyb_callback_attached_when_robot_load_then_centre_plan_called_then_ispyb_deposited( + start_load: MagicMock, + update_barcode_and_snapshots: MagicMock, + end_load: MagicMock, + robot_load_composite: RobotLoadAndEnergyChangeComposite, + robot_load_and_energy_change_params: RobotLoadAndEnergyChange, +): + robot_load_composite.oav.snapshot.last_saved_path.put("test_oav_snapshot") # type: ignore + set_mock_value(robot_load_composite.webcam.last_saved_path, "test_webcam_snapshot") + robot_load_composite.webcam.trigger = MagicMock(return_value=NullStatus()) + + RE = RunEngine() + RE.subscribe(RobotLoadISPyBCallback()) + + action_id = 1098 + start_load.return_value = action_id + + RE( + robot_load_and_change_energy_plan( + robot_load_composite, robot_load_and_energy_change_params + ) + ) + + start_load.assert_called_once_with("cm31105", 4, 12345, 40, 3) + update_barcode_and_snapshots.assert_called_once_with( + action_id, "BARCODE", "test_webcam_snapshot", "test_oav_snapshot" + ) + end_load.assert_called_once_with(action_id, "success", "OK") + + +@patch("mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.datetime") +async def test_when_take_snapshots_called_then_filename_and_directory_set_and_device_triggered( + mock_datetime: MagicMock, oav: OAV, webcam: Webcam +): + TEST_DIRECTORY = "TEST" + + mock_datetime.now.return_value.strftime.return_value = "TIME" + + RE = RunEngine() + oav.snapshot.trigger = MagicMock(side_effect=oav.snapshot.trigger) + webcam.trigger = MagicMock(return_value=NullStatus()) + + RE(take_robot_snapshots(oav, webcam, Path(TEST_DIRECTORY))) + + oav.snapshot.trigger.assert_called_once() + assert oav.snapshot.filename.get() == "TIME_oav_snapshot_after_load" + assert oav.snapshot.directory.get() == TEST_DIRECTORY + + webcam.trigger.assert_called_once() + assert (await webcam.filename.get_value()) == "TIME_webcam_after_load" + assert (await webcam.directory.get_value()) == TEST_DIRECTORY + + +def test_given_lower_gonio_moved_when_robot_load_then_lower_gonio_moved_to_home_and_back( + robot_load_composite: RobotLoadAndEnergyChangeComposite, + robot_load_and_energy_change_params_no_energy: RobotLoadAndEnergyChange, + sim_run_engine: RunEngineSimulator, +): + initial_values = {"x": 0.11, "y": 0.12, "z": 0.13} + + def get_read(axis, msg): + return {"readback": initial_values[axis]} + + for axis in initial_values.keys(): + sim_run_engine.add_handler( + "locate", partial(get_read, axis), f"lower_gonio-{axis}" + ) + + messages = sim_run_engine.simulate_plan( + robot_load_and_change_energy_plan( + robot_load_composite, + robot_load_and_energy_change_params_no_energy, + ) + ) + + for axis in initial_values.keys(): + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "set" + and msg.obj.name == f"lower_gonio-{axis}" + and msg.args == (0,), + ) + + for axis, initial in initial_values.items(): + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "set" + and msg.obj.name == f"lower_gonio-{axis}" + and msg.args == (initial,), + ) + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.set_energy_plan", + MagicMock(return_value=iter([])), +) +def test_when_plan_run_then_lower_gonio_moved_before_robot_loads_and_back_after_smargon_enabled( + robot_load_composite: RobotLoadAndEnergyChangeComposite, + robot_load_and_energy_change_params_no_energy: RobotLoadAndEnergyChange, + sim_run_engine: RunEngineSimulator, +): + initial_values = {"x": 0.11, "y": 0.12, "z": 0.13} + + def get_read(axis, msg): + return {"readback": initial_values[axis]} + + for axis in initial_values.keys(): + sim_run_engine.add_handler( + "locate", partial(get_read, axis), f"lower_gonio-{axis}" + ) + + messages = sim_run_engine.simulate_plan( + robot_load_and_change_energy_plan( + robot_load_composite, + robot_load_and_energy_change_params_no_energy, + ) + ) + + assert_message_and_return_remaining( + messages, lambda msg: msg.command == "set" and msg.obj.name == "robot" + ) + + for axis in initial_values.keys(): + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "set" + and msg.obj.name == f"lower_gonio-{axis}" + and msg.args == (0,), + ) + + assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "read" and msg.obj.name == "smargon-disabled", + ) + + for axis, initial in initial_values.items(): + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "set" + and msg.obj.name == f"lower_gonio-{axis}" # noqa + and msg.args == (initial,), # noqa + ) + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.set_energy_plan", + MagicMock(return_value=iter([])), +) +def test_when_plan_run_then_thawing_turned_on_for_expected_time( + robot_load_composite: RobotLoadAndEnergyChangeComposite, + robot_load_and_energy_change_params_no_energy: RobotLoadAndEnergyChange, + sim_run_engine: RunEngineSimulator, +): + robot_load_and_energy_change_params_no_energy.thawing_time = (thaw_time := 50) + + sim_run_engine.add_handler( + "read", + lambda msg: {"dcm-energy_in_kev": {"value": 11.105}}, + "dcm-energy_in_kev", + ) + + messages = sim_run_engine.simulate_plan( + robot_load_and_change_energy_plan( + robot_load_composite, + robot_load_and_energy_change_params_no_energy, + ) + ) + + assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "set" + and msg.obj.name == "thawer-thaw_for_time_s" + and msg.args[0] == thaw_time, + ) diff --git a/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py new file mode 100644 index 000000000..8d37e8a0c --- /dev/null +++ b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py @@ -0,0 +1,410 @@ +from unittest.mock import MagicMock, patch + +import pytest +from bluesky.run_engine import RunEngine +from bluesky.simulators import RunEngineSimulator, assert_message_and_return_remaining +from bluesky.utils import Msg +from dodal.devices.robot import SampleLocation +from ophyd.sim import NullStatus +from ophyd_async.core import set_mock_value + +from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import ( + GridDetectThenXRayCentreComposite, +) +from mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan import ( + RobotLoadThenCentreComposite, + robot_load_then_centre, +) +from mx_bluesky.hyperion.parameters.gridscan import ( + PinTipCentreThenXrayCentre, + RobotLoadThenCentre, +) + +from ....conftest import assert_none_matching, raw_params_from_file + + +@pytest.fixture +def robot_load_composite( + smargon, + dcm, + robot, + aperture_scatterguard, + oav, + webcam, + thawer, + lower_gonio, + eiger, + xbpm_feedback, + flux, + zocalo, + panda, + backlight, + attenuator, + pin_tip, + fast_grid_scan, + detector_motion, + synchrotron, + s4_slit_gaps, + undulator, + zebra, + panda_fast_grid_scan, + vfm, + vfm_mirror_voltages, + undulator_dcm, + sample_shutter, +) -> RobotLoadThenCentreComposite: + composite: RobotLoadThenCentreComposite = RobotLoadThenCentreComposite( + smargon=smargon, + dcm=dcm, + robot=robot, + aperture_scatterguard=aperture_scatterguard, + oav=oav, + webcam=webcam, + lower_gonio=lower_gonio, + thawer=thawer, + eiger=eiger, + xbpm_feedback=xbpm_feedback, + flux=flux, + zocalo=zocalo, + panda=panda, + backlight=backlight, + attenuator=attenuator, + pin_tip_detection=pin_tip, + zebra_fast_grid_scan=fast_grid_scan, + detector_motion=detector_motion, + synchrotron=synchrotron, + s4_slit_gaps=s4_slit_gaps, + undulator=undulator, + zebra=zebra, + panda_fast_grid_scan=panda_fast_grid_scan, + vfm=vfm, + vfm_mirror_voltages=vfm_mirror_voltages, + undulator_dcm=undulator_dcm, + sample_shutter=sample_shutter, + ) + set_mock_value(composite.dcm.energy_in_kev.user_readback, 11.105) + composite.aperture_scatterguard = aperture_scatterguard + composite.smargon.stub_offsets.set = MagicMock(return_value=NullStatus()) + composite.aperture_scatterguard.set = MagicMock(return_value=NullStatus()) + return composite + + +@pytest.fixture +def robot_load_then_centre_params(): + params = raw_params_from_file( + "tests/test_data/parameter_json_files/good_test_robot_load_and_centre_params.json" + ) + return RobotLoadThenCentre(**params) + + +@pytest.fixture +def robot_load_then_centre_params_no_energy(robot_load_then_centre_params): + robot_load_then_centre_params.demand_energy_ev = None + return robot_load_then_centre_params + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" +) +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.robot_load_and_change_energy_plan", + MagicMock(return_value=iter([])), +) +def test_when_plan_run_then_centring_plan_run_with_expected_parameters( + mock_centring_plan: MagicMock, + robot_load_composite: RobotLoadThenCentreComposite, + robot_load_then_centre_params: RobotLoadThenCentre, +): + RE = RunEngine() + + RE(robot_load_then_centre(robot_load_composite, robot_load_then_centre_params)) + composite_passed = mock_centring_plan.call_args[0][0] + params_passed: PinTipCentreThenXrayCentre = mock_centring_plan.call_args[0][1] + + for name, value in vars(composite_passed).items(): + assert value == getattr(robot_load_composite, name) + + for name in GridDetectThenXRayCentreComposite.__dataclass_fields__.keys(): + assert getattr(composite_passed, name), f"{name} not in composite" + + assert isinstance(params_passed, PinTipCentreThenXrayCentre) + assert params_passed.detector_params.expected_energy_ev == 11100 + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" +) +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.robot_load_and_change_energy_plan", + MagicMock(return_value=iter([])), +) +def test_when_plan_run_with_requested_energy_specified_energy_set_on_eiger( + mock_centring_plan: MagicMock, + robot_load_composite: RobotLoadThenCentreComposite, + robot_load_then_centre_params: RobotLoadThenCentre, + sim_run_engine: RunEngineSimulator, +): + robot_load_composite.eiger.set_detector_parameters = MagicMock() + sim_run_engine.simulate_plan( + robot_load_then_centre(robot_load_composite, robot_load_then_centre_params) + ) + det_params = robot_load_composite.eiger.set_detector_parameters.call_args[0][0] + assert det_params.expected_energy_ev == 11100 + params_passed: PinTipCentreThenXrayCentre = mock_centring_plan.call_args[0][1] + assert params_passed.detector_params.expected_energy_ev == 11100 + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", + MagicMock(), +) +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.robot_load_and_change_energy_plan", + MagicMock(return_value=iter([])), +) +def test_given_no_energy_supplied_when_robot_load_then_centre_current_energy_set_on_eiger( + robot_load_composite: RobotLoadThenCentreComposite, + robot_load_then_centre_params_no_energy: RobotLoadThenCentre, + sim_run_engine: RunEngineSimulator, +): + robot_load_composite.eiger.set_detector_parameters = MagicMock() + sim_run_engine.add_handler( + "locate", + lambda msg: {"readback": 11.105}, + "dcm-energy_in_kev", + ) + sim_run_engine.simulate_plan( + robot_load_then_centre( + robot_load_composite, + robot_load_then_centre_params_no_energy, + ) + ) + det_params = robot_load_composite.eiger.set_detector_parameters.call_args[0][0] + assert det_params.expected_energy_ev == 11105 + + +def run_simulating_smargon_wait( + robot_load_then_centre_params, + robot_load_composite, + total_disabled_reads, + sim_run_engine: RunEngineSimulator, +): + num_of_reads = 0 + + def return_not_disabled_after_reads(_): + nonlocal num_of_reads + num_of_reads += 1 + return {"values": {"value": int(num_of_reads < total_disabled_reads)}} + + sim_run_engine.add_handler( + "locate", + lambda msg: {"readback": 11.105}, + "dcm-energy_in_kev", + ) + sim_run_engine.add_handler( + "read", return_not_disabled_after_reads, "smargon-disabled" + ) + + return sim_run_engine.simulate_plan( + robot_load_then_centre(robot_load_composite, robot_load_then_centre_params) + ) + + +def dummy_robot_load_plan(*args, **kwargs): + return (yield Msg("robot_load")) + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", + MagicMock(return_value=iter([])), +) +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.robot_load_and_change_energy_plan", + MagicMock(side_effect=dummy_robot_load_plan), +) +def test_when_plan_run_then_detector_arm_started_before_wait_on_robot_load( + robot_load_composite: RobotLoadThenCentreComposite, + robot_load_then_centre_params: RobotLoadThenCentre, + sim_run_engine, +): + messages = sim_run_engine.simulate_plan( + robot_load_then_centre(robot_load_composite, robot_load_then_centre_params) + ) + messages = assert_message_and_return_remaining( + messages, lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm" + ) + + assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "robot_load", + ) + + +def mock_current_sample(sim_run_engine: RunEngineSimulator, sample: SampleLocation): + sim_run_engine.add_handler( + "read", + lambda msg: {"robot-current_puck": {"value": sample.puck}}, + "robot-current_puck", + ) + sim_run_engine.add_handler( + "read", + lambda msg: {"robot-current_pin": {"value": sample.pin}}, + "robot-current_pin", + ) + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", + MagicMock(return_value=iter([Msg("centre_plan")])), +) +def test_given_sample_already_loaded_and_chi_not_changed_when_robot_load_called_then_eiger_not_staged_and_centring_not_run( + robot_load_composite: RobotLoadThenCentreComposite, + robot_load_then_centre_params: RobotLoadThenCentre, + sim_run_engine: RunEngineSimulator, +): + sample_location = SampleLocation(2, 6) + robot_load_then_centre_params.sample_puck = sample_location.puck + robot_load_then_centre_params.sample_pin = sample_location.pin + robot_load_then_centre_params.chi_start_deg = None + + mock_current_sample(sim_run_engine, sample_location) + + messages = sim_run_engine.simulate_plan( + robot_load_then_centre( + robot_load_composite, + robot_load_then_centre_params, + ) + ) + + assert_none_matching( + messages, lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm" + ) + + assert_none_matching( + messages, lambda msg: msg.command == "set" and msg.obj.name == "robot" + ) + + assert_none_matching( + messages, + lambda msg: msg.command == "centre_plan", + ) + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", + MagicMock(return_value=iter([Msg("centre_plan")])), +) +def test_given_sample_already_loaded_and_chi_is_changed_when_robot_load_called_then_eiger_staged_and_centring_run( + robot_load_composite: RobotLoadThenCentreComposite, + robot_load_then_centre_params: RobotLoadThenCentre, + sim_run_engine: RunEngineSimulator, +): + sample_location = SampleLocation(2, 6) + robot_load_then_centre_params.sample_puck = sample_location.puck + robot_load_then_centre_params.sample_pin = sample_location.pin + robot_load_then_centre_params.chi_start_deg = 30 + + mock_current_sample(sim_run_engine, sample_location) + + messages = sim_run_engine.simulate_plan( + robot_load_then_centre( + robot_load_composite, + robot_load_then_centre_params, + ) + ) + + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm", + ) + + assert_none_matching( + messages, lambda msg: msg.command == "set" and msg.obj.name == "robot" + ) + + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "centre_plan", + ) + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", + MagicMock(return_value=iter([Msg("centre_plan")])), +) +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.robot_load_and_change_energy_plan", + MagicMock(return_value=iter([Msg("full_robot_load_plan")])), +) +def test_given_sample_not_loaded_and_chi_not_changed_when_robot_load_called_then_eiger_staged_before_robot_and_centring_run_after( + robot_load_composite: RobotLoadThenCentreComposite, + robot_load_then_centre_params: RobotLoadThenCentre, + sim_run_engine: RunEngineSimulator, +): + robot_load_then_centre_params.sample_puck = 2 + robot_load_then_centre_params.sample_pin = 6 + robot_load_then_centre_params.chi_start_deg = None + + mock_current_sample(sim_run_engine, SampleLocation(1, 1)) + + messages = sim_run_engine.simulate_plan( + robot_load_then_centre( + robot_load_composite, + robot_load_then_centre_params, + ) + ) + + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm", + ) + + messages = assert_message_and_return_remaining( + messages, lambda msg: msg.command == "full_robot_load_plan" + ) + + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "centre_plan", + ) + + +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", + MagicMock(return_value=iter([Msg("centre_plan")])), +) +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.robot_load_and_change_energy_plan", + MagicMock(return_value=iter([Msg("full_robot_load_plan")])), +) +def test_given_sample_not_loaded_and_chi_changed_when_robot_load_called_then_eiger_staged_before_robot_and_centring_run( + robot_load_composite: RobotLoadThenCentreComposite, + robot_load_then_centre_params: RobotLoadThenCentre, + sim_run_engine: RunEngineSimulator, +): + robot_load_then_centre_params.sample_puck = 2 + robot_load_then_centre_params.sample_pin = 6 + robot_load_then_centre_params.chi_start_deg = 30 + + mock_current_sample(sim_run_engine, SampleLocation(1, 1)) + + messages = sim_run_engine.simulate_plan( + robot_load_then_centre( + robot_load_composite, + robot_load_then_centre_params, + ) + ) + + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm", + ) + + messages = assert_message_and_return_remaining( + messages, lambda msg: msg.command == "full_robot_load_plan" + ) + + messages = assert_message_and_return_remaining( + messages, + lambda msg: msg.command == "centre_plan", + ) diff --git a/tests/unit_tests/hyperion/experiment_plans/test_wait_for_robot_load_then_centre.py b/tests/unit_tests/hyperion/experiment_plans/test_wait_for_robot_load_then_centre.py deleted file mode 100644 index 93b7668cf..000000000 --- a/tests/unit_tests/hyperion/experiment_plans/test_wait_for_robot_load_then_centre.py +++ /dev/null @@ -1,730 +0,0 @@ -from functools import partial -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest -from bluesky.run_engine import RunEngine -from bluesky.simulators import RunEngineSimulator, assert_message_and_return_remaining -from bluesky.utils import Msg -from dodal.devices.aperturescatterguard import ApertureValue -from dodal.devices.oav.oav_detector import OAV -from dodal.devices.robot import SampleLocation -from dodal.devices.smargon import StubPosition -from dodal.devices.webcam import Webcam -from ophyd.sim import NullStatus -from ophyd_async.core import set_mock_value - -from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import ( - GridDetectThenXRayCentreComposite, -) -from mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan import ( - RobotLoadThenCentreComposite, - prepare_for_robot_load, - robot_load_then_centre, - take_robot_snapshots, -) -from mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback import ( - RobotLoadISPyBCallback, -) -from mx_bluesky.hyperion.parameters.gridscan import ( - PinTipCentreThenXrayCentre, - RobotLoadThenCentre, -) - -from ....conftest import assert_none_matching, raw_params_from_file - - -@pytest.fixture -def robot_load_composite( - smargon, - dcm, - robot, - aperture_scatterguard, - oav, - webcam, - thawer, - lower_gonio, - eiger, - xbpm_feedback, - flux, - zocalo, - panda, - backlight, - attenuator, - pin_tip, - fast_grid_scan, - detector_motion, - synchrotron, - s4_slit_gaps, - undulator, - zebra, - panda_fast_grid_scan, - vfm, - vfm_mirror_voltages, - undulator_dcm, - sample_shutter, -) -> RobotLoadThenCentreComposite: - composite: RobotLoadThenCentreComposite = RobotLoadThenCentreComposite( - smargon=smargon, - dcm=dcm, - robot=robot, - aperture_scatterguard=aperture_scatterguard, - oav=oav, - webcam=webcam, - lower_gonio=lower_gonio, - thawer=thawer, - eiger=eiger, - xbpm_feedback=xbpm_feedback, - flux=flux, - zocalo=zocalo, - panda=panda, - backlight=backlight, - attenuator=attenuator, - pin_tip_detection=pin_tip, - zebra_fast_grid_scan=fast_grid_scan, - detector_motion=detector_motion, - synchrotron=synchrotron, - s4_slit_gaps=s4_slit_gaps, - undulator=undulator, - zebra=zebra, - panda_fast_grid_scan=panda_fast_grid_scan, - vfm=vfm, - vfm_mirror_voltages=vfm_mirror_voltages, - undulator_dcm=undulator_dcm, - sample_shutter=sample_shutter, - ) - set_mock_value(composite.dcm.energy_in_kev.user_readback, 11.105) - composite.aperture_scatterguard = aperture_scatterguard - composite.smargon.stub_offsets.set = MagicMock(return_value=NullStatus()) - composite.aperture_scatterguard.set = MagicMock(return_value=NullStatus()) - return composite - - -@pytest.fixture -def robot_load_then_centre_params(): - params = raw_params_from_file( - "tests/test_data/parameter_json_files/good_test_robot_load_params.json" - ) - return RobotLoadThenCentre(**params) - - -@pytest.fixture -def robot_load_then_centre_params_no_energy(robot_load_then_centre_params): - robot_load_then_centre_params.demand_energy_ev = None - return robot_load_then_centre_params - - -def dummy_set_energy_plan(energy, composite): - return (yield Msg("set_energy_plan")) - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_when_plan_run_then_centring_plan_run_with_expected_parameters( - mock_centring_plan: MagicMock, - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, -): - RE = RunEngine() - - RE(robot_load_then_centre(robot_load_composite, robot_load_then_centre_params)) - composite_passed = mock_centring_plan.call_args[0][0] - params_passed: PinTipCentreThenXrayCentre = mock_centring_plan.call_args[0][1] - - for name, value in vars(composite_passed).items(): - assert value == getattr(robot_load_composite, name) - - for name in GridDetectThenXRayCentreComposite.__dataclass_fields__.keys(): - assert getattr(composite_passed, name), f"{name} not in composite" - - assert isinstance(params_passed, PinTipCentreThenXrayCentre) - assert params_passed.detector_params.expected_energy_ev == 11100 - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(side_effect=dummy_set_energy_plan), -) -def test_when_plan_run_with_requested_energy_specified_energy_change_executes( - mock_centring_plan: MagicMock, - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, - sim_run_engine: RunEngineSimulator, -): - sim_run_engine.add_handler( - "read", - lambda msg: {"dcm-energy_in_kev": {"value": 11.105}}, - "dcm-energy_in_kev", - ) - messages = sim_run_engine.simulate_plan( - robot_load_then_centre(robot_load_composite, robot_load_then_centre_params) - ) - assert_message_and_return_remaining( - messages, lambda msg: msg.command == "set_energy_plan" - ) - params_passed: PinTipCentreThenXrayCentre = mock_centring_plan.call_args[0][1] - assert params_passed.detector_params.expected_energy_ev == 11100 - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", - MagicMock(), -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([Msg("set_energy_plan")])), -) -def test_robot_load_then_centre_doesnt_set_energy_if_not_specified_and_current_energy_set_on_eiger( - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params_no_energy: RobotLoadThenCentre, - sim_run_engine: RunEngineSimulator, -): - robot_load_composite.eiger.set_detector_parameters = MagicMock() - sim_run_engine.add_handler( - "locate", - lambda msg: {"readback": 11.105}, - "dcm-energy_in_kev", - ) - messages = sim_run_engine.simulate_plan( - robot_load_then_centre( - robot_load_composite, - robot_load_then_centre_params_no_energy, - ) - ) - assert not any(msg for msg in messages if msg.command == "set_energy_plan") - det_params = robot_load_composite.eiger.set_detector_parameters.call_args[0][0] - assert det_params.expected_energy_ev == 11105 - - -def run_simulating_smargon_wait( - robot_load_then_centre_params, - robot_load_composite, - total_disabled_reads, - sim_run_engine: RunEngineSimulator, -): - num_of_reads = 0 - - def return_not_disabled_after_reads(_): - nonlocal num_of_reads - num_of_reads += 1 - return {"values": {"value": int(num_of_reads < total_disabled_reads)}} - - sim_run_engine.add_handler( - "locate", - lambda msg: {"readback": 11.105}, - "dcm-energy_in_kev", - ) - sim_run_engine.add_handler( - "read", return_not_disabled_after_reads, "smargon-disabled" - ) - - return sim_run_engine.simulate_plan( - robot_load_then_centre(robot_load_composite, robot_load_then_centre_params) - ) - - -@pytest.mark.parametrize("total_disabled_reads", [5, 3, 14]) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_given_smargon_disabled_when_plan_run_then_waits_on_smargon( - mock_centring_plan: MagicMock, - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, - total_disabled_reads: int, - sim_run_engine, -): - messages = run_simulating_smargon_wait( - robot_load_then_centre_params, - robot_load_composite, - total_disabled_reads, - sim_run_engine, - ) - - mock_centring_plan.assert_called_once() - - sleep_messages = filter(lambda msg: msg.command == "sleep", messages) - read_disabled_messages = filter( - lambda msg: msg.command == "read" and msg.obj.name == "smargon-disabled", - messages, - ) - - assert len(list(sleep_messages)) == total_disabled_reads - 1 - assert len(list(read_disabled_messages)) == total_disabled_reads - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_given_smargon_disabled_for_longer_than_timeout_when_plan_run_then_throws_exception( - mock_centring_plan: MagicMock, - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, - sim_run_engine, -): - with pytest.raises(TimeoutError): - run_simulating_smargon_wait( - robot_load_then_centre_params, - robot_load_composite, - 1000, - sim_run_engine, - ) - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_when_plan_run_then_detector_arm_started_before_wait_on_robot_load( - mock_centring_plan: MagicMock, - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, - sim_run_engine, -): - messages = run_simulating_smargon_wait( - robot_load_then_centre_params, - robot_load_composite, - 1, - sim_run_engine, - ) - - arm_detector_messages = filter( - lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm", - messages, - ) - read_disabled_messages = filter( - lambda msg: msg.command == "read" and msg.obj.name == "smargon-disabled", - messages, - ) - - arm_detector_messages = list(arm_detector_messages) - assert len(arm_detector_messages) == 1 - - idx_of_arm_message = messages.index(arm_detector_messages[0]) - idx_of_first_read_disabled_message = messages.index(list(read_disabled_messages)[0]) - - assert idx_of_arm_message < idx_of_first_read_disabled_message - - -async def test_when_prepare_for_robot_load_called_then_moves_as_expected( - robot_load_composite: RobotLoadThenCentreComposite, -): - smargon = robot_load_composite.smargon - aperture_scatterguard = robot_load_composite.aperture_scatterguard - set_mock_value(smargon.x.user_readback, 10) - set_mock_value(smargon.z.user_readback, 5) - set_mock_value(smargon.omega.user_readback, 90) - - RE = RunEngine() - RE(prepare_for_robot_load(robot_load_composite)) - - assert await smargon.x.user_readback.get_value() == 0 - assert await smargon.z.user_readback.get_value() == 0 - assert await smargon.omega.user_readback.get_value() == 0 - - smargon.stub_offsets.set.assert_called_once_with(StubPosition.RESET_TO_ROBOT_LOAD) # type: ignore - aperture_scatterguard.set.assert_called_once_with(ApertureValue.ROBOT_LOAD) # type: ignore - - -@patch( - "mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback.ExpeyeInteraction.end_load" -) -@patch( - "mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback.ExpeyeInteraction.update_barcode_and_snapshots" -) -@patch( - "mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback.ExpeyeInteraction.start_load" -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_given_ispyb_callback_attached_when_robot_load_then_centre_plan_called_then_ispyb_deposited( - mock_centring_plan: MagicMock, - start_load: MagicMock, - update_barcode_and_snapshots: MagicMock, - end_load: MagicMock, - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, -): - robot_load_composite.oav.snapshot.last_saved_path.put("test_oav_snapshot") # type: ignore - set_mock_value(robot_load_composite.webcam.last_saved_path, "test_webcam_snapshot") - robot_load_composite.webcam.trigger = MagicMock(return_value=NullStatus()) - - RE = RunEngine() - RE.subscribe(RobotLoadISPyBCallback()) - - action_id = 1098 - start_load.return_value = action_id - - RE(robot_load_then_centre(robot_load_composite, robot_load_then_centre_params)) - - start_load.assert_called_once_with("cm31105", 4, 12345, 40, 3) - update_barcode_and_snapshots.assert_called_once_with( - action_id, "BARCODE", "test_webcam_snapshot", "test_oav_snapshot" - ) - end_load.assert_called_once_with(action_id, "success", "OK") - - -@patch("mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.datetime") -async def test_when_take_snapshots_called_then_filename_and_directory_set_and_device_triggered( - mock_datetime: MagicMock, oav: OAV, webcam: Webcam -): - TEST_DIRECTORY = "TEST" - - mock_datetime.now.return_value.strftime.return_value = "TIME" - - RE = RunEngine() - oav.snapshot.trigger = MagicMock(side_effect=oav.snapshot.trigger) - webcam.trigger = MagicMock(return_value=NullStatus()) - - RE(take_robot_snapshots(oav, webcam, Path(TEST_DIRECTORY))) - - oav.snapshot.trigger.assert_called_once() - assert oav.snapshot.filename.get() == "TIME_oav_snapshot_after_load" - assert oav.snapshot.directory.get() == TEST_DIRECTORY - - webcam.trigger.assert_called_once() - assert (await webcam.filename.get_value()) == "TIME_webcam_after_load" - assert (await webcam.directory.get_value()) == TEST_DIRECTORY - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", - MagicMock(), -) -def test_given_lower_gonio_moved_when_robot_load_then_lower_gonio_moved_to_home_and_back( - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params_no_energy: RobotLoadThenCentre, - sim_run_engine: RunEngineSimulator, -): - initial_values = {"x": 0.11, "y": 0.12, "z": 0.13} - - def get_read(axis, msg): - return {"readback": initial_values[axis]} - - for axis in initial_values.keys(): - sim_run_engine.add_handler( - "locate", partial(get_read, axis), f"lower_gonio-{axis}" - ) - - messages = sim_run_engine.simulate_plan( - robot_load_then_centre( - robot_load_composite, - robot_load_then_centre_params_no_energy, - ) - ) - - for axis in initial_values.keys(): - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" - and msg.obj.name == f"lower_gonio-{axis}" - and msg.args == (0,), - ) - - for axis, initial in initial_values.items(): - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" - and msg.obj.name == f"lower_gonio-{axis}" - and msg.args == (initial,), - ) - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_when_plan_run_then_lower_gonio_moved_before_robot_loads_and_back_after_smargon_enabled( - mock_centring_plan: MagicMock, - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params_no_energy: RobotLoadThenCentre, - sim_run_engine: RunEngineSimulator, -): - initial_values = {"x": 0.11, "y": 0.12, "z": 0.13} - - def get_read(axis, msg): - return {"readback": initial_values[axis]} - - for axis in initial_values.keys(): - sim_run_engine.add_handler( - "locate", partial(get_read, axis), f"lower_gonio-{axis}" - ) - - messages = sim_run_engine.simulate_plan( - robot_load_then_centre( - robot_load_composite, - robot_load_then_centre_params_no_energy, - ) - ) - - assert_message_and_return_remaining( - messages, lambda msg: msg.command == "set" and msg.obj.name == "robot" - ) - - for axis in initial_values.keys(): - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" - and msg.obj.name == f"lower_gonio-{axis}" - and msg.args == (0,), - ) - - assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "read" and msg.obj.name == "smargon-disabled", - ) - - for axis, initial in initial_values.items(): - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" - and msg.obj.name == f"lower_gonio-{axis}" # noqa - and msg.args == (initial,), # noqa - ) - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan" -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_when_plan_run_then_thawing_turned_on_for_expected_time( - mock_centring_plan: MagicMock, - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params_no_energy: RobotLoadThenCentre, - sim_run_engine: RunEngineSimulator, -): - robot_load_then_centre_params_no_energy.thawing_time = (thaw_time := 50) - - sim_run_engine.add_handler( - "read", - lambda msg: {"dcm-energy_in_kev": {"value": 11.105}}, - "dcm-energy_in_kev", - ) - - messages = sim_run_engine.simulate_plan( - robot_load_then_centre( - robot_load_composite, - robot_load_then_centre_params_no_energy, - ) - ) - - assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" - and msg.obj.name == "thawer-thaw_for_time_s" - and msg.args[0] == thaw_time, - ) - - -def mock_current_sample(sim_run_engine: RunEngineSimulator, sample: SampleLocation): - sim_run_engine.add_handler( - "read", - lambda msg: {"robot-current_puck": {"value": sample.puck}}, - "robot-current_puck", - ) - sim_run_engine.add_handler( - "read", - lambda msg: {"robot-current_pin": {"value": sample.pin}}, - "robot-current_pin", - ) - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", - MagicMock(return_value=iter([Msg("centre_plan")])), -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_given_sample_already_loaded_and_chi_not_changed_when_robot_load_called_then_eiger_not_staged_and_centring_not_run( - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, - sim_run_engine: RunEngineSimulator, -): - sample_location = SampleLocation(2, 6) - robot_load_then_centre_params.sample_puck = sample_location.puck - robot_load_then_centre_params.sample_pin = sample_location.pin - robot_load_then_centre_params.chi_start_deg = None - - mock_current_sample(sim_run_engine, sample_location) - - messages = sim_run_engine.simulate_plan( - robot_load_then_centre( - robot_load_composite, - robot_load_then_centre_params, - ) - ) - - assert_none_matching( - messages, lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm" - ) - - assert_none_matching( - messages, lambda msg: msg.command == "set" and msg.obj.name == "robot" - ) - - assert_none_matching( - messages, - lambda msg: msg.command == "centre_plan", - ) - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", - MagicMock(return_value=iter([Msg("centre_plan")])), -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_given_sample_already_loaded_and_chi_is_changed_when_robot_load_called_then_eiger_staged_and_centring_run( - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, - sim_run_engine: RunEngineSimulator, -): - sample_location = SampleLocation(2, 6) - robot_load_then_centre_params.sample_puck = sample_location.puck - robot_load_then_centre_params.sample_pin = sample_location.pin - robot_load_then_centre_params.chi_start_deg = 30 - - mock_current_sample(sim_run_engine, sample_location) - - messages = sim_run_engine.simulate_plan( - robot_load_then_centre( - robot_load_composite, - robot_load_then_centre_params, - ) - ) - - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm", - ) - - assert_none_matching( - messages, lambda msg: msg.command == "set" and msg.obj.name == "robot" - ) - - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "centre_plan", - ) - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", - MagicMock(return_value=iter([Msg("centre_plan")])), -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_given_sample_not_loaded_and_chi_not_changed_when_robot_load_called_then_eiger_staged_before_robot_and_centring_run_after( - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, - sim_run_engine: RunEngineSimulator, -): - robot_load_then_centre_params.sample_puck = (puck := 2) - robot_load_then_centre_params.sample_pin = (pin := 6) - robot_load_then_centre_params.chi_start_deg = None - - mock_current_sample(sim_run_engine, SampleLocation(1, 1)) - - messages = sim_run_engine.simulate_plan( - robot_load_then_centre( - robot_load_composite, - robot_load_then_centre_params, - ) - ) - - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm", - ) - - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" - and msg.obj.name == "robot" - and msg.args[0] == SampleLocation(puck, pin), - ) - - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "centre_plan", - ) - - -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.pin_centre_then_xray_centre_plan", - MagicMock(return_value=iter([Msg("centre_plan")])), -) -@patch( - "mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan.set_energy_plan", - MagicMock(return_value=iter([])), -) -def test_given_sample_not_loaded_and_chi_changed_when_robot_load_called_then_eiger_staged_before_robot_and_centring_run( - robot_load_composite: RobotLoadThenCentreComposite, - robot_load_then_centre_params: RobotLoadThenCentre, - sim_run_engine: RunEngineSimulator, -): - robot_load_then_centre_params.sample_puck = (puck := 2) - robot_load_then_centre_params.sample_pin = (pin := 6) - robot_load_then_centre_params.chi_start_deg = 30 - - mock_current_sample(sim_run_engine, SampleLocation(1, 1)) - - messages = sim_run_engine.simulate_plan( - robot_load_then_centre( - robot_load_composite, - robot_load_then_centre_params, - ) - ) - - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" and msg.obj.name == "eiger_do_arm", - ) - - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "set" - and msg.obj.name == "robot" - and msg.args[0] == SampleLocation(puck, pin), - ) - - messages = assert_message_and_return_remaining( - messages, - lambda msg: msg.command == "centre_plan", - ) diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/robot_load/test_robot_load_ispyb_callback.py b/tests/unit_tests/hyperion/external_interaction/callbacks/robot_load/test_robot_load_ispyb_callback.py index cb77f8ec3..ad8e639ff 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/robot_load/test_robot_load_ispyb_callback.py +++ b/tests/unit_tests/hyperion/external_interaction/callbacks/robot_load/test_robot_load_ispyb_callback.py @@ -14,7 +14,7 @@ ) from mx_bluesky.hyperion.parameters.constants import CONST -VISIT_PATH = "/tmp/cm31105-4" +VISIT = "cm31105-4" SAMPLE_ID = 231412 SAMPLE_PUCK = 50 @@ -24,7 +24,7 @@ metadata = { "subplan_name": CONST.PLAN.ROBOT_LOAD, "metadata": { - "visit_path": VISIT_PATH, + "visit": VISIT, "sample_id": SAMPLE_ID, "sample_puck": SAMPLE_PUCK, "sample_pin": SAMPLE_PIN, diff --git a/tests/unit_tests/hyperion/external_interaction/test_ispyb_utils.py b/tests/unit_tests/hyperion/external_interaction/test_ispyb_utils.py index 351467e61..aadb32af3 100644 --- a/tests/unit_tests/hyperion/external_interaction/test_ispyb_utils.py +++ b/tests/unit_tests/hyperion/external_interaction/test_ispyb_utils.py @@ -4,7 +4,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import ( get_proposal_and_session_from_visit_string, - get_visit_string_from_path, ) from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import ( get_current_time_string, @@ -20,24 +19,6 @@ def test_get_current_time_string(): assert re.match(TIME_FORMAT_REGEX, current_time) is not None -@pytest.mark.parametrize( - "visit_path, expected_match", - [ - ("/dls/i03/data/2022/cm6477-45/", "cm6477-45"), - ("/dls/i03/data/2022/cm6477-45", "cm6477-45"), - ("/dls/i03/data/2022/mx54663-1/", "mx54663-1"), - ("/dls/i03/data/2022/mx54663-1", "mx54663-1"), - ("/dls/i03/data/2022/mx53-1/", None), - ("/dls/i03/data/2022/mx53-1", None), - ("/dls/i03/data/2022/mx5563-1565/", None), - ("/dls/i03/data/2022/mx5563-1565", None), - ], -) -def test_find_visit_in_visit_path(visit_path: str, expected_match: str): - test_visit_path = get_visit_string_from_path(visit_path) - assert test_visit_path == expected_match - - @pytest.mark.parametrize( "visit_string, expected_proposal, expected_session", [ diff --git a/tests/unit_tests/hyperion/parameters/test_parameter_model.py b/tests/unit_tests/hyperion/parameters/test_parameter_model.py index 9f0c670c5..cc6a59553 100644 --- a/tests/unit_tests/hyperion/parameters/test_parameter_model.py +++ b/tests/unit_tests/hyperion/parameters/test_parameter_model.py @@ -84,7 +84,6 @@ def test_robot_load_then_centre_params(): } params["detector_distance_mm"] = 200 test_params = RobotLoadThenCentre(**params) - assert test_params.visit_directory assert test_params.detector_params