-
Notifications
You must be signed in to change notification settings - Fork 5
Add code for robot load #1078
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
Add code for robot load #1078
Changes from all commits
fc54e6b
8d1f7c3
3039b7f
e5e7ff0
c63b537
a69fb98
00c3c5a
242cb5e
3f1db2d
6970e02
364f64a
f1a01a0
f8679bd
b073ea5
d80f559
03bc24e
db3cbbd
a8439ac
911ee2c
6e33ecc
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 |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import bluesky.plan_stubs as bps | ||
| import bluesky.preprocessors as bpp | ||
| from bluesky.utils import MsgGenerator | ||
| from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue | ||
| from dodal.devices.motors import XYZStage | ||
| from dodal.devices.robot import BartRobot | ||
| from dodal.devices.smargon import CombinedMove, Smargon, StubPosition | ||
| from dodal.plan_stubs.motor_utils import MoveTooLarge, home_and_reset_wrapper | ||
|
|
||
| from mx_bluesky.common.utils.log import LOGGER | ||
| from mx_bluesky.hyperion.parameters.constants import CONST | ||
|
|
||
|
|
||
| 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 _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 do_plan_while_lower_gonio_at_home(plan: MsgGenerator, lower_gonio: XYZStage): | ||
| """Moves the lower gonio to home then performs the provided plan and moves it back. | ||
|
|
||
| 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 need to wait for the move as the robot | ||
| is interlocked to the lower gonio and the move is quicker than the robot takes to | ||
| get to the load position. | ||
|
|
||
| Args: | ||
| plan (MsgGenerator): The plan to run while the lower gonio is at home. | ||
| lower_gonio (XYZStage): The lower gonio to home. | ||
| """ | ||
| yield from bpp.contingency_wrapper( | ||
| home_and_reset_wrapper( | ||
| plan, | ||
| 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, | ||
| ) | ||
| return "reset-lower_gonio" | ||
|
|
||
|
|
||
| def prepare_for_robot_load( | ||
| aperture_scatterguard: ApertureScatterguard, smargon: Smargon | ||
| ): | ||
| yield from bps.abs_set( | ||
| aperture_scatterguard.selected_aperture, | ||
| ApertureValue.OUT_OF_BEAM, | ||
| group="prepare_robot_load", | ||
| ) | ||
|
|
||
| yield from bps.mv(smargon.stub_offsets, StubPosition.RESET_TO_ROBOT_LOAD) | ||
|
|
||
| yield from bps.mv(smargon, CombinedMove(x=0, y=0, z=0, chi=0, phi=0, omega=0)) | ||
|
|
||
| yield from bps.wait("prepare_robot_load") | ||
|
|
||
|
|
||
| def robot_unload( | ||
| robot: BartRobot, | ||
| smargon: Smargon, | ||
| aperture_scatterguard: ApertureScatterguard, | ||
| lower_gonio: XYZStage, | ||
| visit: str, | ||
| ): | ||
| """Unloads the currently mounted pin into the location that it was loaded from. The | ||
| loaded location is stored on the robot and so need not be provided. | ||
| """ | ||
| yield from prepare_for_robot_load(aperture_scatterguard, smargon) | ||
|
Contributor
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. Think it might be worth noting here in a docstring, that it's implicit that the the puck/pin location is stored on the robot. |
||
| sample_id = yield from bps.rd(robot.sample_id) | ||
|
|
||
| @bpp.run_decorator( | ||
| md={ | ||
| "subplan_name": CONST.PLAN.ROBOT_UNLOAD, | ||
| "metadata": {"visit": visit, "sample_id": sample_id}, | ||
| "activate_callbacks": [ | ||
| "RobotLoadISPyBCallback", | ||
| ], | ||
| }, | ||
| ) | ||
| def do_robot_unload_and_send_to_ispyb(): | ||
| yield from bps.create(name=CONST.DESCRIPTORS.ROBOT_UPDATE) | ||
| yield from bps.read(robot) | ||
| yield from bps.save() | ||
|
|
||
| def _unload(): | ||
| yield from bps.trigger(robot.unload, wait=True) | ||
| yield from wait_for_smargon_not_disabled(smargon) | ||
|
|
||
| gonio_finished = yield from do_plan_while_lower_gonio_at_home( | ||
| _unload(), lower_gonio | ||
| ) | ||
| yield from bps.wait(gonio_finished) | ||
|
|
||
| yield from do_robot_unload_and_send_to_ispyb() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,9 @@ | ||
| import configparser | ||
| from dataclasses import dataclass | ||
| from enum import StrEnum | ||
| from typing import Any, Literal | ||
|
|
||
| from event_model.documents import Event | ||
| from requests import JSONDecodeError, patch, post | ||
| from requests.auth import AuthBase | ||
|
|
||
|
|
@@ -63,6 +65,19 @@ class BLSampleStatus(StrEnum): | |
| ) | ||
|
|
||
|
|
||
| def create_update_data_from_event_doc( | ||
|
Contributor
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.
Contributor
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. Neither of these modules owns the namespaces of the things being mapped. I don't really like this, but I can't come up with anything better that doesn't also increase verbosity. |
||
| mapping: dict[str, str], event: Event | ||
| ) -> dict[str, Any]: | ||
| """Given a mapping between bluesky event data and an event itself this function will | ||
| create a dict that can be used to update exp-eye.""" | ||
| event_data = event["data"] | ||
| return { | ||
| target_key: event_data[source_key] | ||
| for source_key, target_key in mapping.items() | ||
| if source_key in event_data | ||
| } | ||
|
|
||
|
|
||
| class ExpeyeInteraction: | ||
| """Exposes functionality from the Expeye core API""" | ||
|
|
||
|
|
@@ -74,24 +89,22 @@ def __init__(self) -> None: | |
| self._base_url = url | ||
| self._auth = BearerAuth(token) | ||
|
|
||
| def start_load( | ||
| def start_robot_action( | ||
| self, | ||
| action_type: Literal["LOAD", "UNLOAD"], | ||
| proposal_reference: str, | ||
| visit_number: int, | ||
| sample_id: int, | ||
| dewar_location: int, | ||
| container_location: int, | ||
| ) -> RobotActionID: | ||
| """Create a robot load entry in ispyb. | ||
| """Create a robot action entry in ispyb. | ||
|
|
||
| Args: | ||
| action_type ("LOAD" | "UNLOAD"): The robot action being performed | ||
| proposal_reference (str): The proposal of the experiment e.g. cm37235 | ||
| visit_number (int): The visit number for the proposal, usually this can be | ||
| found added to the end of the proposal e.g. the data for | ||
| visit number 2 of proposal cm37235 is in cm37235-2 | ||
| sample_id (int): The id of the sample in the database | ||
| dewar_location (int): Which puck in the dewar the sample is in | ||
| container_location (int): Which pin in that puck has the sample | ||
|
|
||
| Returns: | ||
| RobotActionID: The id of the robot load action that is created | ||
|
|
@@ -102,39 +115,28 @@ def start_load( | |
|
|
||
| data = { | ||
| "startTimestamp": get_current_time_string(), | ||
| "actionType": action_type, | ||
| "sampleId": sample_id, | ||
| "actionType": "LOAD", | ||
| "containerLocation": container_location, | ||
| "dewarLocation": dewar_location, | ||
| } | ||
| response = _send_and_get_response(self._auth, url, data, post) | ||
| return response["robotActionId"] | ||
|
|
||
| def update_barcode_and_snapshots( | ||
| def update_robot_action( | ||
| self, | ||
| action_id: RobotActionID, | ||
| barcode: str, | ||
| snapshot_before_path: str, | ||
| snapshot_after_path: str, | ||
| data: dict[str, Any], | ||
| ): | ||
| """Update the barcode and snapshots of an existing robot action. | ||
| """Update an existing robot action to contain additional info. | ||
|
|
||
| Args: | ||
| action_id (RobotActionID): The id of the action to update | ||
| barcode (str): The barcode to give the action | ||
| snapshot_before_path (str): Path to the snapshot before robot load | ||
| snapshot_after_path (str): Path to the snapshot after robot load | ||
| data (dict): The data to update with, where the keys match those expected | ||
| by exp-eye. | ||
| """ | ||
| url = self._base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id) | ||
|
|
||
| data = { | ||
| "sampleBarcode": barcode, | ||
| "xtalSnapshotBefore": snapshot_before_path, | ||
| "xtalSnapshotAfter": snapshot_after_path, | ||
| } | ||
| _send_and_get_response(self._auth, url, data, patch) | ||
|
|
||
| def end_load(self, action_id: RobotActionID, status: str, reason: str): | ||
| def end_robot_action(self, action_id: RobotActionID, status: str, reason: str): | ||
| """Finish an existing robot action, providing final information about how it went | ||
|
|
||
| Args: | ||
|
|
||
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.
There seems to be an asymmetry between robot load and robot unload in terms of usage.
robot_unloadmanages the preparation and the moving of the gonio, whereasrobot_load_and_snapshotsis outside of the module and calls these things separately. One is a device setup plan and the other is an experiment plan.Perhaps it would be better to consider moving
robot_load_and_snapshotsintorobot_load_unload- thawing and taking snapshots seem like a natural part of loading which people would want on any beamline if there is the facility to do so. Then the implementation functions can be private to the module, and it only exposes loading and unloading.Changing energy to me seems to be the only thing which is not naturally part of loading; you could supply an arbitrary plan as a parameter which you execute while waiting for the robot to load.
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.
Agreed, pulled into #1137