diff --git a/docs/developer/hyperion/hyperion-blueapi.rst b/docs/developer/hyperion/hyperion-blueapi.rst new file mode 100644 index 0000000000..b4fe1e082a --- /dev/null +++ b/docs/developer/hyperion/hyperion-blueapi.rst @@ -0,0 +1,35 @@ +Hyperion on BlueAPI +=================== + +This document describes the migration of Hyperion from a monolithic service that contains its own application server +and is only partially dependent on BlueAPI, +to a standard BlueAPI application deployment. + +Architecture +------------ + +Hyperion on BlueAPI consists of two components: + +* hyperion-blueapi: This is intended to ultimately be a standard blueapi installation, consisting of a beamline + module and a dodal plan module. In the interim, deployment may vary from the standard method until such time as + monolithic operation can be desupported. ``hyperion-blueapi`` exposes a minimal set of bluesky plans for UDC data + collection. + +* hyperion-supervisor: This will be a separate service that is responsible for fetching instructions from + Agamemnon, decoding them and sending corresponding requests to ``hyperion-blueapi`` for execution. The supervisor + also monitors the state of ``hyperion-blueapi``, manages the Hyperion baton and provides endpoints for status + monitoring. + +Deployment +---------- + +``hyperion-blueapi`` is automatically available in a standard Hyperion deployment. + +Launching +--------- + +``hyperion-blueapi`` can be launched in using the ``run_hyperion.sh`` script, using the ``--blueapi`` option: + +:: + + ./run_hyperion.sh --beamline=i03 --dev --blueapi diff --git a/docs/developer/hyperion/index.rst b/docs/developer/hyperion/index.rst index 81c1f9507e..a6f542affe 100644 --- a/docs/developer/hyperion/index.rst +++ b/docs/developer/hyperion/index.rst @@ -17,6 +17,7 @@ Documentation specific for the Hyperion module within MX-Bluesky reference/param-hierarchy reference/coordinate-systems system-tests + hyperion-blueapi +++ diff --git a/run_hyperion.sh b/run_hyperion.sh index 95f8c13066..80ce1b9b03 100755 --- a/run_hyperion.sh +++ b/run_hyperion.sh @@ -7,6 +7,9 @@ START=1 IN_DEV=false MODE=gda +CONFIG_DIR=`dirname $0`/src/mx_bluesky/hyperion +BLUEAPI_CONFIG=$CONFIG_DIR/blueapi_config.yaml + for option in "$@"; do case $option in -b=*|--beamline=*) @@ -22,10 +25,14 @@ for option in "$@"; do ;; --dev) IN_DEV=true + BLUEAPI_CONFIG=$CONFIG_DIR/blueapi_dev_config.yaml ;; --udc) MODE=udc ;; + --blueapi) + MODE=blueapi + ;; --help|--info|--h) source .venv/bin/activate echo "`basename $0` [options]" @@ -38,10 +45,9 @@ Options: --stop Used to stop a currently running instance of Hyperion. Will override any other operations options. --no-start Used to specify that the script should be run without starting the server. - --skip-startup-connection - Do not connect to devices at startup --dev Enable dev mode to run from a local workspace on a development machine. --udc Start hyperion in UDC mode instead of taking commands from GDA + --blueapi Start hyperion in blueapi mode instead of taking commands from GDA --help This help By default this script will start an Hyperion server unless the --no-start flag is specified. @@ -59,6 +65,7 @@ kill_active_apps () { echo "Killing active instances of hyperion and hyperion-callbacks..." pkill -e -f "python.*hyperion" pkill -e -f "SCREEN.*hyperion" + blueapi controller stop 2>/dev/null echo "done." } @@ -120,8 +127,6 @@ if [[ $START == 1 ]]; then source .venv/bin/activate - #Add future arguments here - declare -A h_and_cb_args=( ["IN_DEV"]="$IN_DEV" ) declare -A h_and_cb_arg_strings=( ["IN_DEV"]="--dev" ) @@ -136,8 +141,15 @@ if [[ $START == 1 ]]; then done unset PYEPICS_LIBCA - echo "Starting hyperion with hyperion $h_commands, start_log is $start_log_path" - hyperion `echo $h_commands;`>$start_log_path 2>&1 & + if [ $MODE = "blueapi" ]; then + echo "Starting hyperion in blueapi mode, start log is $start_log_path" + blueapi --config $BLUEAPI_CONFIG serve > $start_log_path 2>&1 & + HEALTHCHECK_ENDPOINT="healthz" + else + echo "Starting hyperion with hyperion $h_commands, start_log is $start_log_path" + hyperion `echo $h_commands;`>$start_log_path 2>&1 & + HEALTHCHECK_ENDPOINT="status" + fi echo "Starting hyperion-callbacks with hyperion-callbacks $cb_commands, start_log is $callback_start_log_path" hyperion-callbacks `echo $cb_commands;`>$callback_start_log_path 2>&1 & echo "$(date) Waiting for Hyperion to start" @@ -145,7 +157,7 @@ if [[ $START == 1 ]]; then for i in {1..30} do echo "$(date)" - curl --head -X GET http://localhost:5005/status >/dev/null + curl --head -X GET http://localhost:5005/$HEALTHCHECK_ENDPOINT >/dev/null ret_value=$? if [ $ret_value -ne 0 ]; then sleep 1 diff --git a/src/mx_bluesky/common/device_setup_plans/robot_load_unload.py b/src/mx_bluesky/common/device_setup_plans/robot_load_unload.py index ca01a5779b..868fed66ab 100644 --- a/src/mx_bluesky/common/device_setup_plans/robot_load_unload.py +++ b/src/mx_bluesky/common/device_setup_plans/robot_load_unload.py @@ -5,7 +5,7 @@ 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.robot import SAMPLE_LOCATION_EMPTY, BartRobot from dodal.devices.smargon import CombinedMove, Smargon, StubPosition from dodal.plan_stubs.motor_utils import MoveTooLargeError, home_and_reset_wrapper @@ -117,7 +117,7 @@ def do_robot_unload_and_send_to_ispyb(): yield from bps.save() def _unload(): - yield from bps.abs_set(robot, None, wait=True) + yield from bps.abs_set(robot, SAMPLE_LOCATION_EMPTY, wait=True) yield from wait_for_smargon_not_disabled(smargon) gonio_finished = yield from do_plan_while_lower_gonio_at_home( diff --git a/src/mx_bluesky/hyperion/blueapi_config.yaml b/src/mx_bluesky/hyperion/blueapi_config.yaml new file mode 100644 index 0000000000..da8bfcf231 --- /dev/null +++ b/src/mx_bluesky/hyperion/blueapi_config.yaml @@ -0,0 +1,18 @@ +env: + sources: + - kind: deviceManager + module: dodal.beamlines.i03 + - kind: planFunctions + module: mx_bluesky.hyperion.blueapi_plans + events: + broadcast_status_events: false +api: + url: http://localhost:5005 + cors: + allow_credentials: True + origins: + - "*" +logging: + graylog: + url: "tcp://graylog-log-target.diamond.ac.uk:12232" + enabled: true diff --git a/src/mx_bluesky/hyperion/blueapi_dev_config.yaml b/src/mx_bluesky/hyperion/blueapi_dev_config.yaml new file mode 100644 index 0000000000..16ac4e3eb5 --- /dev/null +++ b/src/mx_bluesky/hyperion/blueapi_dev_config.yaml @@ -0,0 +1,17 @@ +env: + sources: + - kind: deviceManager + module: dodal.beamlines.i03 + mock: true + - kind: planFunctions + module: mx_bluesky.hyperion.blueapi_plans + events: + broadcast_status_events: false +api: + url: http://localhost:5005 + cors: + allow_credentials: True + origins: + - "*" +logging: + level: DEBUG diff --git a/src/mx_bluesky/hyperion/blueapi_plans/__init__.py b/src/mx_bluesky/hyperion/blueapi_plans/__init__.py new file mode 100644 index 0000000000..cef1bab1d0 --- /dev/null +++ b/src/mx_bluesky/hyperion/blueapi_plans/__init__.py @@ -0,0 +1,77 @@ +""" +This module contains the bluesky plan entry points for use with hyperion-blueapi. +The json schema and documentation therein generated by the blueapi /plans endpoint +from this file constitutes the hyperion-blueapi interface to the hyperion supervisor +process. +""" + +from bluesky.utils import MsgGenerator +from dodal.common import inject +from dodal.devices.aperturescatterguard import ApertureScatterguard +from dodal.devices.motors import XYZStage +from dodal.devices.robot import BartRobot +from dodal.devices.smargon import Smargon + +from mx_bluesky.common.device_setup_plans.robot_load_unload import ( + robot_unload as _robot_unload, +) +from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import ( + LoadCentreCollectComposite, +) +from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import ( + load_centre_collect_full as _load_centre_collect_full, +) +from mx_bluesky.hyperion.experiment_plans.udc_default_state import ( + UDCDefaultDevices, +) +from mx_bluesky.hyperion.experiment_plans.udc_default_state import ( + move_to_udc_default_state as _move_to_udc_default_state, +) +from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect + +__all__ = [ + "LoadCentreCollectComposite", + "LoadCentreCollect", + "UDCDefaultDevices", + "load_centre_collect", + "move_to_udc_default_state", + "robot_unload", +] + + +def load_centre_collect( + parameters: LoadCentreCollect, composite: LoadCentreCollectComposite = inject() +) -> MsgGenerator: + """ + Attempt a complete data collection experiment, consisting of the following: + * Load the sample if necessary + * Move to the specified goniometer start angles + * Perform optical centring, then X-ray centring + * If X-ray centring finds one or more diffracting centres then for each centre + that satisfies the chosen selection function, + move to that centre and do a collection with the specified parameters. + """ + yield from _load_centre_collect_full(composite, parameters) + + +def robot_unload( + visit: str, + robot: BartRobot = inject("robot"), + smargon: Smargon = inject("smargon"), + aperture_scatterguard: ApertureScatterguard = inject("aperture_scatterguard"), + lower_gonio: XYZStage = inject("lower_gonio"), +) -> MsgGenerator: + """ + Unload the currently mounted pin into the location that it was loaded from. + This is to be invoked as the final step upon successful completion of the UDC queue. + """ + yield from _robot_unload(robot, smargon, aperture_scatterguard, lower_gonio, visit) + + +def move_to_udc_default_state( + composite: UDCDefaultDevices = inject(), +) -> MsgGenerator: + """ + Move beamline hardware to known positions prior to UDC start. + """ + yield from _move_to_udc_default_state(composite) diff --git a/tests/test_data/hyperion_blueapi/move_to_udc_default_state.json b/tests/test_data/hyperion_blueapi/move_to_udc_default_state.json new file mode 100644 index 0000000000..7a5e3cebfd --- /dev/null +++ b/tests/test_data/hyperion_blueapi/move_to_udc_default_state.json @@ -0,0 +1,5 @@ +{ + "name": "move_to_udc_default_state", + "params": {}, + "instrument_session": "cm12345-1" +} diff --git a/tests/test_data/hyperion_blueapi/robot_unload.json b/tests/test_data/hyperion_blueapi/robot_unload.json new file mode 100644 index 0000000000..f1e26039ae --- /dev/null +++ b/tests/test_data/hyperion_blueapi/robot_unload.json @@ -0,0 +1,7 @@ +{ + "name": "robot_unload", + "params": { + "visit": "cm12345-1" + }, + "instrument_session": "cm12345-1" +} diff --git a/tests/test_data/hyperion_blueapi/test_load_centre_collect.json b/tests/test_data/hyperion_blueapi/test_load_centre_collect.json new file mode 100644 index 0000000000..376eeace9f --- /dev/null +++ b/tests/test_data/hyperion_blueapi/test_load_centre_collect.json @@ -0,0 +1,56 @@ +{ + "name": "load_centre_collect", + "params": { + "parameters": { + "parameter_model_version": "5.0.0", + "beamline": "BL03S", + "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", + "insertion_prefix": "SR03S", + "visit": "cm31105-4", + "detector_distance_mm": 255, + "sample_id": 12345, + "sample_puck": 40, + "sample_pin": 3, + "robot_load_then_centre": { + "storage_directory": "{tmp_data}/123458/xraycentring", + "file_name": "robot_load_centring_file", + "comment": "Robot load and centre", + "exposure_time_s": 0.004, + "use_roi_mode": false, + "demand_energy_ev": 11100, + "run_number": 0, + "panda_runup_distance_mm": 0.17, + "box_size_um": 20 + }, + "multi_rotation_scan": { + "comment": "Rotation", + "storage_directory": "{tmp_data}/123458/", + "file_name": "file_name", + "exposure_time_s": 0.004, + "selected_aperture": "SMALL_APERTURE", + "transmission_frac": 1.0, + "demand_energy_ev": 11100, + "rotation_increment_deg": 0.1, + "shutter_opening_time_s": 0.6, + "snapshot_omegas_deg": [ + 0, + 90, + 180, + 270 + ], + "run_number": 1, + "rotation_scans": [ + { + "rotation_axis": "omega", + "rotation_direction": "Negative", + "scan_width_deg": 180.0, + "omega_start_deg": 0, + "phi_start_deg": 0.47, + "chi_start_deg": 23.85 + } + ] + } + } + }, + "instrument_session": "cm12345-1" +} diff --git a/tests/unit_tests/common/device_setup_plans/test_robot_load_unload.py b/tests/unit_tests/common/device_setup_plans/test_robot_load_unload.py index 1c5289f82f..f4de244c98 100644 --- a/tests/unit_tests/common/device_setup_plans/test_robot_load_unload.py +++ b/tests/unit_tests/common/device_setup_plans/test_robot_load_unload.py @@ -7,7 +7,7 @@ from bluesky.utils import Msg from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue from dodal.devices.motors import XYZStage -from dodal.devices.robot import BartRobot +from dodal.devices.robot import SAMPLE_LOCATION_EMPTY, BartRobot from dodal.devices.smargon import CombinedMove, Smargon, StubPosition from ophyd_async.core import get_mock_put, set_mock_value @@ -87,7 +87,9 @@ async def test_when_robot_unload_called_then_sample_area_prepared_before_load( assert_message_and_return_remaining( msgs, - lambda msg: msg.command == "set" and msg.obj is robot and msg.args[0] is None, + lambda msg: msg.command == "set" + and msg.obj is robot + and msg.args[0] == SAMPLE_LOCATION_EMPTY, ) assert_message_and_return_remaining( msgs, @@ -134,7 +136,9 @@ def locate_gonio(_): msgs = assert_message_and_return_remaining( msgs, - lambda msg: msg.command == "set" and msg.obj is robot and msg.args[0] is None, + lambda msg: msg.command == "set" + and msg.obj is robot + and msg.args[0] == SAMPLE_LOCATION_EMPTY, ) msgs = assert_message_and_return_remaining( diff --git a/tests/unit_tests/hyperion/test_blueapi_plans.py b/tests/unit_tests/hyperion/test_blueapi_plans.py new file mode 100644 index 0000000000..3e226c32c0 --- /dev/null +++ b/tests/unit_tests/hyperion/test_blueapi_plans.py @@ -0,0 +1,73 @@ +from pathlib import Path +from unittest.mock import patch + +import pytest +from blueapi.config import ApplicationConfig +from blueapi.core import BlueskyContext +from blueapi.worker import Task +from bluesky import RunEngine + +from .conftest import raw_params_from_file + + +@pytest.fixture +def bluesky_context(run_engine: RunEngine): + config = ApplicationConfig( + **{ + "env": { + "sources": [ + { + "kind": "deviceManager", + "module": "dodal.beamlines.i03", + "mock": True, + }, + { + "kind": "planFunctions", + "module": "mx_bluesky.hyperion.blueapi_plans", + }, + ] + } + } + ) + yield BlueskyContext(run_engine=run_engine, configuration=config) + + +def test_load_centre_collect(bluesky_context: BlueskyContext, tmp_path: Path): + params = raw_params_from_file( + "tests/test_data/parameter_json_files/good_test_load_centre_collect_params.json", + tmp_path, + ) + _call_blueapi_plan( + bluesky_context, + "load_centre_collect", + "_load_centre_collect_full", + {"parameters": params}, + ) + + +def test_robot_unload(bluesky_context: BlueskyContext, tmp_path: Path): + _call_blueapi_plan( + bluesky_context, "robot_unload", "_robot_unload", {"visit": "cm12345-67"} + ) + + +def test_move_to_udc_default_state(bluesky_context: BlueskyContext): + _call_blueapi_plan( + bluesky_context, "move_to_udc_default_state", "_move_to_udc_default_state", {} + ) + + +def _call_blueapi_plan( + bluesky_context: BlueskyContext, + plan_name: str, + internal_name: str, + parameters: dict, +): + with patch( + f"mx_bluesky.hyperion.blueapi_plans.{internal_name}", + return_value=iter([]), + create=False, + ) as mock_plan: + Task(name=plan_name, params=parameters).do_task(bluesky_context) + + mock_plan.assert_called_once()